Skip to content

docs(objectql): name the include-relation boundary of a cross-field comparand, and pin it (#15103, fallback B) - #15781

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-15103-include-relation-cross-field-pushdown
Sep 5, 2026
Merged

docs(objectql): name the include-relation boundary of a cross-field comparand, and pin it (#15103, fallback B)#15781
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-15103-include-relation-cross-field-pushdown

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #15103

The gate, measured before any code — it does not open; fallback B is delivered

Answer: the ADR-0071 join chain cannot serve the where compilation of a { $field } comparand without inventing JOIN planning — or an alias contract, or a second enforcement site for the #5222 rulings. Per the ruling on the card (comment 5548479553), this PR lands B: capability unchanged, the boundary named on the query-syntax page, the #14104 ruling record corrected in a comment on that card, and one pin that measures the boundary over a real engine.

Where the chain is, and where the comparand goes (files, functions, order of operations)

  1. packages/services/service-analytics/src/dataset-compiler.ts compileDataset — every include path becomes an entry of cube.joins (alias to table), under MAX_JOIN_HOPS = 3; the allowlist is every alias.
  2. packages/services/service-analytics/src/strategies/native-sql-strategy.ts generateSql — a per-query joins map is filled lazily by qualifyAndRegisterJoin from dimensions, measures and filter members (compileFilterNode leaf, then resolveFieldSql, then qualifyAndRegisterJoin); then the allowlist check and per-alias RLS; then FROM base LEFT JOIN …. So on this path the chain already participates in the predicate — for the left-hand MEMBER. Nothing is fixed before the joins are known; the order of operations is not the obstacle.
  3. NativeSQLStrategy.canHandle declines every query whose where or read scope carries a $field — maintainer ruling 2026-08-12, Q1 = B ([spec] service-analytics' read-scope / Cube filter compilers still refuse $field, so a CEL field-to-field RLS rule 400s on those faces #7598), quoted verbatim inside that method: the four [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222 rulings are enforced in ONE place, the driver, with no new StrategyContext hooks and no second implementation. The $field shape therefore never reaches the chain.
  4. The declined query runs ObjectQLStrategy, then ctx.executeAggregate({ groupBy, aggregations, filter, timezone, context }), then SqlDriver.aggregate(), applyFilters, applyCrossFieldComparison (packages/drivers/driver-sql/src/sql-driver.ts), where the dotted reference is refused. The bridge carries no joins, no include, no alias; sql-driver.ts (17,371 lines) contains zero knex join calls — every statement it builds is FROM "TABLE". At the refusal site there is no join chain in scope at all.

Why A is not reachable inside the ruling

route what it needs why it is excluded
(i) synthesise the join in the driver alias, ON clause, per-hop read scope (ADR-0071 D-C), and the dataset's include list — which never reaches the driver JOIN planning in the driver, the ruling's fallback trigger; and it could not refuse an outside-include relation
(ii) compile $field in NativeSQLStrategy through qualifyAndRegisterJoin the declared field set of the joined object, the column types (same-class and numeric-offset checks) and the tenant column of both objects — StrategyContext exposes none of them option A of the 2026-08-12 ruling, rejected there; a packages/spec contract change; not "without a new mechanism"
(iii) thread a join descriptor through executeAggregate, the engine and DriverQuery a new cross-package contract the alias contract that both the 2026-08-06 and the 2026-09-05 rulings exclude

Q3 — the three-hop bound

Inherited only on the chain that exists: cube.joins is built under MAX_JOIN_HOPS = 3 at compile time. The driver's comparand compiler has no chain to inherit from, so route (i) would have had to restate it. Moot under B.

What this PR changes

  • content/docs/protocol/objectql/query-syntax.mdx — one paragraph under "Comparing Two Fields": a dataset's include does not widen the same-table rule, with the mechanism and the pin named.
  • packages/services/service-analytics/src/__tests__/include-relation-cross-field-boundary.test.ts — the measurement, executable. Over one dataset with include: ['duty'] on a real SqliteWasmDriver: (1) a dimension and a runtimeFilter member on duty.grace_days compile to one LEFT JOIN "cross_field_duty" "duty" ON "cross_field_task"."duty" = "duty"."id", with the joined column in the WHERE, and return real grouped rows; (2) the offset arm AND the bare arm of a $field comparand naming duty.grace_days are refused INVALID_FILTER / 400 with the dotted-path message, and the native-SQL spy proves that pass was declined (no SQL emitted, so no join was ever built); (3) the same-table spelling answers rows 2 and 3 on the same fixture. Declared deviation from "docs-only": one test file, publishing nothing; it goes red the day A lands silently. The memory half of the asymmetry stays pinned where the evaluator lives (packages/formula/src/matches-filter-field-reference-offset.test.ts); this package does not depend on @objectstack/formula.
  • No production code; no packages/formula change (assumption 6 held). No changeset — nothing published changes (skip-changeset). Clause-②: no — no contract increment, so needs:contract-review is not hung; that reading is stated in the report for the PM to overrule.

Zone 2 assumptions, measured

  • 4 holds: SqliteWasmDriver extends SqlDriver (sqlite-wasm-driver.ts:67) — one compiler serves both.
  • 5 holds under B. Noted for the record: under route (ii) the native path's outside-include refusal is DATASET_INVALID / 400 — a different code from the driver's INVALID_FILTER — so A there would have changed the refusal for a caller-visible input.
  • 6 holds: getPath in matches-filter.ts walks dotted paths; the memory path is unchanged.
  • 7 holds: the filter corpus is packages/drivers/driver-sql/src/cross-field-conformance-cases.ts plus the two driver suites and cross-field-offset-dataset.test.ts; packages/qa/dogfood/test/authz-conformance.matrix.ts is untouched.

Verification — head 50cf9e8f1 (after merging origin/main at 7dafaaedd)

  • pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2 src/__tests__/include-relation-cross-field-boundary.test.ts src/__tests__/cross-field-offset-dataset.test.ts — exit 0: "Test Files 2 passed (2) · Tests 82 passed (82)".
  • pnpm --filter @objectstack/service-analytics typecheck — exit 0; tsc --noEmit --listFiles lists the new test file (1 hit; the sibling test as control, 1 hit).
  • Gate union from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (72 commands, derived at 50cf9e8f1, no stale-tree warning), every exit code captured before any pipe: 70 exit 0, among them check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:doc-anchors, check:docs-single-h1, check:type-check-coverage, @objectstack/spec check:docs, and @objectstack/spec check:skill-examples (green after building client and client-react: "257 prose examples type-check across 3 surface(s)"). NOT MEASURED, both whole-tree prerequisites this diff cannot narrow: check:dual-build-cjs-loads (exit 3, "PREREQUISITE NOT MET — some package has no dist/") and check:type-check-debt (exit 3, "--re-measure cannot run: 23 workspace dependencies have no built type entry point"). Neither reads this diff's two files; CI runs both over its full build.
  • Out of scope, filed unassigned: [finding] FieldReferenceSchema's TSDoc @example spells the #14104 shape as duty.grace_days — a relation path SQL push-down refuses — two paragraphs above the prose that says so #15778 — the FieldReferenceSchema TSDoc @example still spells the dotted form the prose beside it says is refused.

Generated by Claude Code

…omparand, and pin it (#15103 fallback B)

The #15103 gate was measured before any code: the ADR-0071 join chain lives
in NativeSQLStrategy, which declines every { $field } comparand (2026-08-12
ruling, #7598), and the driver that compiles the comparand builds no joins —
so the chain cannot serve the where compilation without JOIN planning in the
driver, an alias contract through executeAggregate, or a second enforcement
site for the #5222 rulings. Fallback B: capability unchanged; the query-syntax
page names the boundary and a service-analytics pin measures it over a real
engine (dimension and filter member through the join; the comparand refused
INVALID_FILTER/400 with duty in include; native declined on that pass).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e52bb6a442421d8decff89ae2270e1c5f790624cpackageMentionDocs.

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/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants