Skip to content

feat(orm): order_by() accepts a correlated-subquery builder#169

Open
tmgbedu wants to merge 2 commits into
mainfrom
task/orm-orderby-subquery-895a
Open

feat(orm): order_by() accepts a correlated-subquery builder#169
tmgbedu wants to merge 2 commits into
mainfrom
task/orm-orderby-subquery-895a

Conversation

@tmgbedu

@tmgbedu tmgbedu commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up A from splitting #168. Adds correlated-subquery ordering to the async query builder:

posts = await Post.query().order_by(
    Category.query().select("name").where_column("categories.id", "posts.category_id")
).paginate(per_page=15, page=1)

order_by() now accepts a QueryBuilder as its column argument and compiles (subquery) ASC|DESC, mirroring the existing SubSelectExpression handling in where(). Subquery bindings are propagated in the correct position (after WHERE). String-based order_by() is unchanged.

Tests

  • tests/masoniteorm/query/grammars/test_order_by_subquery_grammar.py — grammar-level to_sql()/to_qmark() + binding-order assertions across all 4 dialects (SQLite, MySQL, Postgres, MSSQL).
  • tests/masoniteorm/sqlite/builder/test_sqlite_order_by_subquery.py — real sqlite-backed suite (categories deliberately not in id order), including the full order_by(subquery) → paginate() chain with paginate-meta assertions.

Gates

  • uv run pytest --ignore=tests/masoniteorm/postgres --cov → 1807 passed / 7 skipped, coverage 78.87% (≥ fail_under 68).
  • ruff check + ruff format --check clean.
  • Zero regression to existing string-based order_by().

Relationship to other PRs

Independent of #168 at the code level (the correlated subquery here uses the 2-arg where_column that already exists on main). #168 adds the operator form of where_column; if you want the operator form in ordering subqueries, merge #168 first. Recommended overall order: #168 → this → follow-up B.

order_by() now accepts a QueryBuilder as its column argument and compiles
'(subquery) ASC|DESC', mirroring the existing SubSelectExpression handling
in where(). Subquery bindings are propagated in the correct position
(after WHERE, before nothing that follows ORDER BY). String-based
order_by() is unchanged.

Covered by grammar-level to_sql()/to_qmark() assertions with binding-order
checks across all four dialects (SQLite, MySQL, Postgres, MSSQL), plus a
real sqlite-backed suite (categories not in id order) including the full
order_by(subquery) -> paginate chain.
@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.45455% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...startkit/masoniteorm/query/grammars/BaseGrammar.py 91.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@tmgbedu

tmgbedu commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

✅ Code Review Verdict: APPROVE (do not merge — approval only, per process)

Reviewed for every risk area in the assignment. All pass. (Posted as a comment because GitHub blocks self-approval of one's own PR.)

Correctness & binding order

  • Binding order SELECT-sub → WHERE → ORDER-BY-sub is correct. In BaseGrammar._compile_select the format kwargs evaluate left-to-right (columns=wheres=order_by=); each process_* appends to the grammar's _bindings in that order. process_order_by(qmark=True) calls builder.to_qmark() then add_binding(*builder._bindings), placing ORDER-BY subquery bindings last. Verified live → bindings ['active', 1].
  • Injection-safe. Adversarial probe with x'; DROP TABLE posts;-- inside the ordering subquery's where(): value is bound via ?, never interpolated (DROP TABLE absent from compiled SQL). Execution paths (get_models, paginate) use to_qmark() + get_bindings().
  • Paginate end-to-end (sqlite). count builder copies only _wheres/_joins/_global_scopes, not _order_by, so the ORDER BY subquery never pollutes the count query; paginate meta is correct with correlated ordering that differs from id order.

Per-grammar

  • Grammar suite asserts exact to_sql()/to_qmark() for SQLite, MySQL, Postgres, MSSQL (({query}) ASC|DESC), incl. MSSQL.

Regression

  • String order_by() unchanged; OrderByExpression sets direction/raw/bindings/builder before the early return. Existing order tests (14) pass.

Gates reproduced from fastapi_startkit/

  • New tests: 8 passed / 16 subtests. Targeted order tests: 14 passed. ruff check + ruff format --check clean.

Test quality

  • Real sqlite-backed suite (categories deliberately out of id order) + grammar-level parity across 4 dialects. No hollow mocks.

Minor (non-blocking): double .upper() on direction; MSSQL/Postgres verified at grammar/string level only (consistent with repo's postgres-ignored strategy). Neither affects correctness.

Replace raw conn.execute CREATE TABLE / INSERT with the framework's Schema
builder and Model.insert(). Same tables and data (categories 1=Zebra,
2=Apple, 3=Mango; posts 1-4), same assertions.
@tmgbedu

tmgbedu commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

✅ Light re-review (force-push / task #911 fixture change) — APPROVE holds

Re-reviewed the single new commit 3ffe9bf8 (test-only). My original approval stands.

  • Production code unchanged. expressions.py, builder.py, BaseGrammar.py are byte-identical to the version I approved; the new commit touches only test_sqlite_order_by_subquery.py (24 lines in asyncSetUp).
  • (a) Schema tables/columns match test needs. categories(id, name) via table.id()+table.string("name"); posts(id, title, category_id) via table.id()+table.string("title")+table.integer("category_id"). Matches the correlated subquery (SELECT name … WHERE categories.id = posts.category_id).
  • (b) Seeded rows identical. categories 1=Zebra, 2=Apple, 3=Mango; posts (1,z,1)(2,a,2)(3,m,3)(4,a2,2) — same as the previous raw INSERTs, via Model.insert() with explicit ids.
  • (c) No raw DDL/DML left. grep confirms zero conn.execute and the fixtures.db.DB import is removed.
  • (d) Assertions unchanged & pass. The 4 test-method bodies are untouched; all 4 pass (ordering asc/desc, paginate meta, string order_by).
  • (e) Gates green. Target file 4 passed; sqlite/builder dir 53 passed (no cross-test contamination from the shared schema fixtures); ruff check + ruff format --check clean.

Do not merge — approval only, per process.

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