Skip to content

[finding] a no-operator object under a lookup, master_detail or json field answers per driver: the declared nested-relation filter returns no rows on memory and a 400 on SQL, and a json object comparand deep-equals on memory and is refused on SQL #20745

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect with a measured reach:. Finding class (a) (one filter, two answers by driver), with a (b) half (a declared form no data-path driver serves). reach: is POST /api/v1/data/:object/query. The #20546 dev measured it on origin/main fbec216e2d, and it is unchanged at PR #20744's head, on InMemoryDriver, SqlDriver SQLite and a live PostgreSQL 16 (os-dev-report on #20546, out_of_scope_findings[0]). The readings are the dev's; the seat read the spec's declaration on origin/main.

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 the filter contract and the drivers' nested-relation arm.

What happens

where InMemoryDriver SqlDriver (SQLite, PostgreSQL 16)
{ owner: { region: 'NA' } } on a lookup (also on master_detail and a multiple lookup) 200, no rows (rows d1 and d3 were meant: their owner is in region NA) 400 INVALID_FILTER, in the driver's words
{ meta: { a: 1 } } on a json field 200, 1 row (deep equality) 400 INVALID_FILTER, in the driver's words
{ id: { a: 1 } } (id is not in the registry's declared field map) 200, no rows 400 INVALID_FILTER

Why it is its own card

Scope for whoever takes it (⛔ not a ruling)

Triage decides, per the governing text:

In either case, pin the table above on memory, SQLite and PostgreSQL.

Dedupe

search_issues in objectstack-ai/objectstack, open and closed:

Dedupe words: nested relation filter lookup memory 200 sql 400 · FilterCondition nested relation unserved · json object comparand memory deep equality sql refusal


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 loud answer now. The engine door refuses a no-operator object under a relation or json field on every driver, and the published docs stop promising what nothing serves. Serving relation filters is a separate question for the maintainer

    Triage: lands in packages/objectql's filter door (the door PR #20744 extends for scalars) ⇒ domain:engine, the same lane as its parent #20546. The spec's prose and content/docs change with it.

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

    Why p2. One filter gets two answers by driver:

    • memory answers no rows, silently;
    • SQL answers 400 in the driver's words.

    Production SQL is loud, so it is not p1. But the published contract page teaches the form that fails: content/docs/kernel/contracts/data-engine.mdx shows // Nested relation filter with where: { account: { industry: 'tech' } }. By NORTH-STAR 4, wrong published docs are a product defect.

    Direction for this card.

    For the maintainer, not ruled here: should the platform serve relation filters? Filtering records by a related record's field is a common business need, and the docs already promise it.

  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-20745-nested-object-door
    Worktree: objectstack-issue-20745
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (triage 5902573237):

    • packages/objectql/src/no-operator-object-door.ts and its call path: the door PR fix(objectql)!: a no-operator object where a scalar field's value belongs is refused INVALID_FILTER / 400 on every driver (#20546) #20744 opened for scalars refuses a no-operator object under a lookup / master_detail (single or multiple) field and under a json field with INVALID_FILTER, in the engine's words, on every driver. The refusal names what works today (filter the related object, then $in on its ids). ⛔ No second door.
    • packages/spec/src/data/filter.zod.ts: prose only. The "4. Nested relations" docblock and the comments that teach the form state that the engine refuses it. ⛔ The FilterCondition type is not narrowed.
    • content/docs/kernel/contracts/data-engine.mdx: the // Nested relation filter example is removed.
    • tests in packages/objectql and packages/rest, the card's table on memory, SQLite and PostgreSQL;
    • .changeset/20745-*.md: @objectstack/objectql minor with a BREAKING banner and its ADR-0087 marker; @objectstack/spec patch (shipped JSDoc).

    Stop on breach and explain in the report. ⛔ Not serving relation filters (triage raised that to the maintainer; the v18 convergence on #5930 is its natural place). ⛔ No driver edit.
    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: 5902573237
    Serial constraints cleared: read at 2026-09-30T03:20Z against origin/main.

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20745,
    "status": "done",
    "branch": "claude/issue-20745-nested-object-door",
    "pr": "#20781",
    "session": "session_01DEvba2nBuD4tWzfq8r8NFY",
    "premise_still_valid": true,
    "summary": "The card's table reproduced exactly on origin/main a51920f through POST /api/v1/data/:object/query on InMemoryDriver, SqlDriver SQLite and a live PostgreSQL 16.13. The no-operator-object arm that PR #20744 put in the number-comparand door's walk now judges three kinds of column: scalar (as before), relation (spec REFERENCE_VALUE_TYPES: lookup, master_detail, user, tree, single or multiple) and structured-JSON (spec STRUCTURED_JSON_TYPES). It also judges the platform-provisioned id / created_at / updated_at columns when the declared map omits them. Each kind is refused INVALID_FILTER / 400 in its own engine words, on every driver, at where / aggregations[i].filter / having, before any read. There is no second door and no driver file changed. The words put the verdict and the route first, because the REST door truncates 4xx messages at 500 characters: $in on the related object's ids; $contains per id on a multi-valued relation, since SQL refuses $in there; $null or a stored field for JSON. The spec prose, data-engine.mdx and two changesets (objectql minor BREAKING with an ADR-0087 marker; spec patch) are included. CONFLICT, named, not silently chosen: the zone-1 line 'the id row answers the door's existing unknown-field verdict' cannot hold together with the Pins ruling ('every row answers the same INVALID_FILTER'). The door's existing verdict for an undeclared key is tolerance, which is per-driver. I implemented the Pins reading: id is platform-provisioned, not unknown, by the engine's own sets. See open_questions.",
    "tests": "Final HEAD ee17b18 (a merge of origin/main eead9dc). objectql test 339 files / 6719 passed; rest test 231 files / 4469 passed / 71 skipped; spec test 577 files / 17007 passed / 1 todo; typecheck objectql+rest+spec exit 0 (each check:test-typecheck OK); spec check:generated: all 15 artifacts up to date. test:repo at e08fd68: spec 45/794, objectql 1/5, rest 1/8. Downstream at e08fd68: metadata-protocol 190 files passed, 3 skipped / 2792 passed, 19 skipped; service-analytics 140/3266; plugin-security 147 files / 3202 passed, 23 skipped. New pins: objectql engine-nested-object-door.test.ts (15 tests); rest data-nested-object-door.test.ts with sqlite 4 + live postgres 4 passed and mysql 4 skipped (no URL). Two name-gate controls were rewritten to the engine's refusal (protocol-explicit-filter-field-gate, query-expression-conformance), and the #20546 pins keep a file field as their only control. Ablation from the committed fix via ablation-replace WRAP: the anchor 'if (facts.column !== null && isNoOperatorObject(value)) {' went 1 to 0 and the marker ablated_20745 0 to 1, blob ea19d1959255 to 3134e87031b1. After a rebuild, dist-preflight found the marker in 4 built files. Predicted red, observed red: objectql 13 failed / 236 passed (all 11 new refusals plus the 2 rewritten controls; every GUARD, control and #20546 pin green); rest 4 failed / 12 passed / 8 skipped. Restore: blob equals HEAD ea19d1959255, git diff HEAD empty, whole-tree porcelain empty. After a rebuild, the --absent preflight found the marker in 0 of 14 files, and the pins were green again (249 passed; 16 passed + 8 skipped). Gates: dispatch-gates --commands at ee17b18 (fetched, not stale) derived 110 commands; --ran reports 110 derived / 110 run / 0 NOT-MEASURED / 0 UNRUN, all exit 0. Lint, narrowed: eslint --no-inline-config --format json over the 11 changed .ts files at ee17b18 gave 11 files / 0 errors / 0 warnings. isPathIgnored is false for all 11; parserOptions.project and projectService are null, so there is no type-aware linting and untouched files cannot move.",
    "mcp_calls": "0",
    "api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, each executed by fleet-write.yml as objectstack-fleet[bot]): pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, #20781); label-write assign → POST /repos//issues/20781/assignees (os-support-ai); comment → POST /repos//issues/20745/comments (this os-dev-report). git push to claude/issue-20745-nested-object-door is not a REST write. No card writes; the card assignee was not touched.",
    "open_questions": [
    {
    "question": "Zone-1 conflict on the id row. Triage says '{ id: { a: 1 } } answers the door's existing unknown-field verdict on every driver'. The Pins ruling says every row of the card's table answers the same INVALID_FILTER. The door's existing verdict for an undeclared key is tolerance, which leaves id to the drivers (memory 200 no rows, SQL 400). Which reading stands?",
    "options": [
    "A (implemented): id / created_at / updated_at are platform-provisioned columns, as find/findOne's known set, the write gate's PLATFORM_PROVISIONED_COLUMNS, the REST ingress resolveQueryFields and the per-aggregation reference names all say. When the declared map omits one, the arm judges it by the type it stores, in the scalar words, and every other undeclared key stays tolerated. Cost: nothing new is refused except an object under those three names.",
    "B: keep id tolerated, as the literal 'unknown-field verdict'. The id row then keeps answering per driver, and the Pins row for it is false."
    ],
    "recommendation": "A. It is the only reading under which one filter has one answer on every driver, it adds no second verdict about names, and it reads id the way every other engine seam already does."
    },
    {
    "question": "Scope of the JSON and relation kinds. Triage names json and lookup / master_detail. The arm judges the spec's classes: STRUCTURED_JSON_TYPES (json, composite, repeater, record, location, address, vector) and REFERENCE_VALUE_TYPES (adds user, tree). address, composite, user and tree were measured with the identical split. This is the bounded in-place fix (same defect class, mechanical, same file, same gate family). Keep it, or narrow it to json / lookup / master_detail?",
    "options": [
    "A (implemented): the spec's classes, a closed definition matching how #20546 judged the scalar side.",
    "B: only the named types. address / composite / user / tree keep answering per driver, and a second card would be needed."
    ],
    "recommendation": "A: one classification from the spec's sets, and no one-filter-two-answers left in the same class."
    }
    ],
    "out_of_scope_findings": [
    "class: c (with a b half) · reach: named producer: the published skill skills/objectstack-query (ships to customer projects; AI authors read it). SKILL.md 'Nested Relation Filters' (about line 185) shows { object: 'account', where: { contact: { profile: { verified: true } } } }; the 'Filter parent by child conditions → Nested relation filter' row (about line 341); the search paragraph (about line 332); and rules/filters.md 'Nested Relation Filters' (about line 132) marks the form with a check mark. The engine now refuses the form with INVALID_FILTER on every driver, and before this change memory answered no rows and SQL 400. It is a governed Tier H surface outside this claim, so it was not touched. Seam: spec:FilterCondition form 4 → runtime:no-operator-object arm (number-comparand-declared-type-door walkCondition). Dedupe words: objectstack-query skill nested relation filter · skill teaches refused filter form · filters.md nested relation",
    "class: a · reach: POST /api/v1/data/:object/query with { groupBy: ['meta'], aggregations: [{ function: 'count', alias: 'n' }] } over a json field answers 500 DATABASE_ERROR on live PostgreSQL 16.13. InMemoryDriver merges every row into one group (meta {a:1}, n: 3); SQLite gives one group per serialized value. Measured on origin/main a51920f, and unchanged by this PR. Dedupe words: groupBy json field postgres 500 · aggregate group by json column DATABASE_ERROR · memory driver groups json values into one group",
    "carrier: 承接者:无 · noted, not filed: a file/media object comparand ({ photo: { url: 'x' } }) still answers per driver (memory 200 no rows, SQL 400). This is the #8371 carve-out, kept unjudged by H1, and it is in the PR's Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed: the pending 20546-no-operator-object-on-scalar changeset's 'Unchanged' paragraph (relation and structured-JSON fields keep their forms) is now superseded. The 20745 changeset says so. If the seat prefers one coherent CHANGELOG text, that paragraph can be amended in a changeset-only edit."
    ],
    "gates": "dispatch-gates --commands at ee17b18: 110 derived / 110 run / 0 NOT-MEASURED / 0 UNRUN, all exit 0 (--ran verdict line: '110 derived famil(ies) accounted for — 110 run, 0 NOT-MEASURED'). check:dual-build-cjs-loads ran after a full turbo build (cache replay) and passed: 105 entry points, 66 packages. check:type-check-debt ran after building the ledgered closure and passed. check:skill-examples ran after building client-react and passed.",
    "line_budget": "changed lines 1102 (+1008 / -94) over 14 files vs the human-merge threshold 5000: under. No skills/** or other governed path is touched (not Tier S / Tier H).",
    "deviations": [
    "The id row: implemented the Pins reading (platform-provisioned columns judged when undeclared), not the literal 'existing unknown-field verdict'. See open_questions[0].",
    "The JSON and relation kinds judge the spec's classes (adds address / composite / repeater / record / location / vector and user / tree) as a bounded in-place fix. See open_questions[1].",
    "Triage's '$contains' example for json is refused on every driver by the text-operator door (measured), so the JSON words name $null and a stored field instead.",
    "The #20546 scalar words were rewritten: the middle sentence ('only a relation field ... can carry') became false, and every kind's words were reordered so the verdict and route land inside the REST door's 500-character bound.",
    "The per-aggregation filter with a JSON object was consistent before (count 1 on all drivers, engine deep equality) and is refused now, one answer per filter at every position. The changeset names it.",
    "data-engine.mdx: the example is removed and replaced by the working route plus one paragraph, not just deleted.",
    "Two changeset files (objectql minor breaking; spec patch) instead of one, so spec's CHANGELOG does not carry a BREAKING banner for a JSDoc patch.",
    "A packages/runtime test for the memory route cell was written and then dropped: check:driver-memory-census refuses a new in-memory-driver test consumer without a maintainer ruling. The memory route readings are measured, in the PR body, not pinned.",
    "Two existing objectql controls outside the declared file list were rewritten (protocol-explicit-filter-field-gate.test.ts, query-expression-conformance.test.ts). They pinned the nested-relation form passing the doors.",
    "origin/main eead9dc was merged into the branch (merge commit ee17b18) so that the gate derivation was not stale. No conflicts; spec and objectql were rebuilt and check:generated was re-run.",
    "The test commands, gates and ablation ran with a private PostgreSQL 16.13 on 127.0.0.1:45873, started and stopped by this run."
    ],
    "files_changed": [
    ".changeset/20745-nested-object-door.md (+35)",
    ".changeset/20745-nested-relation-prose.md (+7)",
    "content/docs/kernel/contracts/data-engine.mdx (+11 -4)",
    "packages/objectql/src/engine-nested-object-door.test.ts (+369)",
    "packages/objectql/src/engine-no-operator-object-door.test.ts (+15 -24)",
    "packages/objectql/src/engine.ts (+9 -2, comments)",
    "packages/objectql/src/having-filter.ts (+2, comment)",
    "packages/objectql/src/no-operator-object-door.ts (+213 -21)",
    "packages/objectql/src/number-comparand-declared-type-door.ts (+34 -12)",
    "packages/objectql/src/protocol-explicit-filter-field-gate.test.ts (+8 -1)",
    "packages/objectql/src/query-expression-conformance.test.ts (+16 -11)",
    "packages/rest/src/data-nested-object-door.test.ts (+263)",
    "packages/rest/src/data-no-operator-object-door.test.ts (+12 -11)",
    "packages/spec/src/data/filter.zod.ts (+14 -8, prose only)"
    ]
    }

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer — #20745's two open questions: A and A

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T05:19Z. Answered in-seat: both are readings of triage 5902573237, not new product decisions. The maintainer may veto.

    1. The id row: A. Triage has two sentences here. One says { id: { a: 1 } } "answers the door's existing unknown-field verdict on every driver". The other, the Pins, says "every row answers the same INVALID_FILTER". The dev measured that the door's existing verdict for an undeclared key is tolerance. Tolerance leaves id to the drivers: memory answers 200 with no rows, SQL answers 400. So the first sentence cannot hold on every driver, and the Pins can. Triage's aim is one answer per filter on every driver. The engine already treats id, created_at and updated_at as platform-provisioned columns at find / findOne, at the write gate, at the REST ingress, and in per-aggregation reference names. So when the declared map omits one of them, judging it by the type it stores is reading it as every other seam does. It is not a second verdict about names. Every other undeclared key stays tolerated.

    2. Scope of the kinds: A. The arm judges the spec's own classes, REFERENCE_VALUE_TYPES (lookup, master_detail, user, tree) and STRUCTURED_JSON_TYPES (json, composite, repeater, record, location, address, vector). That is how #20546 judged the scalar side, and the dev measured the same split on address, composite, user and tree. Judging only the named types would leave one filter with two answers inside the same class and need a second card for the rest. The changeset names every type the narrowing reaches.

    Both answers go to the at-tier contract review as ruled, and the review judges whether the changeset states them truthfully.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20781 @ ee17b18fd

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

    Landing: once every check on this head concludes green, the seat flips it ready and arms auto-merge.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #20781 as 4b4ee88fb

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T06:03Z.

    • Verified on main: 4b4ee88fb is a squash with one parent (085ca6bc1) and an ancestor of origin/main. The pin packages/objectql/src/engine-nested-object-door.test.ts exists at the squash and not at its parent, and the // Nested relation filter example is gone from content/docs/kernel/contracts/data-engine.mdx at the squash (0) while its parent has it (1). 14 files, +1008/-94, as reviewed.
    • Route: ready and auto-merge through the relay at the reviewed head ee17b18fd (PASS 5904773035 on that head), with both skipped checks rostered. added_to_merge_queue at 05:38Z, then merged by the queue at 06:01Z.
    • What changed for authors: a no-operator object under a relation field, a structured-JSON field, or an undeclared id / created_at / updated_at is refused INVALID_FILTER / 400 on every driver, at where, aggregations[i].filter and having, and the refusal names the route that works ($in on the related ids; $contains per id on a multi-valued relation; $null or a stored field for JSON). @objectstack/objectql minor with a BREAKING note.
    • Card: the PR's Fixes #20745 did not close the card on merge (no close event two minutes after, unlike PR fix(objectql)!: a no-operator object where a scalar field's value belongs is refused INVALID_FILTER / 400 on every driver (#20546) #20744's), so the seat closes it completed in this act and removes pm:dispatched.
    • Next on this door: [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 (groupBy on a json field, p2), which triage serialised after this PR.

    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