Skip to content

[All] Fix $count/extras filter columns being re-aliased to the root table in relational queries#5998

Open
Develop-KIM wants to merge 1 commit into
drizzle-team:mainfrom
Develop-KIM:fix/rqb-extras-foreign-column-alias
Open

[All] Fix $count/extras filter columns being re-aliased to the root table in relational queries#5998
Develop-KIM wants to merge 1 commit into
drizzle-team:mainfrom
Develop-KIM:fix/rqb-extras-foreign-column-alias

Conversation

@Develop-KIM

Copy link
Copy Markdown

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 an extras value in a relational query, the generated count subquery 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

db.query.candidates.findMany({
  extras: {
    activeJobs: db.$count(
      jobCandidacy,
      and(
        eq(jobCandidacy.candidateId, candidates.id),
        not(inArray(jobCandidacy.status, ['rejected', 'withdrawn'])),
      ),
    ).as('activeJobs'),
  },
});

Beforejob_candidacy columns get the root candidates alias:

(select count(*) from "job_candidacy"
 where ("candidates"."candidate_id" = "candidates"."id"
        and not "candidates"."status" in ($1, $2)))
-- ERROR: column candidates.candidate_id does not exist

After — only root-table columns are re-aliased:

(select count(*) from "job_candidacy"
 where ("job_candidacy"."candidate_id" = "candidates"."id"
        and not "job_candidacy"."status" in ($1, $2)))

Root cause

mapColumnsInSQLToAlias (drizzle-orm/src/alias.ts) walks every chunk of the SQL and applies aliasedTableColumn(c, alias) to any Column, with no check that the column belongs to the table being aliased. The relational query builder calls it for the where, extras and orderBy values of each query, so columns of unrelated tables embedded in those SQLs (via $count, correlated subqueries, etc.) get the wrong qualifier.

The change

  • mapColumnsInSQLToAlias / mapColumnsInAliasedSQLToAlias take an optional table argument. 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. When table is omitted, the previous behaviour (re-alias every column) is preserved, so existing callers are unaffected.
  • Columns that can't be attributed to a concrete table (e.g. view / subquery selections) fall back to the old behaviour.
  • The relational-query call sites (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.ts that runs a $count(postsTable, eq(postsTable.ownerId, usersTable.id)) inside extras, 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 on main (throws no such column) and passes with this change; the rest of the relational sqlite suite continues to pass.

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread drizzle-orm/src/alias.ts
if (!is(columnTable, Table)) {
return true;
}
return tableOriginalUniqueName(columnTable) === tableOriginalUniqueName(table);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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.

[BUG]: Incorrect table name used in count query [BUG]: Table names not referenced correctly in WHERE clause

1 participant