Skip to content

driver-sql: an unresolvable WHERE column on aggregate() answers DATABASE_ERROR/500 where find() and count() answer INVALID_FILTER/400 — the #8790 refusal never reached the third read door #11541

Description

@os-zhuang

Found while implementing #11455 (the aggregate door's terminal backend-fault envelope). Filed unassigned as an out-of-scope finding — #11455 is fenced to enveloping-only and answering this needs a classifier it did not measure.

What was measured

On claude/issue-11455-bool-aggregate-envelope (i.e. with#11455's envelope in place), against embedded SQLite via the built @objectstack/driver-sql:

find where nosuchcol => code=INVALID_FILTER status=400
"Filter on 'nosuchcol' names a column that object 'probe_par' has no column for…"
count where nosuchcol => code=INVALID_FILTER status=400 (identical message)
agg where nosuchcol => code=DATABASE_ERROR status=500
"The database refused to run this query for object 'probe_par'. The driver could
not attribute the failure to any part of the request…"
agg avg(nosuchcol) => code=DATABASE_ERROR status=500
agg groupBy nosuchcol => code=DATABASE_ERROR status=500

The first two rows are #8790's ruled answer (maintainer, 2026-08-15): one unresolvable WHERE column, one answer, on both read halves, naming the column so the caller can fix it. aggregate() is the third read door and never got the arm — before #11455 it threw the raw dialect error, and after #11455 it answers the generic terminal envelope.

So the exact split #8790 closed is still open one door over: a list view's find()/count() say "'nosuchcol' is not a column of this object" with a 400, while the dashboard tile over the same object answers an opaque 500 for the same predicate.

Why #11455 deliberately did not fix it, and what the fix actually needs

A blanket isUnresolvableColumnError arm on this door would be wrong, which is why it was not added. unresolvableFilterColumnError's words are

Filter on '' names a column that object '' has no column for, so the predicate never ran.

and aggregate() names columns in three clauses, not one: the where (via applyFilters), the groupBy fields, and each aggregation's field. Rows 4 and 5 above show the other two — an author who wrote avg('nosuchcol') or grouped by a missing field would be told their filter was wrong. That is exactly the unsupportable attribution the #8931 ruling («同意 C», 2026-08-17) refuses to make, so #11455 took the honest generic envelope instead.

Closing it properly means deciding which clause the dialect-named column came from. The information is available at the call site without a new dialect predicate — aggregate() already holds the groupBy fields and the aggregation fields while it builds the statement, and unresolvableColumnNameOf already extracts the name from all three dialects' wordings — so the shape is roughly:

⚠️ Note the third arm matters: unresolvableColumnNameOf returning null is a real answer, and reading it as "not an unresolvable column after all" is the regression its own docblock warns about.

Scope note

Independent of #11152 / #11249 (whether a boolean aggregate should answer a number or refuse) — this is about a column that does not exist at all, on any type.

Region: packages/drivers/driver-sql/src/sql-driver.ts, the aggregate() body only.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions