Uh oh!
There was an error while loading. Please reload this page.
feat(driver-sql,objectql): qualify cross-schema foreign keys with referencedSchema; refuse to wire the bare name - #11906
Conversation
…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
📓 Docs Drift Check5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not 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
Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
Uh oh!
There was an error while loading. Please reload this page.
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:
Clause-②: yes (widens a public contract shape).
What landed
Driver half (
packages/drivers/driver-sql/src/sql-driver.ts):IntrospectedForeignKeygains an optionalreferencedSchema, present when — and only when — the referenced parent lives outside the session's resolution scope.referencedTablestays 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.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 inos11377_faranswersreferenced_schema = os11377_farwith the table name bare; an in-path parent answers NULL.undefined) for an in-scope parent — presence itself carries the fact.Consumer half (
packages/objectql/src/util.ts— same PR, per the ruling):IntrospectedForeignKeymirror gains the same key (consumer-side copy of the producer contract, theisUniquepattern).convertIntrospectedSchemaToObjectsrefuses to wire a lookup for a foreign key carryingreferencedSchema: a loud flag through the newoptions.logger(defaultconsole, 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, matchingPluginContext.logger);util.tsitself 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_USAGEcarries the parent's database asREFERENCED_TABLE_SCHEMAon the same row the arm already reads; InnoDB permits cross-database foreign keys; and the child-side filterTABLE_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 fromDATABASE(), 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'sTemporal 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 holdingos11377_remote_parent, referenced from an in-path child, with an in-path parent as the sibling control.toStrictEqualdiff shows the expectedreferencedSchemaline missing from the received answer (2 PG tests failed: the direct pin and theintrospectSchemacarry-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).referencedSchemakey, asserted withtoStrictEqualplus an explicitObject.keysabsence read (toEqualwould acceptreferencedSchema: undefined). Preservation pin: green on both sides by design.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.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) andgit status --porcelain(lone unstagedMduring the leg; clean after restore). Both RED legs ran from committed fixes viagit restore --source=$BASE(tree-only), never against uncommitted work.Verification (local; head d67d937)
@objectstack/driver-sql: full suite green in Test Core posture (no live URLs — live cells report named skips); the whole introspection family additionally run against live PostgreSQL 16.13: 9 files, 48 passed, 2 skipped (the two live-MySQL cells — named skips, unprovisioned in this container; they run in CI'sTemporal Conformance (live PG + MySQL)job). One pre-existing pin updated: driver-sql (PG): introspectForeignKeys' JOIN correlations are wrong in two ways — a cross-schema FK target vanishes, and a composite FK is returned as a cartesian product #11324's cross-schema fixture IS the shape this card qualifies, so its two cross-schema assertions now expectreferencedSchema(its own subject — the key is returned at all — is untouched).@objectstack/objectql:pnpm testgreen (util suite 26/26 within it); typecheck green.driver-sql/objectqlhas no import path to the changed types): green, 0 TS errors.node scripts/pm/dispatch-gates.mjs(no paths — the script derives the change set itself): 16 path-derived + 6 convention-triggered families run locally at the final head; results and the derivation line are quoted in the report comment on driver-sql (PG): a cross-schema foreign key is reported with an UNQUALIFIEDreferencedTable, and the bare name does not resolve on the session's search_path #11377.driver-sqlminor +objectqlminor (no major — launch-window rule).Generated by Claude Code