[2b] Add typed select() for column selection - #89
Draft
davegaeddert wants to merge 4 commits into
Draft
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 newRowQuerySet[R], never partial model instances.Stacked on
typed-where(base of this PR), which provides the typed field-access surface.Design
Selectable[T]base —Field[T]andBaseExpressionsubclass a shared generic marker soselect()can bind each column's value type. The generic base shifts the C-level solid base, so theEmptystand-ins that Field and Query cloning reassign__class__onto also subclassSelectableto stay layout-compatible.values_listiterable machinery.first()/get()/iteration/slicing return rows;update()/delete()andvalues()/values_list()raiseTypeErroron a selected queryset, matching post-values()behavior.values_listplumbing.result_typemaps columns onto dataclass fields positionally (arity + field-name checks atselect()time).Typing
The overload ladder is keyed on
Selectable[T]itself: aTYPE_CHECKING-only annotated member (__plain_selected_type__) onSelectablegives the type checker something to unifyTagainst inside the overloaded, genericselect()method. Pure-field selects get precise per-column types; a select that mixes in an expression types that expression's column asAnywhile the fields around it stay precise — e.g.select(D.priority, Upper("name"))→RowQuerySet[tuple[int, Any]]. This is asserted intest_select_typing.py::test_mixed_field_and_expression_rowand documented in the README and code.Not in this slice
FK traversal refs (
User.profile.city) raise a clearTypeError.values()/values_list()/only()/defer()/annotate()are untouched — the consumer sweep is a later slice.Tests / checks
./scripts/type-check plain-postgresgreen (ty markers accurate), full./scripts/testgreen (830 passed in plain-postgres, all packages pass).