Skip to content

Commit fed0db8

Browse files
docs(data): count_distinct is lowered on the SQL family; state the real limit (#21085)
Fixes #21039 Clause-②: no ## What this changes Docs only, two hand-written pages. Both said the SQL drivers refuse `count_distinct` with `501 NOT_IMPLEMENTED` and that its SQL lowering is "scheduled". It has been lowered since #6409; both callouts now state what `main` does. ## Before / after | page | before | after | |:--|:--|:--| | `content/docs/data-modeling/queries.mdx` (warn callout under the function table) | "`count_distinct` is not yet lowered by the SQL drivers ... refuse it as a capability gap, `501 NOT_IMPLEMENTED` ... its SQL lowering is scheduled" | Computed on every backend: SQL drivers (SqlDriver, both Turso transports) lower it to `COUNT(DISTINCT field)`; MongoDB, the in-memory driver and the engine's in-memory fallback count distinct non-null values; no `field` is `400 INVALID_QUERY`. The one real limit: a JSON-stored field (structured-JSON types, `multiselect` / `checkboxes` / `tags`, or `multiple: true`) is `400 INVALID_FIELD` at the engine before any driver runs. One sentence on the merged #21037 effect (min / max / avg over a refused type answer the same `400 INVALID_FIELD`). | | `content/docs/protocol/objectql/query-syntax.mdx` (callout after "Schema enum", old lines 971-982) | "Only count, sum, avg, min, max are portable today. count_distinct ... not yet by the SQL drivers ... 501 ... Its SQL lowering is scheduled" | All six functions computed on every backend, same backend list, `median` still `400 INVALID_QUERY`, same JSON-stored limit. Callout type warn to info. This also removes the page's self-contradiction with its own line 118 (`COUNT(DISTINCT field)` on both SQL faces since #6409). | ## Code anchors (origin/main a75311d) - SQL lowering table: `packages/drivers/driver-sql/src/sql-driver.ts:1524` (`['count_distinct', { sql: 'count', distinct: true }]`); emission `:10252-10254` (`count(distinct ??)`); fieldless refusal `:10222` calling `refuseDistinctAggregateWithoutField` (`:1827`, `INVALID_QUERY` / 400). - Turso remote transport: `packages/drivers/driver-turso/src/remote-transport.ts:677` and `:1408`. Local Turso `extends SqlDriver` (`turso-driver.ts:631`). - Memory driver: `packages/drivers/driver-memory/src/memory-driver.ts:2164` (null and undefined excluded). Engine in-memory fallback: `packages/objectql/src/in-memory-aggregation.ts:234`. MongoDB: `packages/drivers/driver-mongodb/src/mongodb-aggregation.ts:406`. - Engine refusal: `packages/objectql/src/aggregate-field-type-door.ts:236` (`assertAggregationFieldTypesAccepted`; `INVALID_FIELD` at `:261`, status 400 at `:262`), called at `packages/objectql/src/engine.ts:16891`. Table row: `packages/spec/src/data/aggregate-field-type-compatibility.ts:214` (`count_distinct: DISTINCT_COMPARABLE_FIELD_TYPES`; JSON-stored list `:190-196`). - Pins: `packages/objectql/src/engine-json-stored-group-distinct-door.test.ts:189`; `packages/rest/src/data-json-stored-group-distinct-door.test.ts:219` (read; its SQLite cell always runs, PostgreSQL / MySQL cells are skips without `OS_TEST_POSTGRES_URL` / `OS_TEST_MYSQL_URL`). - #21037 at write time: MERGED (2026-10-01T04:51:07Z, head 9497f4c). Its `aggregate-field-type-door.ts` is on this base, so the one sentence about min / max / avg is true on main. - Not run locally: the pins above (docs-only change; the claims are read from the code and the pin files, not re-executed). ## Census of hand-written pages mentioning `count_distinct` - `data-modeling/queries.mdx`: stale, fixed here. - `protocol/objectql/query-syntax.mdx`: stale at the callout, fixed here; lines 104, 118, 134 and 1067 current (118 states the lowering correctly; 134 is the interface union; 1067 points to the aggregation). - `ai/natural-language-queries.mdx` (lines 18, 27): current. Lists `count_distinct` among the `aggregate_records` functions, which the tool declares (`packages/mcp/src/mcp-http-tools.ts:821`); no capability claim. - `api/data-api.mdx` (line 418): current. The `_count_distinct` measure suffix is the one the analytics service names (`packages/services/service-analytics/src/analytics-service.ts:2882`). - `deployment/validating-metadata.mdx` (lines 210-213): current. States exactly the table row: every type except the structured-JSON types and `multiselect` / `checkboxes` / `tags` (`aggregate-field-type-compatibility.ts:190-214`). - `kernel/contracts/data-engine.mdx` (lines 152, 498, 509): current. "every face computes" holds for the vocabulary on all backends above; the JSON-stored limit is a field-type refusal, not a face gap. - `ui/dashboards.mdx` (line 328): current. Function table row, no capability claim. - `content/docs/references/**` and `content/docs/releases/**`: not touched. ## Gates `node scripts/pm/dispatch-gates.mjs --commands`: 40 derived; `--ran` reports 40 run, 0 NOT-MEASURED, 0 UNRUN. Five gates (two in `@objectstack/lint`, `check:skill-examples`, `check:docs-transcript-drift`) first answered exit 3 PREREQUISITE NOT MET and `check:docs` exit 1 before the spec / lint / client-react packages were built; after the builds all exit 0. No changeset (docs only). --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c2ec298 commit fed0db8

2 files changed

Lines changed: 23 additions & 17 deletions

File tree

‎content/docs/data-modeling/queries.mdx‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -370,12 +370,20 @@ querying the related object directly.
370370
| `count_distinct` | Count unique values | `{ function: 'count_distinct', field: 'category', alias: 'categories' }` |
371371

372372
<Callout type="warn">
373-
`count_distinct` is not yet lowered by the SQL drivers. They map
374-
`count`/`sum`/`avg`/`min`/`max` and refuse it as a **capability gap** —
375-
`501 NOT_IMPLEMENTED`, "declared but not implemented by this backend" — rather than as a
376-
caller mistake, because the query is spelled correctly and the gap is the backend's
377-
(#5907). It works on the MongoDB driver and on the engine's in-memory fallback, and its
378-
SQL lowering (`COUNT(DISTINCT field)`) is scheduled.
373+
`count_distinct` is computed on every backend. The SQL drivers (`SqlDriver`, and both
374+
Turso transports) lower it to `COUNT(DISTINCT field)`; the MongoDB driver, the in-memory
375+
driver and the engine's in-memory fallback count the distinct non-null values. Nulls are
376+
not counted, and a `count_distinct` with no `field` is refused `400 INVALID_QUERY`
377+
(`COUNT(DISTINCT *)` is not SQL).
378+
379+
The one real limit is the field's type: a `count_distinct` over a JSON-stored field — the
380+
structured-JSON types (`json`, `composite`, `repeater`, `record`, `location`, `address`,
381+
`vector`), `multiselect` / `checkboxes` / `tags`, or a `select` / `lookup` / `user` /
382+
`file` / `image` declared `multiple: true` — is refused `400 INVALID_FIELD` by the engine
383+
before any driver runs, because no two backends compare such values for equality alike.
384+
Store the part you count in a field of its own, or count the rows with `count`. `min`, `max` and
385+
`avg` over a type the table refuses answer the same `400 INVALID_FIELD` on every driver
386+
(`sum` is not judged by that door yet).
379387
</Callout>
380388

381389
<Callout type="warn">

‎content/docs/protocol/objectql/query-syntax.mdx‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -968,17 +968,15 @@ const query: QueryAST = {
968968

969969
**Schema enum:** `count`, `sum`, `avg`, `min`, `max`, `count_distinct`.
970970

971-
<Callout type="warn">
972-
Only `count`, `sum`, `avg`, `min`, and `max` are portable today. `count_distinct` is
973-
declared and is implemented by the MongoDB driver and by the engine's in-memory
974-
aggregation fallback, but not yet by the SQL drivers — on `SqlDriver` (and on the Turso
975-
driver, both transports) it is refused as a **capability gap**:
976-
`501 NOT_IMPLEMENTED`, "declared but not implemented by this backend". That is
977-
deliberately a different answer from a function the schema enum never declared
978-
(`median`), which is `400 INVALID_QUERY` — the caller's mistake — so an author who
979-
wrote `count_distinct` is never told they made a typo (#5907). Its SQL lowering
980-
(`COUNT(DISTINCT field)`) is scheduled: the declaration leads the implementation here
981-
by decision, not by drift.
971+
<Callout type="info">
972+
All six functions are computed on every backend. `count_distinct` lowers to
973+
`COUNT(DISTINCT field)` on `SqlDriver` and on the Turso driver (both transports), and is
974+
computed by the MongoDB driver, the in-memory driver and the engine's in-memory
975+
aggregation fallback. A function the schema enum never declared (`median`) is
976+
`400 INVALID_QUERY`. The remaining limit is the field's type: a `count_distinct` over a
977+
JSON-stored field (the structured-JSON types, `multiselect` / `checkboxes` / `tags`, or a
978+
field declared `multiple: true`) is refused `400 INVALID_FIELD` by the engine before any
979+
driver runs.
982980
</Callout>
983981

984982
<Callout type="warn">

0 commit comments

Comments
 (0)