Skip to content

feat(driver-sql,objectql): qualify cross-schema foreign keys with referencedSchema; refuse to wire the bare name - #11906

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-11377-cross-schema-fk-qualification
Aug 25, 2026
Merged

feat(driver-sql,objectql): qualify cross-schema foreign keys with referencedSchema; refuse to wire the bare name#11906
os-warren merged 3 commits into
mainfrom
claude/issue-11377-cross-schema-fk-qualification

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#11377

Maintainer ruling (carried out; 引文保持原文,未翻译)

Provenance: maintainer, 2026-08-24, live PM chat, ruling on the four-facet analysis (issue comment 5393638385), verbatim:

接受你的建议。

The accepted recommendation, verbatim from that block:

推荐:选项 1(加可选 referencedSchema,越界时填),且同一张卡带消费端半边:util.ts 对带 schema 且不可解析的目标响亮跳过并标记,⛔ 不裸名接线(否则加了键没人读,声明无强制原地复发)。四轴同向。

Clause-②: yes (widens a public contract shape).

What landed

Driver half (packages/drivers/driver-sql/src/sql-driver.ts):

  • IntrospectedForeignKey gains an optional referencedSchema, present when — and only when — the referenced parent lives outside the session's resolution scope. referencedTable stays a bare name always (option 2's conditional spelling rejected as a trap; option 3 rejected as hiding truth). The interface TSDoc states the per-arm semantics.
  • PG arm: joins the parent's namespace and projects its schema when it is not in current_schemas(false) — the same search_path scoping driver-sql (PG): introspectForeignKeys' information_schema query is not schema-scoped — a same-named table in another schema contributes its foreign keys #11201 gave the child side, asked about the parent. Measured on live PostgreSQL 16.13 with the card's fixture shape before wiring: a parent in os11377_far answers referenced_schema = os11377_far with the table name bare; an in-path parent answers NULL.
  • The key is absent (never undefined) for an in-scope parent — presence itself carries the fact.

Consumer half (packages/objectql/src/util.ts — same PR, per the ruling):

  • The local IntrospectedForeignKey mirror gains the same key (consumer-side copy of the producer contract, the isUnique pattern).
  • convertIntrospectedSchemaToObjects refuses to wire a lookup for a foreign key carrying referencedSchema: a loud flag through the new options.logger (default console, so a bare call is loud) names the constraint, the qualified address, the consequence, and the remedy; the column converts as a plain field so its data stays visible while the false address does not ship. Foreign keys with in-scope targets keep wiring byte-identically (whole-object pins). The warn idiom was measured from the package (action-governance.ts's minimal injected-logger surface, matching PluginContext.logger); util.ts itself had none.

MySQL-arm measurement (confidence-gap check folded in from the four-facet block)

The MySQL arm can fill the key symmetrically, and now does: information_schema.KEY_COLUMN_USAGE carries the parent's database as REFERENCED_TABLE_SCHEMA on the same row the arm already reads; InnoDB permits cross-database foreign keys; and the child-side filter TABLE_SCHEMA = DATABASE() never constrained the referenced side — so a cross-database foreign key was already being returned, bare. The arm now projects the parent's database when it differs from DATABASE(), compared with MySQL's null-safe equality operator (with no default database selected, no bare name resolves, so every parent is qualified). No live MySQL exists in this container: the new test file's live-MySQL cell runs in CI's Temporal Conformance (live PG + MySQL) job (a named skip elsewhere, per the matrix contract), and a no-server emission probe (the #11379 pattern) pins the projection and the TS row mapping in every job. #11379's separate MySQL ordering issue was not touched.

SQLite: never fills the key — no schemas, and a foreign key cannot cross an ATTACHed database; documented in the interface TSDoc.

Reverse verification (RED first, both halves)

Fixture: live PostgreSQL 16.13 in this container; the card's shape — a _far-suffixed sibling schema holding os11377_remote_parent, referenced from an in-path child, with an in-path parent as the sibling control.

  • (a) driver, cross-schema pin — RED by the key's absence against pre-fix source: toStrictEqual diff shows the expected referencedSchema line missing from the received answer (2 PG tests failed: the direct pin and the introspectSchema carry-through; the MySQL emission-probe pins failed alongside — 4 failed, 1 passed [the fixture control], 1 skipped). GREEN after: 5 passed, 1 skipped (live MySQL, named skip).
  • (b) driver, in-path pin — byte-identical answer with NO referencedSchema key, asserted with toStrictEqual plus an explicit Object.keys absence read (toEqual would accept referencedSchema: undefined). Preservation pin: green on both sides by design.
  • (c) consumer — RED against pre-fix util.ts: all 4 new pins failed (a lookup wired to the bare name, no flag); the 22 pre-existing tests stayed green. GREEN after: 26/26. The flag message and meta are pinned fragment by fragment.
  • (d) consumer, resolvable FK — whole-object byte-identical lookup, exactly one warn emitted (the cross-schema refusal, not about the resolvable key). Preservation pin.

Each mutation and each restore was confirmed on disk before reading any run: grep -c referencedSchema (driver: 0 pre-fix / 5 fixed; consumer: 0 pre-fix / 9 fixed) and git status --porcelain (lone unstaged M during the leg; clean after restore). Both RED legs ran from committed fixes via git restore --source=$BASE (tree-only), never against uncommitted work.

Verification (local; head d67d937)


Generated by Claude Code

…chema (#11377)
IntrospectedForeignKey gains an optional referencedSchema, present when -
and only when - the referenced parent lives outside the session's own
resolution scope (PG: the parent's schema vs current_schemas(false);
MySQL: REFERENCED_TABLE_SCHEMA vs DATABASE(), null-safe). referencedTable
stays a bare name unconditionally. SQLite never sets the key - no schemas,
and a foreign key cannot cross an ATTACHed database.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
…erencedSchema (#11377)
convertIntrospectedSchemaToObjects reads the driver's new qualification:
a foreign key whose target carries referencedSchema is loudly skipped and
flagged through options.logger (default console) - never wired to the
bare name - and the column converts as a plain field so the data stays
visible. Resolvable foreign keys keep wiring byte-identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
…chema pins; add changeset
The #11324 fixture IS the cross-schema shape #11377 qualifies, so its two
cross-schema assertions now carry referencedSchema; presence/absence
semantics stay pinned in the #11377 file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 18 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 fd50e59e722dadbfe8961b265d79cb4e38a0ca74packageMentionDocs.

Which tree this was computed on

This run read content/docs from 90f40f85cee6360cb47dd6cb11579b37611efdd0 — the merge of head d67d93793e27c24d90698e309d2e2535844b0e7d into base fd50e59e722dadbfe8961b265d79cb4e38a0ca74, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 90f40f85cee6360cb47dd6cb11579b37611efdd0 && git checkout 90f40f85cee6360cb47dd6cb11579b37611efdd0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fd50e59e722dadbfe8961b265d79cb4e38a0ca74 d67d93793e27c24d90698e309d2e2535844b0e7d && git checkout -B drift-repro fd50e59e722dadbfe8961b265d79cb4e38a0ca74 && git merge --no-ff d67d93793e27c24d90698e309d2e2535844b0e7d
node scripts/docs-audit/affected-docs.mjs --json fd50e59e722dadbfe8961b265d79cb4e38a0ca74

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 25, 2026
@os-warren
os-warren marked this pull request as ready for review August 25, 2026 00:39
@os-warren
os-warren added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 0010797Aug 25, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-11377-cross-schema-fk-qualification branch August 25, 2026 01:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-warren@claude