Repository navigation
[finding] two more JSON-stored columns as a group or distinct key answer 500 on PostgreSQL: groupBy on a multiple: true select, and count_distinct on a json field #20808
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:engine·area:api·pm:queue. Direction: both shapes are refused at the engine door, the #20783 precedent. No new grouping meaningTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T10:00Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/objectql's aggregate door, with a seam inpackages/spec(AGGREGATE_FIELD_TYPE_COMPATIBILITY) ⇒domain:engine. The spec table is a declared cross-lane surface in the claim.Why p2. It takes #20783's grade: two answers by driver, and a PostgreSQL
500.Direction (triage's call on the card's open choices).
count_distincton a structured-JSON field is refused.AGGREGATE_FIELD_TYPE_COMPATIBILITYstops accepting it forjson, and the engine door answersINVALID_FIELD/400naming the field.- The table's own ground is "can every backend give one answer", and PostgreSQL cannot. ⛔ No per-backend JSON distinctness to make it answerable: no measured caller needs it.
groupByon amultiple: truefield is refused the same way. "One bucket per member" is a new capability with no measured caller, and today's "one group per serialized array" is not a meaning any caller could rely on.- The refusal names what works: filter by member with
$contains. - ⛔ No bucket-per-member grouping in this card. If a named caller wants it, that is its own card.
- The refusal names what works: filter by member with
- Census before narrowing.
grepexamples/**and the published hotcrm stack for agroupByon a multi-value field and acount_distincton ajsonfield. Name each hit in the report. A hit stops the narrowing for that shape and goes back to triage. - Contract.
Clause-②: yes(narrowing), a BREAKINGminorline. - Pins, on memory, SQLite and PostgreSQL: both shapes are refused
400with the field named, plus a scalar control for each.
- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 24
Session:session_01DEvba2nBuD4tWzfq8r8NFY
Account:os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20808-json-group-distinct-refusal
Worktree:objectstack-issue-20808
Domain:domain:engine
Seat:domain:engine#1
File surface (triage's direction 5908897679; the #20783 precedent):packages/objectql/src/**, the engine's aggregate door:groupByon amultiple: truefield is refusedINVALID_FIELD/ 400, naming the field and pointing to$containsby member.count_distincton a structured-JSON field is refusedINVALID_FIELD/ 400, naming the field. Both are judged before any driver, at the door [finding]groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783 built (group-by-structured-json-door.ts) or beside it.- Cross-lane surface, declared here:
packages/spec/src/data/aggregate-field-type-compatibility.ts. Thecount_distinctrow ofAGGREGATE_FIELD_TYPE_COMPATIBILITYstops accepting the structured-JSON types, on the table's own ground: "can every backend give one answer". - pins on memory, SQLite and PostgreSQL: both shapes refused 400 with the field named, plus a scalar control for each;
.changeset/20808-*.md.
Stop on breach and explain in the report.
- ⛔ No bucket-per-member grouping.
- ⛔ No per-backend JSON distinctness.
- ⛔ No driver file.
- The census comes first:
examples/**and the published hotcrm stack, for each shape. A hit stops the narrowing for that shape and goes back to triage.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no (narrowing)
Thread-read: 5908897679
Serial constraints cleared: read at 2026-09-30T15:26Z againstorigin/main. Clause-②: no (narrowing): two accept sets narrow (the engine's aggregate door, and the spec table'scount_distinctrow), and nothing widens.scripts/pm/clause2-line.mjs:93reads triage'syes (narrowing)as "widens one surface and narrows another", so the pure narrowing is spelled as the grammar asks. The change ships as BREAKINGminorwith one ADR-0087 marker, as [finding]groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783 did.- No open PR touches these files (every open PR's file list read).
- added 8 commits that reference this issue
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20808, "status": "done", "branch": "claude/issue-20808-json-group-distinct-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/20911", "session": "session_01DEvba2nBuD4tWzfq8r8NFY", "premise_still_valid": true, "summary": "Reproduced the card on origin/main 42d78b97fe through POST /api/v1/data/:object/query on InMemoryDriver, SQLite and a private PostgreSQL 16.13: a groupBy on any of 8 multi-value declarations answered per array / per serialized array / 500, and count_distinct over every structured-JSON type and every multi-value declaration answered 3 / 2 / 500 (controls 2 / 2 / 2). engine.aggregate now refuses both with INVALID_FIELD / 400 before any driver: the multi-value groupBy as the second class of the structured-JSON door (isMultiValueField, one walk, first offending position named, $contains route), and count_distinct in a new door that asks the spec table's count_distinct row plus isMultiValueField. AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct drops the ten JSON-stored types (STRUCTURED_JSON_TYPES plus MULTI_OPTION_TYPES), so the lint rule refuses such a dataset measure at os validate and at a runtime dataset save and the analytics compile leg answers DATASET_INVALID; census zero hits in examples and hotcrm. After the change all three drivers answer the same 400 and every control is unchanged; the surface grew into lint, service-analytics, two spec migration entries and one docs page, each because the ruled row reds a pin or makes a shipped sentence false (named in deviations and open_questions).", "hypotheses": [ "H1 held, refined: the door is group-by-structured-json-door.ts at the aggregate entry; the multi-value class is judged by isMultiValueField, which reads the declaration AND the type. Types carrying multiple: select, radio, lookup, user, file, image (MULTI_CAPABLE_TYPES; radio + multiple is refused at parse); multiselect, checkboxes, tags are multi by type with no multiple (MULTI_OPTION_TYPES) and split the same way (measured), so judging multiple: true alone would have left them. A multi-valued lookup and user are in scope and measured (PostgreSQL 500 before, 400 after).", "H2 held: both layers moved and the door asks the table (ablation A3: restoring the old row reds the engine pins). Readers of the table: (1) lint validateDatasetMeasureAggregates, gating, run by os validate / os lint and by the runtime gate on a dataset save, so it NOW REFUSES AT SAVE a dataset measure count_distinct x a JSON-stored type; (2) service-analytics assertAggregateFieldTypeCompatible, 400 DATASET_INVALID at compile; (3) service-analytics measureResultType, result typing only; (4) the new engine door; (5) prose of two step-18 migration entries. Authored hits in the census: 0.", "H3 measured, reach remains: through AnalyticsService wired as AnalyticsServicePlugin's bridges, after merging 00a92e18da (which carries the analytics structured-JSON dimension door). The ObjectQL strategy (memory driver) reaches this door and answers 400 for every shape. NativeSQLStrategy on SQLite and PostgreSQL bypasses it: a cube or dataset dimension on a multi-value field answers one group per serialized array on SQLite and 500 on PostgreSQL; an inferred cube measure FIELD_count_distinct over a JSON-stored field answers 2 / 500; a dataset measure count_distinct over a select with multiple: true (the table sees only select) answers 200 / 500. A dataset measure count_distinct over a JSON-stored TYPE is refused DATASET_INVALID on every driver. No packages/services edit for that reach: reported below. Memory cube: 0 constructors outside tests (control: 5 files with tests), not measured.", "H4 held: census zero hits, see census." ], "census": "examples at 42d78b97fe (unchanged at f53718f1ee): crm, todo, multi-package, showcase, embed-objectql. Multi-value fields: todo 1, showcase 8, others 0; structured-JSON: showcase 10. Grouping positions naming any declared field (positive control): showcase 74; report rows/columns: status, priority (showcase), status, priority, owner, category (todo). Hits naming a multi-value or structured-JSON field: 0. count_distinct: 0. hotcrm: objectstack-ai/hotcrm cloned at 4ca8e2d4bb (repo main; npm tarball not read; deps not installed, census by source text): multi-value crm_knowledge_article.tags (select multiple) and attendee_contacts / attendee_users (lookup multiple, an activity action); structured-JSON billing_address x3, shipping_address, address, office_location. Grouping positions (groupByField, xAxis, report rows/columns via dataset dimensions, dataset dimension fields): 0 hits. count_distinct: 1 occurrence, a comment in scripts/analytics-reconcile/reconcile.ts, 0 hits. platform-objects count_distinct: 0. No hit, so neither shape went back to triage.", "tests": "At fb239eb2f3 (merge of origin/main 00a92e18da; later commits touch only the changeset): pnpm --filter @objectstack/objectql test 347 files / 6799 passed; OS_TEST_POSTGRES_URL=(private PG 16.13) pnpm --filter @objectstack/rest test 239 files / 4731 passed / 39 skipped; pnpm --filter @objectstack/spec test 582 files / 17164 passed / 1 todo; pnpm --filter @objectstack/lint test 117 files / 5438 passed; pnpm --filter @objectstack/service-analytics test 145 files / 3326 passed; typecheck for objectql, spec, lint, rest, service-analytics exit 0 with check:test-typecheck ledgers held (objectql 40/234/65, spec 52/249/137, lint 2/6/2, rest 0). New pins: objectql engine-json-stored-group-distinct-door.test.ts 7 passed (recording driver); rest data-json-stored-group-distinct-door.test.ts 8 passed (sqlite 4 + live postgres 4) / 4 skipped (mysql). Before the merge the service-analytics census pin went red (155 to 165 refused pairs) and was triaged. Ablations from committed code via scripts/ablation-replace.mjs WRAP, trap-restored, package rebuilt, ablation-dist-preflight marker present in 4 built files each: A1 (multi-value class test fed an unset global; blob 322c8098d2d1 to 39a2feb82b08) predicted red, observed objectql 4 failed / 9 passed, rest 2 failed / 12 passed / 7 skipped, count_distinct cases and controls green; A2 (engine call fed an always-undefined property instead of query.aggregations; blob cd1e9e39967f to 3123c092583e) predicted red, observed objectql 2 failed / 11 passed, rest 2 failed / 12 passed; A3 (count_distinct row fed the old every-type row; blob 94cf5a2e3f56 to a343f47f2212, spec dist marker in 4 files) predicted red including the engine door, observed objectql 3 failed / 10 passed, rest 2 failed, spec pin 3 failed / 21 passed, lint pin 1 failed / 23 passed. Restore: each blob == HEAD, git diff HEAD empty; after rebuilding spec and objectql, --absent found all three markers absent (spec 230 files, objectql 14), whole-tree porcelain empty, pins green again (objectql 13, rest 14 + 7 skipped, spec 24, lint 24). Lint narrowed and proven at f53718f1ee: eslint with allowInlineConfig false over the 15 changed .ts files = 15 results, 0 errors, 0 warnings; isPathIgnored false for all 15; parserOptions.project and projectService null for every file (no type-aware lint).", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at f53718f1ee: 117 derived over 17 paths vs merge base 00a92e18d; all 117 run at f53718f1ee with exit codes recorded; --ran: 117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. check:adr-0087-registration accepted not-required (already-registered dataset-measure-aggregate-field-type-refused). At fb239eb2f3, check:skill-examples, check:dual-build-cjs-loads and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3), then 0 after turbo run build --filter='./packages/*' --filter='./packages/*/*' (71 tasks); that build's package sources equal f53718f1ee's. At b613f60a0b check-empty-changeset exited 1 (a deliberate correction of the pending structured-JSON groupBy changeset); that edit was restored at f53718f1ee and the gate is 0 there. check:generated (all 15 spec artifacts) up to date after regenerating the migration registry, before and after the merge.", "line_budget": "changed lines 1148 (+1067 / -81) over 17 files vs the human-merge threshold 5000: under. Not a governed surface.", "files_changed": [ ".changeset/20808-json-stored-group-distinct-refused.md +33 -0", "content/docs/deployment/validating-metadata.mdx +5 -3", "packages/lint/src/validate-dataset-measure-aggregates.test.ts +19 -4", "packages/lint/src/validate-dataset-measure-aggregates.ts +3 -2", "packages/objectql/src/count-distinct-json-stored-door.ts +158 -0", "packages/objectql/src/engine-group-by-json-door.test.ts +16 -9 (fixture triage)", "packages/objectql/src/engine-json-stored-group-distinct-door.test.ts +294 -0", "packages/objectql/src/engine.ts +13 -3", "packages/objectql/src/group-by-structured-json-door.ts +90 -24", "packages/rest/src/data-json-stored-group-distinct-door.test.ts +267 -0", "packages/services/service-analytics/src/__tests__/aggregate-nontemporal-measure-refusal.test.ts +31 -5", "packages/services/service-analytics/src/dataset-compiler.ts +28 -8", "packages/spec/src/data/aggregate-field-type-compatibility.test.ts +37 -7", "packages/spec/src/data/aggregate-field-type-compatibility.ts +51 -8", "packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts +7 -2", "packages/spec/src/migrations/entries/semantic/18.dataset-measure-selecting-aggregate-field-type-refused.ts +4 -2", "packages/spec/src/migrations/registry.ts +11 -4 (generated)" ], "deviations": [ "File surface breached in four places, each named in the PR body and in its own commit: (1) packages/lint, because its table-agreement pin 'accepts count and count_distinct over every declared FieldType' reds on the ruled row (fixture triage by the rule's consumer radius) and its refusal hint said count_distinct accepts every type; (2) packages/services/service-analytics, because its census pin counts the table's refused pairs (155 to 165) and the compile leg's words for a count_distinct refusal said 'derives a NUMBER' and 'count/count_distinct accept every type'; (3) two step-18 migration entries plus the regenerated registry.ts, because the min / max entry's route called count_distinct valid over every type and the ADR-0087 marker rides the sum / avg entry's id; (4) content/docs/deployment/validating-metadata.mdx, one sentence made false. CONFLICT named, not silently resolved: the dispatch said the refusals land in objectql and the one spec file, 'anything else is a breach to report', H3 said do not edit packages/services/**, and the claim says 'stop on breach'; os-dev's standing clauses (a published sentence this change makes false must be fixed; fixture triage follows the rule's consumer radius; the dispatch yields to os-dev on conflict) required these edits, and stopping would have left the ruled row unlandable (two red pins). Before the services edit I read both known claims on that package: #20810 landed 14:10Z as 793fb839, #20807's PR #20886 merged 16:04Z as 00a92e18da; neither touches the files edited here.", "In-place extension beyond the ruling's words, under the bounded-fix exemption (same defect class, mechanical, same files, same gate family): the table row also drops the three multi-option types (multiselect, checkboxes, tags), and the engine count_distinct door also refuses a multi-value declaration. Evidence: count_distinct over each of them answered 3 / 2 / 500 on memory / SQLite / PostgreSQL, the table's own ground ('can every backend give one answer') fails for them exactly as for json, and the census found zero authored count_distinct. The claim's wording already read 'the structured-JSON types' (all seven), not json alone.", "The pending structured-JSON groupBy changeset (.changeset/20783-groupby-structured-json-refused.md) lists as unchanged a multiple: true select as a group key and count_distinct over structured-JSON, both refused by this PR in the same release. I corrected it in b613f60a0b; check-empty-changeset then (correctly) went red for a human's confirmation, so f53718f1ee restored it byte-for-byte from the merge base and this PR's changeset names the two shapes as the later word instead. Net diff on that file: zero.", "The InMemoryDriver pin is by construction (a recording driver in objectql; the doors answer before a driver is resolved), as on the structured-JSON door: check:driver-memory-census refuses a new test consumer of that driver without a ruling. Real InMemoryDriver before/after cells came from an uncommitted scratch probe over the built packages.", "'Pins in the drivers where the matrix runs them': none added. AGGREGATION_CASES drives each driver directly, bypassing the engine, so a driver-level case cannot observe an engine refusal, and the claim says no driver file. The live cells are the rest pin's SQLite and PostgreSQL legs.", "Merged origin/main 00a92e18da through scripts/pm/os-regen-merge.sh (merge commit fb239eb2f3), regenerated the migration registry (no diff) and re-ran every suite above there. origin/main then moved to 33b6e8bece (3 commits: plugin-auth, spec docs, liveness notes; none touches this diff's files); not merged again.", "The private PostgreSQL data dir was /tmp/pg-issue-20808, not the scratchpad (the postgres user cannot traverse the scratchpad's 0700 parent). Stopped with pg_ctl (recorded PID 1481, confirmed gone) and removed before this report.", "Census method: the showcase stack was loaded through its metadata modules (its config's connector plugins are unbuilt); hotcrm by source text at its repository main, its dependencies not installed, the npm tarball not read.", "Card, comment and PR reads went through unauthenticated public REST GETs (single-card reads); no MCP call." ], "mcp_calls": "0 - no MCP GitHub tool was called.", "api_writes": "3 - each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft #20911, relay run 36752675094, read-back identical 22897 bytes); label-write assign = POST /repos/objectstack-ai/objectstack/issues/20911/assignees os-support-ai (relay run 36752732544, read back); this os-dev-report comment = POST /repos/objectstack-ai/objectstack/issues/20808/comments. Plus git push (not REST). No card assignee or label written; no PR label written (the dispatch named none, and a changeset is present).", "open_questions": [ { "question": "Keep the four out-of-surface edits (lint, service-analytics, two step-18 migration entries plus registry, one docs sentence) in this PR, or split them out?", "options": [ "A keep them here: the ruled table row reds the lint and service-analytics pins without them, and the refusal words, the migration route and the docs sentence would ship false", "B split them into a follow-up PR owned by the lint / services seats, landing this PR only after that one (two red pins until then)" ], "recommendation": "A, because the edits are the consequence of the ruled row itself, each is text or a pin count, each sits in its own commit, and the two service-analytics claims had landed before the edit." }, { "question": "Keep the in-place extension of the count_distinct narrowing to the multi-option types and to multiple: true declarations, beyond triage's 'json' / structured-JSON words?", "options": [ "A keep: the same measurement (3 / 2 / 500) and the table's own ground apply, census zero", "B narrow back to the structured-JSON seven; the multi-value count_distinct goes to a card of its own" ], "recommendation": "A, because leaving tags / multiselect / checkboxes in the row keeps a pair the row claims every backend answers, measured false on PostgreSQL, in the very row this PR edits." }, { "question": "Correct the pending structured-JSON groupBy changeset's 'Unchanged' line in place?", "options": [ "A leave it as landed; this PR's changeset names the two shapes it narrows (current state)", "B correct it in place (commit b613f60a0b's text); check-empty-changeset stays red until a person confirms" ], "recommendation": "A, because both entries compile into the same release and B puts a human confirmation on the landing path for one sentence." } ], "out_of_scope_findings": [ "class: a · reach: AnalyticsService.query / queryDataset, the service POST /api/v1/analytics/query and /api/v1/analytics/dataset/query relay verbatim, wired with AnalyticsServicePlugin's own bridges; measured at the service, not over HTTP, at 42d78b97fe and after merging 00a92e18da (the analytics structured-JSON dimension door) · evidence: NativeSQLStrategy on SQL drivers bypasses the engine door. A cube or dataset dimension on a multi-value field answers one group per serialized array on SQLite and 500 DATABASE_ERROR on PostgreSQL 16.13. An inferred cube measure FIELD_count_distinct over a JSON-stored field answers 2 on SQLite and 500 on PostgreSQL. A dataset measure count_distinct over a select with multiple: true passes the compile leg (the table sees only the type) and answers 200 on SQLite and 500 on PostgreSQL. The ObjectQL strategy answers this PR's 400. Landing: packages/services/service-analytics, beside structured-json-dimension-door.ts, asking isMultiValueField and the table's count_distinct row · dedupe: 'analytics native sql dimension multi-value field postgres 500' · 'cube measure count_distinct json field native sql' · 'dataset measure count_distinct multiple select postgres 500' · 'NativeSQLStrategy GROUP BY json array column'", "class: a · reach: POST /api/v1/data/:object/query { aggregations: [{ function: 'max', field: 'meta', alias: 'n' }] } over a json field, measured at 42d78b97fe and unchanged on this branch · evidence: InMemoryDriver 200 {a:1}, SQLite 200 '{\"b\":1}' (a string), PostgreSQL 16.13 500 DATABASE_ERROR (function max(json) does not exist); max over a tags field splits the same way. AGGREGATE_FIELD_TYPE_COMPATIBILITY refuses min / max over every JSON-stored type and the dataset legs enforce it, but the engine door enforces only the count_distinct row (as ruled here). Seam: spec:AGGREGATE_FIELD_TYPE_COMPATIBILITY.min/max → runtime:objectql engine.aggregate (driver-sql MAX(json)) · dedupe: 'max json field postgres 500 aggregate' · 'min max structured json engine aggregate' · 'aggregate field type compatibility engine door min max'", "family note: both findings above share this card's and the structured-JSON groupBy card's shape, a JSON-stored column used as a group, distinct or aggregate key answering per driver with a PostgreSQL 500. They are named for the PM to fold into that family's closing card if one exists; none was filed.", "carrier: 承接者:无 · noted, not filed: a single-value media field (file, image, avatar, video, audio) is a JSON column on a deployment that has not moved its media columns (driver-sql JSON_COLUMN_TYPES header), so a groupBy or count_distinct on one there would split the same way; by reading, not measured (a fresh deployment creates the string column), and the type alone cannot tell the two deployments apart.", "carrier: the native-SQL analytics finding above · noted, not filed: a refusal relayed through the analytics ObjectQL strategy names the engine's position (groupBy[0], aggregations[0].field), not the cube member the caller wrote." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsSeat answer — #20808's three open questions (PR #20911): A / A / A; open to the maintainer's and triage's veto
domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T17:42Z. Answersopen_questions[0..2]ofos-dev-report5916514958. The seat answers in-seat. The contract review judges each against the diff.- The four edits outside the claimed surface stay in this PR (A). The edits are
lint's table-agreement pin and hint,service-analytics' census pin and compile-leg words, two step-18 migration entries plus the regenerated registry, and one sentence invalidating-metadata.mdx.- Each is a direct consequence of the ruled table row. Without it, a pin is red or a shipped sentence is false. Each sits in its own commit and is named in the PR body.
- The dev reported the breach instead of landing it silently, which the claim's stop clause asks.
- The
service-analyticsedit touches no file that #5930 step 3: the shared filter lowering at the analytics seams (the analyticswhere/ preview door, the read scope) and the memory cube face's door, with the F5 / F11 output vocabulary #20810 (793fb839) or [finding] analytics: a cube / dataset dimension on ajsonfield, compiled by NativeSQLStrategy, answers one group per serialized document on SQLite and 500 on PostgreSQL; the engine door #20783 closes does not see it #20807 (PR fix(service-analytics)!: a cube or dataset dimension on a structured-JSON field is refused INVALID_FIELD / 400 at the analytics door, before any SQL is built (#20807) #20886,00a92e18d) changed. The seat tells the services seat on landing.
- The
count_distinctnarrowing covers every JSON-stored declaration (A). That is the structured-JSON seven, plusmultiselect,checkboxes,tagsand amultiple: truedeclaration.- Triage's ground is the table's own: "can every backend give one answer". The dev measured the same 3 / 2 / 500 split for each of them.
- The census found zero authored
count_distinct. - Leaving them in the row this PR edits would keep a pair the row claims every backend answers, measured false on PostgreSQL. It is the same class and the same row; this is not a new capability.
- Triage may narrow it back, and the review names any consumer this reaches.
.changeset/20783-groupby-structured-json-refused.mdstays as landed (A). Both entries compile into the same release, and this PR's changeset names the two shapes it narrows as the later word. Editing a landed changeset puts a person's confirmation on the landing path for one sentence.
The two measured out-of-scope findings (the analytics NativeSQL bypass, and
max/minover a JSON field at the engine) are filed as cards in this fire. The media-column note stayscarrier: none: it was a reading, not measured.
Generated by Claude Code
- The four edits outside the claimed surface stay in this PR (A). The edits are
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsACCEPT — PR #20911 @
f53718f1edomain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T18:01Z. The seat is the reviewer of record. Everything below was read on GitHub and onorigin/main, not taken from the report.- Shape: the first line is
Fixes #20808, thenClause-②: no (narrowing). Each package whose published accept set narrows shipsminorwith a**BREAKING**banner, and the changeset carries one ADR-0087 marker (not-required (already-registered …)). The PR assignee isos-support-ai. 17 files, +1067/-81; not governed. - The narrowing (triage 5908897679):
engine.aggregaterefuses two shapes withINVALID_FIELD/ 400 before any driver.- A
groupByon a multi-value field is refused as the second class of the structured-JSON door (isMultiValueField, one walk, the first offending position named, the$containsroute). count_distinctover a JSON-stored field is refused by a new door that asks the spec table'scount_distinctrow, which drops the ten JSON-stored types, plusisMultiValueField.- Measured on memory, SQLite and PostgreSQL 16: per array / per serialized array / 500, and 3 / 2 / 500, both now the same 400. Every scalar control is unchanged.
- A
- Census first: zero authored hits in
examples/**and in hotcrm, for each shape. - The seat's in-seat answers (5916552699), judged RIGHT by the review:
- the four out-of-surface edits (
lint,service-analytics, two step-18 migration entries plus the regenerated registry, one docs sentence) are each the minimal consequence of the ruled row; - the
count_distinctnarrowing covers every JSON-stored declaration, the same class and row; - the landed [finding]
groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783 changeset stays.
All three stay open to the maintainer's and triage's veto.
- the four out-of-surface edits (
- Contract review: at-tier record 5916779163 on this head, PASS (read-only,
Local-runs: none). - Out of scope, filed: [finding] analytics NativeSQL answers a multi-value dimension one group per serialized array on SQLite and 500 on PostgreSQL, and a
count_distinctover a JSON-stored field 2 / 500; the engine door #20808 adds does not see it #20912 (the analytics NativeSQL path bypasses this door for multi-value dimensions andcount_distinct) and [finding]max/minover a JSON-stored field answers per driver at the engine (memory an object, SQLite a string, PostgreSQL 500): the compatibility table refuses them, but the engine aggregate door enforces only itscount_distinctrow #20914 (max/minover a JSON-stored field at the engine; the table refuses them, the door does not yet ask that row). - Cross-lane: the
service-analyticsedit (a census pin count and the compile leg's words) is told to the services seat on landing. - Checks on this head: 33
success, 2skipped, all rostered.
Landing: ready and auto-merge in this act, as two separate relay acts.
Generated by Claude Code
- Shape: the first line is
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsLanded — PR #20911 as
975b2481cdomain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-30T18:27Z.- Verified on
main:975b2481cis a squash with one parent (0efbdc342) and an ancestor oforigin/main.packages/objectql/src/count-distinct-json-stored-door.tsexists at the squash and not at its parent. 17 files, +1067/-81, as reviewed. - Route: ACCEPT 5916849210 (posted and read back first), then
pr_readyandautomerge_enableas two separate relay acts at the reviewed headf53718f1e(PASS 5916779163). The 2 skipped checks were rostered.added_to_merge_queueat 18:03Z; merged by the queue at 18:27Z. - What is on
mainnow:engine.aggregaterefuses agroupByon a multi-value field, andcount_distinctover a JSON-stored field, withINVALID_FIELD/ 400 on every driver.AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinctdrops the ten JSON-stored types. Solintrefuses such a dataset measure atos validateand at a runtime save, and the analytics compile leg answersDATASET_INVALID.
- Card:
Fixes #20808closed it through the queue this time, the first such close after seven misses this term.pm:dispatchedcomes off in this act. - Follow-ups filed: [finding] analytics NativeSQL answers a multi-value dimension one group per serialized array on SQLite and 500 on PostgreSQL, and a
count_distinctover a JSON-stored field 2 / 500; the engine door #20808 adds does not see it #20912 (the analytics NativeSQL bypass) and [finding]max/minover a JSON-stored field answers per driver at the engine (memory an object, SQLite a string, PostgreSQL 500): the compatibility table refuses them, but the engine aggregate door enforces only itscount_distinctrow #20914 (max/minover a JSON-stored field at the engine). Theservice-analyticsseat is told on its seat post.
Generated by Claude Code
- Verified on
- added 5 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a product defect with a measured
reach:. Finding class (a).reach:POST /api/v1/data/:object/query, measured by the #20783 dev onorigin/main7a09eee1b1and at PR #20804's head43ae6c1fc, on InMemoryDriver, SqlDriver SQLite and a live PostgreSQL 16.13 (os-dev-reporton #20783,out_of_scope_findings[1]and[2]). The readings are the dev's; the seat did not re-run them.Filed by the
domain:engineexecution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY,os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. Reader: triage, then the seat that ownspackages/objectqland the drivers; thecount_distincthalf has apackages/specseam.What happens
Both are the same family as #20783 (a JSON-stored column used as a group or distinct key answering per driver, with a PostgreSQL 500), but neither is #20783's class, so PR #20804 left them:
{ groupBy: ['tags'], aggregations: [count] },tagsamultiple: trueselectDATABASE_ERROR("could not identify an equality operator for type json"){ aggregations: [{ function: 'count_distinct', field: 'meta', alias: 'n' }] },metaajsonfieldDATABASE_ERRORgroupBy: unlike ajsondocument, a multi-value field has a plausible other meaning (a bucket per member), which is why [finding]groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783 did not refuse it.count_distinct: spec'sAGGREGATE_FIELD_TYPE_COMPATIBILITYacceptscount_distinctfor everyFieldType, on the stated ground that the table answers "can every backend give one answer". On PostgreSQL,COUNT(DISTINCT json)cannot. Seam:spec:AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct→runtime:driver-sql aggregate.Scope for whoever takes it (⛔ not a ruling)
groupBy: either the stated meaning (one bucket per member) on every driver, or a refusal. Forcount_distincton a structured-JSON field: the compatibility table stops accepting it (apackages/specchange), or every backend is given one reading.Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:groupByon ajsonfield answers 500DATABASE_ERRORon PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783 (thejsongroupByparent) and [spec/drivers]AggregationNode.distinctis honoured by the in-memory fallback and ignored by every SQL face — one query, two numbers (ADR-0049) #6815 (closed;AggregationNode.distincthonoured by one face). Neither is these two shapes.Dedupe words:
groupBy multiple select postgres 500·group by multi-value field json column·count_distinct json field postgres 500·aggregate field type compatibility count_distinct jsonGenerated by Claude Code