Skip to content

Fix: decode values()/values_list() columns to their field's Python type#13

Merged
vsdudakov merged 2 commits into
mainfrom
fix/values-list-decode-field-types
Jul 6, 2026
Merged

Fix: decode values()/values_list() columns to their field's Python type#13
vsdudakov merged 2 commits into
mainfrom
fix/values-list-decode-field-types

Conversation

@vsdudakov

@vsdudakov vsdudakov commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Problem

values() / values_list() returned whatever the driver handed back, skipping the per-field read decoder that instance hydration applies. Instance hydration (_from_db_row) runs every column through the field's dialect decoder, but _fetch_columns — which backs values/values_list — returned engine.fetch_rows(...) untouched.

On SQLite (stores NUMERIC as text) this means a DecimalField comes back as str from values_list but as Decimal from obj.balance:

oa, rate = (await TransferFunds.all().values_list("outgoing_amount", "exchange_rate_to_rub"))[0]
oa * rate   # TypeError: can't multiply sequence by non-int of type 'str'

It's a general correctness bug — not decimal-specific. Backends that don't express a type natively (MySQL CHAR(36) uuids, naive datetimes) were similarly under-converted through values/values_list.

Fix

python/yara_orm/queryset.py:

  1. _terminal_field(path) — new helper resolving a values() path to the Field it selects, mirroring _resolve_column's traversal (local column, pk, a relation name, a forward-relation chain, or a single reverse-FK/M2M hop). Returns None for anything without a single owning field, so those values pass through untouched.
  2. _fetch_columns now applies dialect.read_decoder(field) per column — the same decoder instance hydration uses. Computed once per query, applied in place. It's a no-op where the driver already returns the native type (e.g. Postgres Decimal), since to_python is idempotent, so other backends are unaffected.

Result: a values()/values_list() value now has the same Python type as the attribute on a hydrated instance, including values that traverse a relation (ledger__balance).

Tests

  • New regression tests in tests/test_values_and_traversal.py covering local and relation-traversed decimals (values(), values_list() tuple + flat=True), parametrized across all backends.
  • Full SQLite suite: 1179 passed, 12 skipped. ruff check / ruff format clean.

vsdudakov and others added 2 commits July 6, 2026 12:56
values()/values_list() returned raw driver values without applying the
per-field read decoder that instance hydration uses. On SQLite (which
stores NUMERIC as text) a DecimalField came back as str from
values_list() but as Decimal from obj.field, so arithmetic on a
projected decimal raised "TypeError: can't multiply sequence by non-int
of type 'str'". Backends that don't express a type natively (MySQL
CHAR(36) uuids, naive datetimes) were similarly under-converted.

_fetch_columns now resolves each projected path to its terminal Field
(new _terminal_field helper, mirroring _resolve_column's traversal —
local columns, pk, forward-relation chains, and a single reverse-FK/M2M
hop) and applies that field's dialect read decoder, so a values()/
values_list() value has the same type as the attribute on a hydrated
instance. The decoder is idempotent where the driver already returns the
native type (e.g. Postgres Decimal), so other backends are unaffected.

Adds regression tests covering local and relation-traversed decimals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the code-review findings on the values()/values_list() decode change:

- The decoder was applied only in _fetch_columns, so annotate()/group_by() +
  values() (the _values_grouped path) still returned raw driver values — the
  exact TypeError the fix set out to remove was reproducible whenever an
  annotation or group_by was present. _grouped_select_sql now yields the
  terminal field per output column (None for aggregates) and _values_grouped
  decodes field columns through the same read decoder.

- The separate _terminal_field resolver mirrored _resolve_column's traversal
  but returned None for a trailing `pk` alias and for bare relation/M2M names,
  so those projections skipped decoding and diverged from the equivalent
  relation projection (visible as a raw-str uuid pk on MySQL/Oracle/SQL Server).
  Folded it into a single _resolve_column_field that returns (column, field) in
  one traversal; _resolve_column is now a thin wrapper and _terminal_field is
  gone, so the SELECT and its decoder can never drift.

Shared _column_decoders/_decode_rows helpers back both projection paths. Adds
regression tests for annotate()/group_by() + values() decode, relation-pk decode
consistency (uuid pk), and a NULL nullable decoded column.
@vsdudakov vsdudakov merged commit f6fee6a into main Jul 6, 2026
4 checks passed
@vsdudakov vsdudakov deleted the fix/values-list-decode-field-types branch July 6, 2026 09:43
vsdudakov added a commit that referenced this pull request Jul 8, 2026
…pe (#13)

* Fix: decode values()/values_list() columns to their field's Python type

values()/values_list() returned raw driver values without applying the
per-field read decoder that instance hydration uses. On SQLite (which
stores NUMERIC as text) a DecimalField came back as str from
values_list() but as Decimal from obj.field, so arithmetic on a
projected decimal raised "TypeError: can't multiply sequence by non-int
of type 'str'". Backends that don't express a type natively (MySQL
CHAR(36) uuids, naive datetimes) were similarly under-converted.

_fetch_columns now resolves each projected path to its terminal Field
(new _terminal_field helper, mirroring _resolve_column's traversal —
local columns, pk, forward-relation chains, and a single reverse-FK/M2M
hop) and applies that field's dialect read decoder, so a values()/
values_list() value has the same type as the attribute on a hydrated
instance. The decoder is idempotent where the driver already returns the
native type (e.g. Postgres Decimal), so other backends are unaffected.

Adds regression tests covering local and relation-traversed decimals.


* fix(values): decode grouped/annotated projections and unify the resolver

Addresses the code-review findings on the values()/values_list() decode change:

- The decoder was applied only in _fetch_columns, so annotate()/group_by() +
  values() (the _values_grouped path) still returned raw driver values — the
  exact TypeError the fix set out to remove was reproducible whenever an
  annotation or group_by was present. _grouped_select_sql now yields the
  terminal field per output column (None for aggregates) and _values_grouped
  decodes field columns through the same read decoder.

- The separate _terminal_field resolver mirrored _resolve_column's traversal
  but returned None for a trailing `pk` alias and for bare relation/M2M names,
  so those projections skipped decoding and diverged from the equivalent
  relation projection (visible as a raw-str uuid pk on MySQL/Oracle/SQL Server).
  Folded it into a single _resolve_column_field that returns (column, field) in
  one traversal; _resolve_column is now a thin wrapper and _terminal_field is
  gone, so the SELECT and its decoder can never drift.

Shared _column_decoders/_decode_rows helpers back both projection paths. Adds
regression tests for annotate()/group_by() + values() decode, relation-pk decode
consistency (uuid pk), and a NULL nullable decoded column.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant