Skip to content

docs(data): count_distinct is lowered on the SQL family; state the real limit - #21085

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21039-count-distinct-docs
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21039-count-distinct-docs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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).
  • fix(objectql)!: engine aggregate asks the field-type table for every row — min / max / avg over a refused type answer INVALID_FIELD / 400 on every driver #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.ai/code/session_01VDtqoecgES7ScQYGbFVDRv

@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Oct 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 134c4fe62ccab94a792450ee9dcab14a9c6ec472
Local-runs: none

① Derived judgments

Base judged: origin/main = 2821e9f15b652b30e577ef2e217ab4c7d2b942f7. Every file:line cite is at that base unless marked @head.

queries.mdx callout (content/docs/data-modeling/queries.mdx :372-386 @Head)

  1. "count_distinct is computed on every backend." TRUE. Six backends, six arms:

    • SqlDriver: packages/drivers/driver-sql/src/sql-driver.ts:1528, emitted at :10174;
    • Turso local: TursoDriver extends SqlDriver (packages/drivers/driver-turso/src/index.ts:9);
    • Turso remote: packages/drivers/driver-turso/src/remote-transport.ts:677 and :1408;
    • MongoDB: packages/drivers/driver-mongodb/src/mongodb-aggregation.ts:804-808 and :895-918;
    • in-memory driver: packages/drivers/driver-memory/src/memory-driver.ts:2164-2165;
    • engine fallback: packages/objectql/src/in-memory-aggregation.ts:234-235.

    The declared-but-uncompiled 501 class is empty on both SQL faces (sql-driver.ts:1670-1675, remote-transport.ts:722-731) and on Mongo (mongodb-aggregation.ts:476-480).

  2. "The SQL drivers (SqlDriver, and both Turso transports) lower it to COUNT(DISTINCT field)." TRUE, cites as in 1.

  3. "Nulls are not counted." TRUE on every listed backend:

    • Mongo: mongodb-aggregation.ts:911-913;
    • memory driver: memory-driver.ts:2165;
    • engine fallback: in-memory-aggregation.ts:235;
    • SQL: COUNT(DISTINCT col) ignores NULL, and the spec declares the same at packages/spec/src/data/query.zod.ts:117.
  4. "a count_distinct with no field is refused 400 INVALID_QUERY". TRUE at the protocol/REST door, on every backend: packages/metadata-protocol/src/protocol.ts:11162-11172 and :11180-11190, via invalidQueryError (:3683). The two SQL faces also refuse it in-process.

    Caveat, not a defect: an in-process engine.aggregate on Mongo, the memory driver or the fallback answers 0 / 0 / null. The neighbouring callout at queries.mdx :398-409 @Head already scopes this ("Internal callers reaching engine.aggregate() directly are unaffected").

  5. "(COUNT(DISTINCT *) is not SQL)". TRUE: sql-driver.ts:1491-1493, remote-transport.ts:765.

  6. The JSON-stored refusal, 400 INVALID_FIELD "by the engine before any driver runs". TRUE: packages/objectql/src/engine.ts:16973, and packages/objectql/src/aggregate-field-type-door.ts:266-275.

  7. The type list is exact, nothing missing and nothing overstated:

    • It matches JSON_STORED_AGGREGATE_FIELD_TYPES at packages/spec/src/data/aggregate-field-type-compatibility.ts:210-213.
    • multiple: true on select / lookup / user / file / image follows MULTI_CAPABLE_TYPES (packages/spec/src/data/field-value.zod.ts:335-337).
    • That set also contains radio, but radio with multiple: true is refused at parse (packages/spec/src/data/field.zod.ts:1165, :2189-2194).
  8. "Store the part you count in a field of its own, or count the rows with count." TRUE: this is the door's own remedy text (aggregate-field-type-door.ts:210-211).

  9. "Since the aggregate door now asks the same field-type table for every function, min, max and avg over a type the table refuses answer the same 400 INVALID_FIELD on every driver."

query-syntax.mdx callout (content/docs/protocol/objectql/query-syntax.mdx :971-980 @Head)

  1. "All six functions are computed on every backend." TRUE, as in 1.
  2. The count_distinct lowering and computation sentence. TRUE, as in 1.
  3. "median is 400 INVALID_QUERY." TRUE: protocol.ts:11141-11148; in-process at sql-driver.ts:1647, remote-transport.ts:713 and mongodb-aggregation.ts:459.
  4. The JSON-stored refusal sentence. TRUE, as in 6-7.

Outside the diff, on both pages.

  • query-syntax :104 and :118-119 agree with the new text.
  • On queries.mdx, :370, :630-631 and :398-409 agree.
  • No 501 / NOT_IMPLEMENTED / #5907 / "scheduled" / "portable today" wording remains.

Census (hand-written content/docs/**, minus references/** and releases/**): no miss.

Merge-cleanliness: git merge-tree --write-tree gives 0fe9dd282d, exit 0, no conflicts.

② Semver level

Docs only: 2 files, +23/-17. No package source is touched, and no changeset is present or owed. Clause-②: no.

③ Boundary flags

  • The one FALSE clause is item 9's premise "asks the same field-type table for every function": sum is held (aggregate-field-type-door.ts:121).
    • Fix in content/docs/data-modeling/queries.mdx :384-386 @Head: qualify the clause with "for every function but sum" plus the hold, or drop the "every function" clause and keep only the min / max / avg sentence.
  • Item 4 holds at the protocol door, not on an in-process engine.aggregate over Mongo, the memory driver or the fallback. The adjacent callout already carries that scope, so no edit is required.
  • No boundary crossed: no code, no changeset, no releases page and no references page.

Implemented-by: claude/issue-21039-count-distinct-docs
Reviewed-by: session_01VDtqoecgES7ScQYGbFVDRv

VERDICT: FAIL: one wording fix. queries.mdx :384 "for every function" overstates the door while sum is held (aggregate-field-type-door.ts:121). Apply the fix in ③ and the record passes.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8f61dfd309d8a4bd2a4d04358ae860f5c84aca48
Local-runs: none

① Derived judgments

  • The prior head carries forward. The review of 134c4fe62ccab94a792450ee9dcab14a9c6ec472 (comment 5925283863) judged every sentence of both rewritten callouts TRUE, except the queries.mdx premise "Since the aggregate door now asks the same field-type table for every function", which was FALSE because sum is held.
    • git diff 134c4fe6 8f61dfd3 touches only content/docs/data-modeling/queries.mdx: 3 lines, one sentence.
    • git diff origin/main...134c4fe6 and git diff origin/main...8f61dfd3 differ only in that sentence and the blob index. The PR diff is otherwise byte-identical (2 files: queries.mdx, protocol/objectql/query-syntax.mdx), so the prior TRUE judgments carry forward. query-syntax.mdx is unchanged between the heads.
  • The new sentence is TRUE against origin/main dff98c1f51 (queries.mdx 389-391 at head): "min, max and avg over a type the table refuses answer the same 400 INVALID_FIELD on every driver (sum is not judged by that door yet)."
    • The door judges min, max and avg.
      • packages/objectql/src/aggregate-field-type-door.ts:154 admits any function that is a key of AGGREGATE_FIELD_TYPE_COMPATIBILITY (packages/spec/src/data/aggregate-field-type-compatibility.ts:211-223).
      • Only ROWS_HELD_FOR_TRIAGE is skipped (door line 155).
    • It refuses with INVALID_FIELD / 400 before any driver.
      • Door lines 261-270: the code, status = 400, httpStatus = 400, and the throw.
      • The call site is packages/objectql/src/engine.ts:17018, inside aggregate. this.getDriver(object) is first reached at 17259, and driver.aggregate() at 17334.
    • sum is skipped.
      • Door 115 const ROWS_HELD_FOR_TRIAGE: ReadonlySet<string> = new Set(['sum']); and door 155 if (ROWS_HELD_FOR_TRIAGE.has(fn)) continue;.
      • Pinned by packages/objectql/src/engine-aggregate-field-type-door.test.ts:194-206 and :266.
      • origin/main has not moved the door: git grep ROWS_HELD_FOR_TRIAGE hits only door lines 65, 115 and 155. "yet" matches the door header at line 66.
  • Merge-cleanliness: git merge-tree --write-tree origin/main FETCH_HEAD wrote tree be34e99a5c, exit 0, no conflicts.

② Semver level

Docs only (content/docs/**, two files). No changeset is present or required. Clause-②: no.

③ Boundary flags

None. No code, spec, test, workflow or changeset files are touched, and the diff makes no new claim beyond the one re-judged sentence.

Implemented-by: claude/issue-21039-count-distinct-docs
Reviewed-by: session_01VDtqoecgES7ScQYGbFVDRv

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 05:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 05:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit fed0db8 Oct 1, 2026
33 of 34 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21039-count-distinct-docs branch October 1, 2026 06:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s

Projects

None yet

1 participant