Skip to content

[finding] groupBy on a json field answers 500 DATABASE_ERROR on PostgreSQL, one merged group on memory, and one group per serialized value on SQLite #20783

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect with a measured reach:. Finding class (a). reach: POST /api/v1/data/:object/query with { groupBy: ['meta'], aggregations: [{ function: 'count', alias: 'n' }] } over a json field meta, measured by the #20745 dev on origin/main a51920f5fb on InMemoryDriver, SqlDriver SQLite and a live PostgreSQL 16.13 (os-dev-report on #20745, out_of_scope_findings[1]). PR #20781 does not change it. The readings are the dev's; the seat did not re-run them.

Filed by the domain:engine execution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY, os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. Reader: triage, then the seat that owns packages/objectql and the drivers.

What happens

The same grouping on three rows with different meta objects:

driver answer
InMemoryDriver 200, one group (meta: {a:1}, n: 3): every row merged into one
SqlDriver SQLite 200, one group per serialized value
SqlDriver PostgreSQL 16 500 DATABASE_ERROR

One query, three answers, and one of them is a 500.

Scope for whoever takes it (⛔ not a ruling)

Dedupe

mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:

Dedupe words: groupBy json field postgres 500 · aggregate group by json column DATABASE_ERROR · memory driver groups json values into one group


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:engine · area:api · pm:queue. Direction: one answer at the engine door. groupBy on a json field is refused with INVALID_... / 400 before any driver is asked. ⛔ Never a 500

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T05:53Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/objectql's query door ⇒ domain:engine. It is #20745's family: #20745 refuses the json object comparand at the same door.

    Why p2. One query gets three answers:

    • memory silently merges every row into one group;
    • SQLite gives one group per serialization;
    • PostgreSQL answers a 500.

    Production PostgreSQL fails loudly but as a server error, and memory's answer is silently wrong.

    Direction.

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    on Sep 30, 2026
  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 24
    Session: session_01DEvba2nBuD4tWzfq8r8NFY
    Account: os-support-ai (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20783-groupby-json-refused
    Worktree: objectstack-issue-20783
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (triage 5905038653):

    • packages/objectql's query door: groupBy on a json field (the structured-JSON class) is refused with an existing INVALID_* code / 400, naming the field and its type, before any driver is asked, on every driver. ⛔ No per-driver serialization rule; ⛔ never a 500.
    • pins in packages/objectql and packages/rest: memory, SQLite and PostgreSQL answer the same 400; a text field groupBy is unchanged (the control);
    • .changeset/20783-*.md: @objectstack/objectql minor with a BREAKING banner and its ADR-0087 marker.

    Stop on breach and explain in the report. ⛔ No driver file. The analytics face (a dataset or cube dimension on a json field) is measured and reported; it is folded in only if the same door serves it, otherwise it is a sibling card.
    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: 5905038653
    Serial constraints cleared: read at 2026-09-30T06:25Z against origin/main.

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20783,
      "status": "done",
      "branch": "claude/issue-20783-groupby-json-refused",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20804",
      "session": "session_01DEvba2nBuD4tWzfq8r8NFY",
      "premise_still_valid": true,
      "summary": "Reproduced the card's table on origin/main 7a09eee1b1 through POST /api/v1/data/:object/query on InMemoryDriver, SqlDriver SQLite and a private PostgreSQL 16.13. Memory answered 200 with one merged group (n 3), SQLite 200 with one group per serialized document, PostgreSQL 500 DATABASE_ERROR. Every other STRUCTURED_JSON_TYPES member split the same way (vector: memory per array), and a { field, dateGranularity } bucket over json answered null bucket / null bucket / 500. engine.aggregate now refuses a groupBy entry naming a declared structured-JSON field with INVALID_FIELD / 400 at its entry, right after rejectCredentialAggregation and before any driver, in both entry spellings. The error names the position, field and type and carries the route inside the REST 500-char bound. New module packages/objectql/src/group-by-structured-json-door.ts, a one-line engine wiring, and an objectql minor BREAKING changeset. After the change all three drivers answer the same 400, and the text control is unchanged. H1 held (the engine's aggregate entry is the one shared door, INVALID_FIELD per the name-type precedent). H2 held (no member answers one way, so the class is refused). H3: the date bucket was NOT already refused and is now. H4: the analytics ObjectQL strategy (the memory driver, any bucketed time dimension) reaches this door and is folded in. NativeSQLStrategy bypasses the engine on SQL drivers (SQLite per-serialization, PostgreSQL 500) and is reported as the sibling card. Private PostgreSQL stopped and removed; worktree removed.",
      "tests": "objectql at 43ae6c1fcc: `pnpm --filter @objectstack/objectql test` 342 files / 6739 passed. rest at 43ae6c1fcc: `OS_TEST_POSTGRES_URL=(private PG 16.13) pnpm --filter @objectstack/rest test` 232 files / 4516 passed / 33 skipped. `typecheck` objectql + rest exit 0 (check:test-typecheck OK; objectql ledger held 40 files / 234 errors / 65 signatures, rest 0). New pins: engine-group-by-json-door.test.ts 6 passed; data-group-by-json-door.test.ts 6 passed (sqlite 3 + live postgres 3) / 3 skipped (mysql, no URL). Downstream (...@objectstack/objectql direction) at cafaf885d8: service-analytics 141 files / 3267 passed; metadata-protocol 190 passed + 3 skipped files / 2792 passed + 19 skipped. After-readings at 43ae6c1fcc via scratch script over the built packages (memory, SQLite, PG): every structured-JSON groupBy answers 400 INVALID_FIELD, control x 2 / y 1. Ablation from committed fix 1eacad5296 via scripts/ablation-replace.mjs WRAP (trap-restored): the engine call fed `(query as { __ablated_20783__?: unknown }).__ablated_20783__` instead of query.groupBy. Anchor 1 to 0, marker 0 to 1, blob 67198fc4a8fc to 837fc9257e37. objectql rebuilt; ablation-dist-preflight marker present in 4 built files. Predicted red, observed red: objectql pins 3 failed / 18 passed (controls, GUARDs, engine-nested-object-door green); rest pins 4 failed / 2 passed / 3 skipped. Restore: blob == HEAD 67198fc4a8fc, git diff HEAD empty, whole-tree porcelain empty. Rebuilt; --absent preflight marker absent from 14 built files, tree clean; pins green again (21 passed; 6 passed + 3 skipped). Lint narrowed and proven at 43ae6c1fcc: eslint --no-inline-config --format json over the 6 changed .ts files = 6 files, 0 errors, 0 warnings; isPathIgnored false for all 6; parserOptions.project/projectService null (no type-aware lint).",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands at 43ae6c1fcc (7 paths vs merge base 96e724475): 65 derived; all 65 run on 43ae6c1fcc with exit codes recorded; --ran: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. check:adr-0087-registration accepted not-required (no-migration-prescription). check:query-options-erasure went red on a first draft (test surface 236 to 237, an `as any` aggregate bag in the REST pin), fixed in cafaf885d8 (back to 236). check:dual-build-cjs-loads and check:type-check-debt answered PREREQUISITE NOT MET (exit 3) before a full turbo build of ./packages/* and ./packages/*/*, then exit 0 after it; the final union at 43ae6c1fcc is all exit 0.",
      "line_budget": "changed lines 576 (+574 / -2) over 7 files vs the human-merge threshold 5000: under.",
      "files_changed": [
        ".changeset/20783-groupby-structured-json-refused.md +19 -0",
        "packages/objectql/src/group-by-structured-json-door.ts +139 -0",
        "packages/objectql/src/engine.ts +9 -0",
        "packages/objectql/src/number-comparand-declared-type-door.ts +3 -1 (comment only)",
        "packages/objectql/src/engine-group-by-json-door.test.ts +196 -0",
        "packages/objectql/src/engine-nested-object-door.test.ts +3 -1 (fixture triage)",
        "packages/rest/src/data-group-by-json-door.test.ts +205 -0"
      ],
      "deviations": [
        "The InMemoryDriver pin is by construction (a recording driver in objectql; the door answers before a driver is resolved), not a real InMemoryDriver suite: check:driver-memory-census refuses a new test consumer of that driver without a ruling, the precedent PR #20781 followed. The real InMemoryDriver before/after cells were measured with an uncommitted scratch script.",
        "Fixture triage outside the new files: PR #20781's engine-nested-object-door.test.ts case `having` over `groupBy: ['meta']` pinned the branch this PR closes; replaced by `having` over a `max` of the json field (the JSON arm still reached). number-comparand-declared-type-door.ts got a comment-only correction (a sentence this change made false). Both in packages/objectql.",
        "Merged origin/main 96e724475c (touches none of this diff's packages) before opening the PR, per AGENTS.md multi-agent section 10; merge commit 43ae6c1fcc; the full objectql/rest suites, typechecks and the gate union were re-run there.",
        "The first full objectql run was spelled `pnpm --filter @objectstack/objectql test -- --maxWorkers=2`: the bare `--` likely dropped the flag. The whole package ran (342 files, the intended scope), and the run at 43ae6c1fcc was spelled without it.",
        "The private PostgreSQL data dir was /tmp/pg-issue-20783, not the scratchpad: the postgres user cannot traverse the scratchpad's 0700 parent. Stopped (pg_ctl on that data dir; recorded PID 24650 gone) and removed before this report.",
        "An eslint probe script was copied into the worktree root for a few seconds (to resolve eslint) and deleted; git status was empty after."
      ],
      "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 #20804, relay run 36686127794); label-write assign = POST /repos/objectstack-ai/objectstack/issues/20804/assignees os-support-ai (relay run 36686218684); this os-dev-report comment = POST /repos/objectstack-ai/objectstack/issues/20783/comments. Plus git push (not REST). No card assignee or label written.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: AnalyticsService.query wired with AnalyticsServicePlugin's own auto-bridges (executeAggregate to engine.aggregate, executeRawSql to engine.execute), the service behind POST /api/v1/analytics/query; measured at the service, not over HTTP; the sibling card triage 5905038653 directed ('otherwise it becomes a sibling card') · evidence: a cube dimension sql 'meta' on a json field goes through NativeSQLStrategy on SQL drivers (engine.aggregate calls 0, raw SQL 1). SQLite answers 200 with one group per serialized document; PostgreSQL 16.13 answers 500 DATABASE_ERROR. Measured at 7a09eee1b1 and unchanged at 43ae6c1fcc. The ObjectQL half (memory driver, bucketed time dimensions) is folded in by this PR. Dataset dimensions compile to the same cube dimension (dataset-compiler checks only assertDeclared). Landing: packages/services/service-analytics · dedupe: 'analytics native sql dimension json field postgres 500' · 'cube dimension structured json group by' · 'NativeSQLStrategy GROUP BY json column'",
        "class: a · reach: POST /api/v1/data/:object/query { groupBy: ['tags'], aggregations: [count] } over a multiple: true select, PostgreSQL 16.13 answers 500 DATABASE_ERROR (could not identify an equality operator for type json), measured at 7a09eee1b1 and 43ae6c1fcc · evidence: memory 200 one group per array, SQLite 200 one group per serialized array. Same family as this card (a JSON-stored column as a group key), not its class: a multi-value field has a plausible other meaning (a bucket per member), so it was not refused here · dedupe: 'groupBy multiple select postgres 500' · 'group by multi-value field json column' · 'aggregate groupBy array field DATABASE_ERROR'",
        "class: a · reach: POST /api/v1/data/:object/query { aggregations: [{ function: 'count_distinct', field: 'meta', alias: 'n' }] } over a json field, PostgreSQL 16.13 answers 500 DATABASE_ERROR, measured at 7a09eee1b1 and 43ae6c1fcc · evidence: memory 3, SQLite 3. AGGREGATE_FIELD_TYPE_COMPATIBILITY accepts count_distinct for every FieldType on the stated ground that the table answers 'can every backend give one answer'. Seam: spec:AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct → runtime:driver-sql aggregate (COUNT(DISTINCT json) on PostgreSQL) · dedupe: 'count_distinct json field postgres 500' · 'count distinct structured json aggregate' · 'aggregate field type compatibility count_distinct json'",
        "family note: the three findings above share one shape, a JSON-stored column used as a group or distinct key answering per driver with a PostgreSQL 500. They are named for the PM to fold into one closing card if one exists; none was filed.",
        "carrier: the native-SQL analytics sibling card · noted, not filed: a refusal relayed through the analytics engine path names the engine's position (groupBy[0]), not the cube member the caller wrote (c20783.meta).",
        "carrier: 承接者:无 · noted, not filed: service-analytics resolveFkAttr (groupBy ['id', attr]) and the display-label pass (groupBy ['id', displayField]) now answer this 400 if that attribute or display field is structured-JSON. By reading, memory and SQLite answered one row per record before. No example or platform dataset names such a dimension (census in the PR body).",
        "carrier: the native-SQL analytics sibling card · noted, not filed: in the stand-in wiring the native-SQL path returned PostgreSQL's count as a string ('2') where SQLite returned a number; observed only, not over HTTP."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20804 @ 43ae6c1fc

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T08:08Z. The seat is the reviewer of record. Everything below was read on GitHub and on origin/main, not taken from the report.

    • Shape: the first line is Fixes #20783, then Clause-②: no (narrowing). The changeset is @objectstack/objectql minor with a BREAKING banner and one ADR-0087 marker (not-required (no-migration-prescription)). The PR assignee is os-support-ai.
    • The narrowing: engine.aggregate refuses a groupBy entry naming a declared structured-JSON field (STRUCTURED_JSON_TYPES) with INVALID_FIELD / 400, in both entry spellings (a bare name and { field, dateGranularity }), before any driver. The refusal names the position, the field and its type, and the route, inside the REST door's 500-character bound. It runs at the engine's aggregate entry, right after the credential-aggregation refusal. No driver file is touched. 7 files, +574/-2; not governed.
    • Measured: the card's table reproduced on memory, SQLite and a private PostgreSQL 16.13 (one merged group / one per document / 500). Every structured-JSON member split the same way, and the date bucket over json answered null / null / 500. After the fix all three drivers answer the same 400, and the text control is unchanged. The ablation was red, then green on restore.
    • Analytics face: the ObjectQL strategy reaches this door and is covered. NativeSQLStrategy bypasses the engine on SQL drivers, so it becomes the sibling card triage directed: [finding] analytics: a cube / dataset dimension on a json field, 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. The two other JSON-stored group / distinct keys the dev measured (a multiple: true select groupBy, and count_distinct on a json field, both 500 on PostgreSQL) are filed as [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.
    • Contract review: at-tier record 5906914441 on this head, PASS (read-only, Local-runs: none).
    • Accepted as is: for vector, the refusal's reason clause says one driver "merged every row into a single group", but memory grouped per array. The class claim, the verdict and the route are right for every member, so the seat takes no new head for it. The wording rides the next edit of the door's words.
    • Checks on this head: 31 success, 3 skipped, all rostered.

    Landing: ready and auto-merge in this act.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #20804 as 157baa75f

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T08:29Z.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions