Skip to content

feat(orm): type QueryBuilder.get() as Collection[TModel] - #210

Merged
tmgbedu merged 3 commits into
mainfrom
feat/orm-get-collection-typing
Aug 23, 2026
Merged

feat(orm): type QueryBuilder.get() as Collection[TModel]#210
tmgbedu merged 3 commits into
mainfrom
feat/orm-get-collection-typing

Conversation

@tmgbedu

@tmgbedu tmgbedu commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

What

Make QueryBuilder.get() return a typed Collection[TModel] instead of an untyped value, so await User.where(...).get() types as Collection[User] (previously Any).

Changes

  • Collection is now generic (Collection[T]) with TYPE_CHECKING-only element-access stubs (first, __iter__, __getitem__). Runtime behaviour is unchanged — the base class supplies all implementations.
  • QueryBuilder.get() / get_models() annotated to return Collection[TModel].

As a result, element access flows through:

  • resultsCollection[User]
  • results[0]User
  • for u in resultsUser
  • results.first()User | None

Collateral (typing surfaced these)

  • Collection.load() gets one # type: ignore — it only runs on model collections, but the ORM Collection is legitimately generic over non-model values too (e.g. Collection(relation._get_value(...))), so T can't be bounded to Model.
  • chunk_by_id switched results.last().get_attributes()results[-1]... — equivalent (non-emptiness is already guaranteed by the count_results != 0 guard above it) and cleanly typed.

Scope

Kept intentionally small: the element stubs are typing-only, and the base Collection and the ~35 chainable builder methods were not touched. A chain like order_by().limit().get() still yields Collection[Unknown] (those intermediate methods drop the type param); the user-facing Model.where(...).get() path is fully typed.

Verification

  • 447 passed / 6 skipped across tests/masoniteorm/.
  • Pyright: test files 0 errors; Collection.py 0 errors; the remaining builder errors are all pre-existing (tuple bindings, None str defaults) and unrelated.

🤖 Generated with Claude Code

Make the ORM Collection generic (Collection[T]) with TYPE_CHECKING-only
element-access stubs (first/__iter__/__getitem__), and annotate
QueryBuilder.get()/get_models() to return Collection[TModel]. So
`User.where(...).get()` now types as Collection[User] instead of Any,
and element access (iteration, indexing, first()) yields User.

- load() gets one `# type: ignore` (runs only on model collections, but
  the ORM Collection is legitimately generic over non-model values too).
- chunk_by_id uses results[-1] (equivalent; non-emptiness already
  guaranteed) so the last row is cleanly typed.

Stubs are typing-only; runtime behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

tmgbedu and others added 2 commits August 23, 2026 06:52
load()'s `else: add_relation` branch was the only uncovered line in
Collection.py — no built-in relationship reaches it, since every
get_related() returns a Collection for a collection input. Add two
tests using a stub relationship whose get_related() returns a
non-Collection value, covering both the truthy-mapped and falsy-mapped
(stored as None) cases. Collection.py is now at 100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tmgbedu
tmgbedu merged commit 42463a0 into main Aug 23, 2026
6 checks passed
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