Skip to content

feat: add select_raw and join_sub to the query builder - #330

Closed
claude-tmgbedu wants to merge 2 commits into
mainfrom
feat/select-raw-join-sub
Closed

claude-tmgbedu wants to merge 2 commits into
mainfrom
feat/select-raw-join-sub

Conversation

@claude-tmgbedu

Copy link
Copy Markdown

Summary

Adds two Laravel query-builder features to the ORM so downstream apps can move raw-SQL listings onto the builder (ADR 049).

  • select_raw(expression: str, bindings: Sequence[Any] = ()) -> Self: appends a raw select expression with bindings. It is never split on commas. If nothing else is selected, only the raw expression is selected, as in Laravel.
  • join_sub(query, alias, column1, equality, column2, bindings=(), clause="inner") -> Self and left_join_sub(query, alias, column1, equality, column2, bindings=()) -> Self: join (subquery) AS "alias" ON .... query is a QueryBuilder, which uses its own bindings, or a raw SQL string with bindings. Passing bindings with a builder raises TypeError.

Binding order

  • _compile_select now compiles clauses in SQL order: columns, joins, wheres, group by, having, order by. Before this change, order-by bindings were collected before group-by bindings.
  • process_joins compiles a join's subquery, and collects its bindings, before the ON clause.
  • Limit and offset are inlined integers, so they have no bindings.

count() / paginate()

  • paginate() already copies _joins into its count query. The subquery join and its bindings are counted, and the select_raw columns are not.
  • aggregate() (count, sum, min, max, avg, exists) on an ungrouped query now leaves out the selected columns and the order by, along with their bindings, and no longer mutates the builder. Before, Postgres rejected count() with column "lexemes.id" must appear in the GROUP BY clause. Queries with group_by or having behave as before, which the existing MySQL test_group_by/test_having tests assert.

Tests

  • tests/masoniteorm/query/grammars/test_select_raw_join_sub_grammar.py: SQL and bindings for all four dialects, covering select_raw, join_sub with a builder, left_join_sub with raw SQL and bindings, and the combined listing query (to_qmark() and to_sql()). Also covers aggregate SQL.
  • tests/masoniteorm/sqlite/builder/test_sqlite_select_raw_join_sub.py and tests/masoniteorm/postgres/builder/test_postgres_select_raw_join_sub.py: run the listing query (Postgres uses GREATEST(MIN(...), CAST(? AS date))), count(), and paginate() for page 1 and page 2.
  • uv run pytest tests/masoniteorm with MySQL and Postgres from docker-compose.yml: 1122 passed, 6 skipped. ruff check and ruff format --check are clean.

Removed features

None.

🤖 Generated with Claude Code

Why:
- Downstream listings need a raw select expression with bindings; select() splits on commas and has no way to bind values.
- They also need to join a subquery (LEFT JOIN (SELECT ... GROUP BY ...) AS due), which join() cannot express.
- Bindings must follow placeholder order across select, join, where and order by; the grammar compiled ORDER BY before GROUP BY and built ON before the joined table.
- count() kept selected columns and ORDER BY beside COUNT(*), which Postgres rejects.

How:
- QueryBuilder.select_raw(expression, bindings) appends a raw SelectExpression carrying its bindings, never split on commas.
- QueryBuilder.join_sub(query, alias, column1, equality, column2, bindings, clause) and left_join_sub(...) add a SubqueryJoinClause; query is a builder (its own bindings) or a raw SQL string with bindings.
- BaseGrammar compiles select clauses in SQL order and compiles a join's subquery before its ON clause.
- aggregate() on an ungrouped query drops the columns and order by with their bindings and no longer mutates the builder; grouped queries are unchanged.
- ADR 049 records the decision; grammar tests cover all four dialects, and SQLite and Postgres tests run the listing query, count() and paginate().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Why:
- Chaining order_by_raw calls joined the fragments with no comma, producing invalid SQL such as "DESClexemes.text".

How:
- The grammar adds the ", " separator before a raw order fragment when an order is already present, as it already did for column and subquery orders.
- Grammar tests cover raw after raw and raw after a column order on all four dialects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants