Skip to content

driver-sql: an unresolvable column on distinct() answers DATABASE_ERROR/500 where find()/count() answer INVALID_FILTER/400 — the #8790 refusal never reached the distinct door #17857

Description

@os-musk

Found while landing #17639 (the ADR-0112 envelope on the distinct() door). That card landed the TERMINAL envelope only — deliberately, following the precedent #11455 set on aggregate(), which left the identical gap FILED rather than guessed at and had it closed a card later by #11541. This is the distinct() half of that same gap.

Measured

driver-sql on branch claude/issue-17639-distinct-envelope (bbdc4ac74c, i.e. with #17639's envelope already in place), one object with a single title column, on two dialects. Every row below is the SAME condition — one column name the table does not have — asked at a different read door:

door outcome message head
count(t, { where: { nosuchcol: 1 } }) INVALID_FILTER / 400 Filter on 'nosuchcol' names a column that object 't' has no column for
find(t, { where: { nosuchcol: 1 } }) INVALID_FILTER / 400 same
aggregate(t, { groupBy: ['nosuchcol'], ... }) INVALID_FIELD / 400 The groupBy of this query names 'nosuchcol', a column that object 't' has no column for
distinct(t, 'title', { nosuchcol: 1 }) DATABASE_ERROR / 500 The database refused to run this query for object 't'. The driver could not attribute the failure to any part of the request
distinct(t, 'nosuchcol') DATABASE_ERROR / 500 same

Identical on better-sqlite3 and on live PostgreSQL 16.13. The dialect raises the same column ... does not exist on every row; what differs is only which door was asked.

Why it matters

This is the split #8790 closed (maintainer ruling 2026-08-15: one unresolvable WHERE column, one answer, on both read halves), and that #11541 closed for the third door, still open on this call shape. A caller's own mistake — a filter key naming a field that does not exist — is served as a server fault that names nothing the caller can act on, where the same mistake one door over is a 400 naming the column. A picklist-populating distinct() call sits beside the find() and count() of the same list view.

Not a repeat of what #17639 landed

#17639 removed the raw dialect object from this door; the envelope itself is correct and catalogued. What is still missing is the ATTRIBUTION arm — and it was left out on purpose, with the reason written into sql-driver.ts at the door: a blanket isUnresolvableColumnError arm answers "Filter on 'x' names a column ...", and this door names columns in TWO clauses — the field being listed and the WHERE compiled from filters. Telling the author of distinct(o, 'nosuchcol'), who passed no filter at all, that their FILTER was wrong is the unsupportable attribution the #8931 ruling refuses to make.

The shape that already exists for this

SqlDriver.aggregateBackendFault (#11541) is the pattern: the dialect names the column, it does not name the clause, so the clause is read off the CALLER'S OWN request — there, the groupBy / aggregations AST the builder just compiled. For distinct() the two sources are the field argument and the filters AST, so the same three arms fall out: the name equals field (a refusal about the listed column), the name is in neither (the only remaining column sources are the WHERE and the tenant-scope predicate, so #8790's unresolvableFilterColumnRefusal applies verbatim), no parsed name at all (the #17639 terminal stands unchanged). No new dialect recognizer is needed: both predicates are #8790's, already shared by find(), count() and aggregate().

One thing the implementer should weigh up front

SqlDriver is an exported class, so a new protected member lands in driver-sql's built entry .d.ts — a published-surface move, which is a clause-2 input rather than a free choice. #11541 paid that cost for aggregate(); whether distinct()'s arms belong in a new member or inline at the door is a real decision, not a formatting one.

Repro

const d = new SqlDriver(cfg);
await d.initObjects([{ name: 't', fields: { title: { type: 'string' } } }]);
await d.count('t', { where: { nosuchcol: 1 } });        // INVALID_FILTER / 400
await d.distinct('t', 'title', { nosuchcol: 1 });       // DATABASE_ERROR / 500

Related

#8790 (the find()/count() ruling) · #11541 (the same arm delivered for aggregate()) · #11455 (the envelope card that left this gap filed one door over) · #8931 (the terminal envelope and the attribution it refuses) · #17639 (the distinct() envelope this is the attribution half of) · #8926 (why no wording-matched recognizer)

Filed unassigned by the #17639 dev. Recording only — no severity asserted, routing is triage's.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions