Skip to content

docs(query-syntax): Filtering Across Relationships states the served nested-relation form - #20906

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20876-relation-filter-docs
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20876-relation-filter-docs

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20876
Clause-②: no

What changed

content/docs/protocol/objectql/query-syntax.mdx, section "Filtering Across Relationships" only. The old callout said relation traversal in where is "not supported" and blamed SqlDriver.applyFilters() for compiling a nested object and emitting a dotted key to Knex. Both were false: the headline since PR #20872 (ca5408c62), the mechanism sentences since the engine refuses the nested form before any driver. The section now states the served form, per triage 5914865117:

  • { relation: { field: value } } in where, one level, forward only, evaluated as the caller; a multi-valued relation matches any member; refused past 1000 ids; an unreadable related field answers 403.
  • Verbs and REST doors that serve it; second level and reverse direction refused; dotted path still INVALID_FIELD / 400; an aggregation's filter and having refuse the form.
  • The two-step example is kept, now framed as the route past the cap and for the reverse direction (a reverse snippet is added).
  • No driver-mechanism prose.

Code anchors (all at origin/main 33b6e8b)

  • Lowering seam: packages/objectql/src/engine.ts:11268 (resolveRelateThenLowerWhere, calls lowerRelationConditions at :11276); the related read is the engine's own find with the caller's context, :11322.
  • $in vs $contains: packages/objectql/src/relation-filter-lowering.ts:213-218 (lowerRelationSite): $in for single-valued, an $or of one $contains per id for multiple: true.
  • Cap: RELATION_FILTER_ID_CAP = 1000 at relation-filter-lowering.ts:84; read asks for cap+1 (engine.ts:11325), refuses at :11330 via relationFilterCapError (relation-filter-lowering.ts:297) as INVALID_FILTER / 400 (filter-comparand-shape.ts:78-85).
  • 403: the related read goes through the security layer's assertReadableQueryFields (packages/plugins/plugin-security/src/predicate-guard.ts:106, called at security-plugin.ts:3652), PERMISSION_DENIED / 403. Pinned: engine-nested-relation-lowering.test.ts:227, packages/rest/src/data-nested-relation-permission.test.ts.
  • One level: admitRelationCondition, relation-filter-lowering.ts:162; dotted key inside the condition :189, relation key inside :197 (second-level), both INVALID_FILTER / 400. Pinned: engine-nested-relation-lowering.test.ts:284, packages/rest/src/data-nested-object-door.test.ts (REFUSED table).
  • Reverse direction: not served by the lowering (relation-filter-lowering.ts:16-17); over REST the parent has no such field, so assertFilterFieldsExist answers INVALID_FIELD / 400 (packages/metadata-protocol/src/protocol.ts:10057, called at :11510). The direct engine.find has no field-name door (query-syntax.mdx section 9, "Unknown Fields Are Tolerated"), so the page states the reverse refusal for the REST doors only.
  • Dotted path: classifyDottedFilterHead (packages/spec/src/data/filter-dotted-head.ts:119); engine door assertFilterIsMaterializable (filter-comparand-shape.ts:204, code set at :270); REST ingress protocol.ts:10114.
  • Verbs: find engine.ts:11662, findOne :11935, update :13558, delete :16227, count :16760, aggregate :17138; pinned for all six plus judgeFilter at engine-nested-relation-lowering.test.ts:190. REST: GET /data/:object and POST /data/:object/query (packages/rest/src/rest-server.ts:8635, :8818) both call findData (protocol.ts:11183); the POST query door is the one the REST test drives.
  • aggregations[i].filter and having refuse the form: no-operator-object-door.ts:307; pinned engine-nested-relation-lowering.test.ts:329, REST data-nested-object-door.test.ts:256.

Shared wording with #20888

The skill half is PR #20902 (open, not merged). Two passages are quoted verbatim from its skills/objectstack-query/rules/filters.md: the served-form sentence ("A condition on a related record's fields beneath a relation field ... any member when multiple: true).") and the "Limits — one level: ..." sentence. The code sentence is identical; if #20902 changes those sentences before landing, this page must be re-quoted.

Census of hand-written content/docs/**

Searched: "Relation traversal", "not supported" near where/relation, applyFilters, dotted 'account. examples, "two queries" / "$in its ids" / "nested form".

  • Fixed: protocol/objectql/query-syntax.mdx (the section above), the only hit.
  • Already correct, no change: kernel/contracts/data-engine.mdx:187-215 states the served form, the 1000 cap, one level, 403, aggregation filter/having and the dotted refusal (landed with feat(objectql): serve the nested-relation filter in where — lowered at the engine seam, the related object read as the caller, a loud cap, drivers untouched (#20802) #20872).
  • Not hits: permissions/*.mdx dotted 'account.annual_revenue' keys are field-permission keys, not filters; protocol/objectql/types.mdx:1035 'metadata.color' is the deliberate structured/JSON carve-out; query-syntax.mdx :1135/:1167 and data-modeling/queries.mdx:442,519, schema-design.mdx:111, api/data-api.mdx:177 concern the search axis and expand/fields paths, not where.
  • Generated reference pages (content/docs/references/**) not touched. Also fixed (contract review, second commit a2a66881e2): the query-syntax.mdx line 4 frontmatter description listed "joins", a tombstoned key (packages/spec/src/data/query.zod.ts:562; the page says so at its Joins section); it now reads "expand", which the page's section 4 (Relationships (Expand)) covers.

Gates

node scripts/pm/dispatch-gates.mjs --commands derived 40 commands at head a2a66881e2 (re-run after the second commit; the first pass was at bd718b653a); 39 ran green (exit 0) after pnpm install and a lint-closure build. --ran reconciliation: 39 of 40 run, 1 UNRUN: pnpm --filter @objectstack/spec run check:skill-examples exited 3 (prerequisite: the client-SDK surface has no built output). NOT MEASURED: it type-checks marked TypeScript examples in skills and docs, and this diff adds no marked block. CI owns it. No changeset (docs only, nothing published).

🤖 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 Sep 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a2a66881e2eabe07c1fd3f1e2c09009cfb34f5a2
Local-runs: none

① Derived judgments

  • The delta bd718b653a..a2a66881e2 is one commit, one line: the description: frontmatter of content/docs/protocol/objectql/query-syntax.mdx (joins → expand).
  • Carried forward from the at-tier review of bd718b653a: the page body from line 6 on is byte-identical, so every TRUE judgment on the rewritten "Filtering Across Relationships" section stands. That review covered:
  • New description sentence: TRUE. expand is a live QuerySchema key (query.zod.ts:612) and page §4 "Relationships (Expand)" (:781). Filtering (:522, §2), aggregations (:565, §5) and sorting (:552, §3) are live. joins stays tombstoned (query.zod.ts:562).
  • The PR merges cleanly against origin/main cf684c98eb (merge-tree tree e385f89b71).

② Semver level

Docs only, one content/docs/** file; no changeset; Clause-②: no.

③ Boundary flags

Implemented-by: claude/issue-20876-relation-filter-docs
Reviewed-by: session_01VDtqoecgES7ScQYGbFVDRv

VERDICT: PASS

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