Skip to content

[2b] Add typed select() for column selection - #89

Draft
davegaeddert wants to merge 4 commits into
typed-wherefrom
select
Draft

[2b] Add typed select() for column selection#89
davegaeddert wants to merge 4 commits into
typed-wherefrom
select

Conversation

@davegaeddert

@davegaeddert davegaeddert commented Jul 23, 2026

Copy link
Copy Markdown
Member

What

Adds QuerySet.select(*items, flat=False, result_type=None) — typed column selection that returns honest rows (tuples, flat scalars, or dataclass instances) via a new RowQuerySet[R], never partial model instances.

User.query.where(...).select(User.id, User.email)       # RowQuerySet[tuple[int, str]]
User.query.select(User.email, flat=True)                # RowQuerySet[str]
User.query.select(User.id, User.email, result_type=UserStats)  # RowQuerySet[UserStats]

Stacked on typed-where (base of this PR), which provides the typed field-access surface.

Design

  • Selectable[T] baseField[T] and BaseExpression subclass a shared generic marker so select() can bind each column's value type. The generic base shifts the C-level solid base, so the Empty stand-ins that Field and Query cloning reassign __class__ onto also subclass Selectable to stay layout-compatible.
  • Rows, not instances — reuses the values_list iterable machinery. first()/get()/iteration/slicing return rows; update()/delete() and values()/values_list() raise TypeError on a selected queryset, matching post-values() behavior.
  • Fields resolve by name; expressions get internal auto-aliases via the existing values_list plumbing. result_type maps columns onto dataclass fields positionally (arity + field-name checks at select() time).

Typing

The overload ladder is keyed on Selectable[T] itself: a TYPE_CHECKING-only annotated member (__plain_selected_type__) on Selectable gives the type checker something to unify T against inside the overloaded, generic select() method. Pure-field selects get precise per-column types; a select that mixes in an expression types that expression's column as Any while the fields around it stay precise — e.g. select(D.priority, Upper("name"))RowQuerySet[tuple[int, Any]]. This is asserted in test_select_typing.py::test_mixed_field_and_expression_row and documented in the README and code.

Not in this slice

FK traversal refs (User.profile.city) raise a clear TypeError. values()/values_list()/only()/defer()/annotate() are untouched — the consumer sweep is a later slice.

Tests / checks

./scripts/type-check plain-postgres green (ty markers accurate), full ./scripts/test green (830 passed in plain-postgres, all packages pass).

Introduce a minimal generic Selectable[T] marker that Field[T] and
BaseExpression subclass, giving select() a single base to bind each
column's value type against. Because the generic base shifts the C-level
solid base, the Empty stand-ins used by Field and Query cloning subclass
Selectable so their __class__-reassignment stays layout-compatible.
select(*items, flat=False, result_type=None) selects specific columns as
tuples, flat scalars, or dataclass instances via RowQuerySet[R], never
partial model instances. Reuses the values_list plumbing for SQL and row
building; writes and values()/values_list() are blocked on a selected
queryset.

The overload ladder binds each column's type through Field[T]: the pinned
ty build unwraps a Field parameter but not the wider Selectable base
inside an overloaded method on a generic class, so fields get precise
per-column types and expression-containing selects type as tuple[Any, ...].
Runtime tests cover tuple/flat/dataclass modes, expression columns,
chaining with where(), and the error paths. Static-typing tests assert
the precise row types and carry load-bearing ty:ignore markers on the
misuse cases. README documents the three modes and why select() returns
rows rather than partial model instances.
@davegaeddert davegaeddert changed the title Add typed select() for column selection [merge 7] Add typed select() for column selection Jul 23, 2026
@davegaeddert davegaeddert changed the title [merge 7] Add typed select() for column selection [2b] Add typed select() for column selection Jul 23, 2026
Give Selectable[T] a TYPE_CHECKING-only annotated member referencing T so
ty can solve the per-column typevar, then re-key the whole select() overload
ladder from Field[T] back to Selectable[T]. Mixing an expression into a
select() now keeps the field columns precise: select(D.priority, Upper("name"))
types as RowQuerySet[tuple[int, Any]] instead of RowQuerySet[tuple[Any, ...]].

Also:
- Drop the unreachable RowQuerySet guards in values()/values_list() (the
  RowQuerySet overrides are the single enforcement point).
- Build select(result_type=...) dataclasses positionally when there are no
  kw-only fields, deciding the strategy once instead of per row.
- Hoist the related_typed import out of _selectable_to_column so it runs at
  most once per select() call.
- Collapse the duplicated Empty solid-base comments to point at selectable.py.
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