[All] Fix $count/extras filter columns being re-aliased to the root table in relational queries#5998
Conversation
When a nested SQL embeds columns of another table — e.g. a `$count(otherTable, <filter>)` used as a relational-query `extras` value — `mapColumnsInSQLToAlias` rewrote *every* column in that SQL to the root query's alias, including the foreign table's columns. The generated subquery then referenced a column that doesn't exist on the aliased table (e.g. `users.owner_id` instead of `posts.owner_id`), throwing at execution time. Thread the table being aliased into `mapColumnsInSQLToAlias` / `mapColumnsInAliasedSQLToAlias` and only re-alias columns that actually belong to it (compared by original, pre-alias schema-qualified name so alias proxies still match). When the table is omitted the previous behaviour is preserved. Applied to the where / extras / orderBy call sites of the relational query builder across all dialects (pg, mysql, sqlite, singlestore, gel). Fixes drizzle-team#4169 Fixes drizzle-team#4836 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27d1721fe5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!is(columnTable, Table)) { | ||
| return true; | ||
| } | ||
| return tableOriginalUniqueName(columnTable) === tableOriginalUniqueName(table); |
There was a problem hiding this comment.
Avoid re-aliasing aliased self-table columns
When the nested SQL uses a second alias of the same physical table (common for self-relations), this original-name comparison treats that foreign alias as belonging to the outer RQB table. For example, a users extras $count over users as invitees with a filter like eq(invitees.invitedBy, users.id) will still rewrite invitees.invited_by to the outer users alias, producing the same invalid or wrongly correlated count this change is trying to avoid. The ownership check needs to distinguish aliases of the same base table rather than matching only the original schema/name.
Useful? React with 👍 / 👎.
What this fixes
Fixes #4169
Fixes #4836
When a
$count(otherTable, <filter>)(or any nested SQL that references another table's columns) is used as anextrasvalue in a relational query, the generatedcountsubquery rewrites every column — including the other table's columns — to the root query's alias. The subquery then references a column that doesn't exist and the query throws.Reproduction
Before —
job_candidacycolumns get the rootcandidatesalias:After — only root-table columns are re-aliased:
Root cause
mapColumnsInSQLToAlias(drizzle-orm/src/alias.ts) walks every chunk of the SQL and appliesaliasedTableColumn(c, alias)to anyColumn, with no check that the column belongs to the table being aliased. The relational query builder calls it for thewhere,extrasandorderByvalues of each query, so columns of unrelated tables embedded in those SQLs (via$count, correlated subqueries, etc.) get the wrong qualifier.The change
mapColumnsInSQLToAlias/mapColumnsInAliasedSQLToAliastake an optionaltableargument. When provided, a column is only re-aliased if it belongs to that table. Ownership is compared by the table's original (pre-alias) schema-qualified name, so columns referenced through an alias proxy still match the underlying table. Whentableis omitted, the previous behaviour (re-alias every column) is preserved, so existing callers are unaffected.where,extras,orderBy) now pass the query's table, across all dialects: pg, mysql, sqlite, singlestore and gel.Tests
Added a regression test to
integration-tests/tests/relational/bettersqlite.test.tsthat runs a$count(postsTable, eq(postsTable.ownerId, usersTable.id))insideextras, asserts the generated SQL keeps"posts"."owner_id"(never"users"."owner_id"), and executes it against an in-memory database to confirm the correct per-row counts. The test fails onmain(throwsno such column) and passes with this change; the rest of the relational sqlite suite continues to pass.