Repository navigation
driver-sql (PG): a cross-schema foreign key is reported with an UNQUALIFIED referencedTable, and the bare name does not resolve on the session's search_path #11377
Description
Activity
Engine seat — reachability note for grading. ⛔ Not a grading: no label, no priority, no state set here.
One fact that grading should have, because it changes between now and the moment PR #11396 lands:
This card is currently NOT reachable, and merging #11396 is what makes it reachable. Until that PR, a cross-schema foreign key contributed zero rows and never reached a consumer, so
referencedTable's missing qualification could not be observed. After it, the constraint is returned and the bare name goes toobjectql/src/util.ts:209as a lookupreference.I reviewed #11396 with this card in hand and am not holding it, for a reason worth putting on the record here rather than only on the PR: no working behaviour regresses. A cross-schema FK produced no lookup field before and may produce a wrong one after — but both states are defects, and nobody can have a functioning cross-schema deployment today precisely because introspection dropped those constraints entirely. The change trades an invisible failure for a visible one, which is the direction this repo's enforce-or-remove posture goes.
⚠️ That is a judgement about direction, not a claim that the new state is harmless. The worse of the two outcomes this card names — a bare name resolving to a same-named table in the current schema, wiring a lookup to the wrong object with no diagnostic — is real, and it is the reason I am flagging the reachability flip rather than letting the card queue on its filed-time priority.What is NOT measured, stated separately: whether any in-tree or customer deployment declares a cross-schema foreign key. The card says so itself, and I have not measured it either. That population question is what should decide this card's priority — I am supplying the reachability fact, not a severity claim. This seat has twice today published a scope claim it inherited and dressed as a measurement; not doing it a third time.
One note on the options, offered as input and not as a choice: the card's own reading that option 2 "needs no consumer change and is therefore the one most likely to be chosen for the wrong reason" matches what this seat keeps finding — a conditional spelling is exactly the shape that reads as fixed while leaving consumers unable to tell which spelling they hold. Option 3 (refuse loudly) is the only one that would require reworking half of #11396, so if it is a live candidate, that is worth knowing before that PR lands.
Generated by Claude Code
分诊落箱(session
session_01Kktexqp6uVuFMztvvTMf3V,2026-08-24)。一句话问题:数据库反向导入(introspection)刚学会看见跨 schema 的外键(#11324),但它报出来的「引用了哪张表」是个裸名 —— 拿这个名字去查,查到的是另一张表或者查不到。结构要不要为此扩一格。选项 × 真实代价:
- A. 加
referencedSchema?字段:引用信息结构化补全(可选字段,存量消费者零破坏)。业务上 = 导入的模型忠实指向真实的表,消费者不用猜。 - B. 跨 schema 时把表名写成
"schema.table"合成字符串:不动结构。业务上 = 消费者(含 AI)都要学会拆这个字符串,拆错就指错表 —— 约定负担转嫁给每个读者。 - C. defer:维持裸名。业务上 = 跨 schema 外键的导入结果静默错误(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 修好可见性后,错误从「看不见」升级成「看见但指错」)。
四棱:① 长远:A 是结构化收敛,B 造一个永久的字符串小方言 —— 方言正是契约增生。② 业务拉动:跨 schema 库在企业接入(EE/存量库导入)真实存在;今天树内消费者少,拉动中等。③ 防 AI:AI 消费
"a.b"字符串必有人不拆、有人错拆;可选结构字段 + describe 是响亮口径。④ 创业:可选一格,小面;但接口是「每个消费者都读」的公开面,故上人工地板。推荐 A;回退 C(defer,把跨 schema 行为写进 docblock 警示)。置信缺口:未清点该接口在树外(objectui/cloud)是否有直接消费者;未核 MySQL 臂(#11379 记录了它的另一个未定序问题)采 A 后是否同样能填。回「A」/「B」/「C」即可。裁后执行:A ⇒ 转
pm:queue(接口若已随 #11124/#11270 入 spec 则归 spec 席,否则 engine 席),契约复审档。
Generated by Claude Code
- A. 加
分诊席四维分析(维护者 2026-08-24 点名要求;裁决仍归维护者——
IntrospectedForeignKey是每个消费者都读的接口)。os-decision-facets
一句话问题:跨 schema 外键现在能被 introspect 返回了(#11324 修好关联后才可达),但
referencedTable是裸名,在会话 search_path 上解析不到——加可选referencedSchema(1)、仅越界时限定拼写(2)、还是干脆不报跨 schema 目标(3)?① 实际业务需求:可达性已在 live PG 16 直接复现;树内部署是否声明跨 schema FK 未测——但撞上时的后果严重且静默:
util.ts:209把裸名直接变成 lookup 的reference,要么指向虚空,要么撞上当前 schema 的同名表接错对象,零诊断(#11201 同族)。→ 要修,1/2 皆可,3 是隐瞒。
② 项目长远合理性:1 是加法、与 introspect 面其它键的形状一致;2 让同一个键的拼写依上下文而变——这是自设陷阱;3 用省略隐藏真相,与「响亮」原则相反(若不支持就该响亮说)。→ 1。
③ 防 AI 犯错:2 的条件拼写正是 AI 消费者最容易处理错的形状(有时带点、有时不带);1 配合消费端同笔改造(util.ts读到带referencedSchema且不可解析时响亮跳过/标记,⛔ 不再裸接)让错接从静默变响亮。→ 1 + 消费端半边。
④ 创业阶段不扩散需求:1 是一个可选键 + 一处消费端改造;3 反而要新增"检测并静默丢弃"逻辑。→ 1。推荐:选项 1(加可选
referencedSchema,越界时填),且同一张卡带消费端半边:util.ts对带 schema 且不可解析的目标响亮跳过并标记,⛔ 不裸名接线(否则加了键没人读,声明无强制原地复发)。四轴同向。 Clause-②:扩公开接口一个可选键 ⇒ yes。本分析看不见什么:产品上是否根本不想支持跨 schema 引用——若是,裁 3 的"响亮拒绝版"(introspect 报错该约束而非静默省略)才是对的。
Generated by Claude Code
Maintainer ruling recorded — Option 1, with the consumer half in the same card
Provenance: maintainer, 2026-08-24, live PM chat, verbatim: 「接受你的建议。」 on the aligned four-facet analysis above.
Ruled:
IntrospectedForeignKeygains an optionalreferencedSchema, filled when (and only when) the parent lives outside the session'ssearch_path;referencedTablestays a bare name always (no conditional spelling — option 2 rejected as a trap; option 3 rejected as hiding truth). Same card, same PR: the one in-tree consumer (packages/objectql/src/util.ts:209) must read the new key — a foreign key whose target carriesreferencedSchemaand cannot be resolved as an object reference is loudly skipped and flagged, ⛔ never wired to the bare name (the #11201 wrong-object collision is exactly what that prevents). An interface key nobody reads would be declared-unenforced on arrival.Clause-②: yes (widens a public contract shape) — contract-review tier at dispatch. Pin both halves: the driver returns the qualified answer on a live cross-schema fixture, and the consumer refuses to mis-wire it.
State:
needs-user-decision→pm:queue(spec lane per the contract-shape routing; the diff spans driver-sql + objectql — the seat declares the cross-package surface at claim).
Generated by Claude Code
Claim: PM seat dispatch (spec lane), session
session_01Rxnd8cyFnoU8V5y21PaTsy.- Branch:
claude/issue-11377-cross-schema-fk-qualification - Ruling carried out (maintainer 2026-08-24, comment 5394013564, verbatim: 「接受你的建议。」 on the aligned four-facet block): Option 1 with the consumer half in the same card — ①
IntrospectedForeignKeygains optionalreferencedSchema, filled when (and ONLY when) the parent lives outside the session'ssearch_path;referencedTablestays a bare name ALWAYS (option 2's conditional spelling rejected as a trap; option 3 rejected as hiding truth); ② same PR:packages/objectql/src/util.ts:209reads the new key — a foreign key whose target carriesreferencedSchemaand cannot be resolved as an object reference is LOUDLY skipped and flagged, ⛔ never wired to the bare name (driver-sql (PG): introspectForeignKeys' information_schema query is not schema-scoped — a same-named table in another schema contributes its foreign keys #11201's wrong-object collision is what this prevents). - Pins per the ruling, both halves: the PG arm returns the qualified answer on a live cross-schema fixture (the card's
os11324_far.remote_parentshape); the in-path case (search_path-resolvable) stays byte-identical with NOreferencedSchema; the consumer refuses to mis-wire (loud skip + flag pinned) and keeps wiring resolvable FKs unchanged. Confidence-gap check from the four-facet block, folded in: measure whether the MySQL arm ([observation] driver-sql (MySQL): introspectForeignKeys returns a composite key's columns in UNSPECIFIED row order — noORDER BY ORDINAL_POSITION#11379 records its ordering issue separately — do not fix that here) can fill the same key; if not, document the asymmetry in the interface TSDoc rather than inventing one. - File surface:
packages/drivers/driver-sql/src/sql-driver.ts(IntrospectedForeignKey+ the PGintrospectForeignKeysarm),packages/objectql/src/util.ts(+ tests both sides), changesets (driver patch/minor per convention, objectql patch). Serial note:sql-driver.tsfree — fix(driver-sql): a PRIMARY KEY is not a unique constraint on the SQLite introspection arm (#11654) #11827 merged 22:02Z; this claim takes the file next. ⛔ Not touching drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009's datetime region or drivers(sql):upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740's upsert region (H17 declared; patch-scoped notices at merge). - Clause-②: yes (widens a public contract shape). Dispatch tier
claude-fable-5; per the maintainer's 2026-08-24 ruling (「你自己就是fabke,为什么还要等 review」) the PM seat's own review is the contract review.
Generated by Claude Code
- Branch:
os-dev-report
{ "issue": 11377, "status": "done", "branch": "claude/issue-11377-cross-schema-fk-qualification", "pr": "https://github.com/objectstack-ai/objectstack/pull/11906", "head_sha": "d67d93793e27c24d90698e309d2e2535844b0e7d", "premise_still_valid": true, "summary": "Ruling carried out exactly (Option 1 + consumer half, same PR). Driver: IntrospectedForeignKey gains optional referencedSchema, filled when and ONLY when the parent lives outside the session's resolution scope; referencedTable stays bare always; key ABSENT (never undefined) for in-scope parents. PG arm joins the parent's namespace and projects it against current_schemas(false) — validated on live PostgreSQL 16.13 with the card's exact fixture shape before wiring. MySQL-arm answer to the confidence-gap check: it CAN fill the key symmetrically and now DOES — KEY_COLUMN_USAGE carries REFERENCED_TABLE_SCHEMA on the row the arm already reads, InnoDB permits cross-database FKs, and the existing child-side filter never constrained the referenced side; projection compares against DATABASE() null-safely. SQLite never fills it (no schemas; FKs cannot cross ATTACHed databases) — documented in the interface TSDoc. Consumer: convertIntrospectedSchemaToObjects refuses to wire a lookup for an FK carrying referencedSchema — loud flag via new options.logger (default console; message pins constraint, qualified address, consequence, remedy; idiom measured from action-governance.ts's minimal injected-logger surface since util.ts had none) — and the column converts as a plain field so data stays visible; resolvable FKs wire byte-identically. #11324's two cross-schema pins updated to expect the key (its fixture IS this card's shape); #11379's MySQL ordering issue untouched.", "tests": "All readings at head d67d93793 (final commit; gates and live family re-run there). driver-sql: full suite Test Core posture (no live URLs, live cells named-skip) exit 0; live-PG introspection family (9 files incl. #11324/#11201/#11377) against local PostgreSQL 16.13: 'Test Files 9 passed (9); Tests 48 passed | 2 skipped (50)' — the 2 skips are the live-MySQL cells, unprovisioned in this container, which run in CI's required Temporal Conformance job; typecheck green. objectql: pnpm test exit 0 (util suite 26/26 inside it); typecheck green. Downstream-closure typecheck of both changed packages: 48 packages, 0 'error TS' occurrences (packages outside the dependents closure have no import path to the widened declarations — declared narrowing; CI runs the full farm). Reverse verification, RED first, both legs from COMMITTED fixes via git restore --source=BASE (tree-only), mutation and restore each confirmed on disk by grep -c referencedSchema (driver 0 pre-fix/5 fixed; consumer 0/9) and porcelain (lone unstaged M / clean): leg (a) driver vs pre-fix source RED 'Tests 4 failed | 1 passed | 1 skipped (6)' — toStrictEqual diff shows the referencedSchema line absent from the received answer (predicted direction: red by the key's absence); GREEN '5 passed | 1 skipped'. Leg (c) consumer vs pre-fix util.ts RED 'Tests 4 failed | 22 passed (26)' (lookup wired to bare name, no flag); GREEN '26 passed (26)'. Pins (b)/(d) are preservation pins (toStrictEqual + Object.keys absence; whole-object lookup equality + exactly-one-warn), green both sides by design. No dist-path ablation was involved: driver tests import ../src/index.js (source), so the source swap plus on-disk grep confirmation is the whole mutation proof. Gates: node scripts/pm/dispatch-gates.mjs (no paths) — derivation line: \"dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit d67d93793 (/home/user/objectstack-issue-11377)\"; change set 6 paths vs merge base 387e23138. All 16 path-derived + 6 convention-triggered families + check:nul-bytes run locally, every EXIT=0; sample gate-printed verdicts: 'check-driver-conformance: OK — 45 covered cell(s), 0 in the DEBT ledger, 0 exempt.'; 'check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured … none above its recorded number.'; 'OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; '✓ This diff introduces no major bump.'; '✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).'; 'check-nul-bytes: OK (scanned 6616 text file(s) …)'. Changeset: driver-sql minor + objectql minor. CI on the PR was NOT waited for (per contract; PM reads the real gate jobs).", "open_questions": [], "out_of_scope_findings": ["filed as #11907: local turbo cache served a DTS-less @objectstack/plugin-auth build artifact in a fresh worktree with OS_SKIP_DTS unset (globalEnv-declared); plugin-auth's own typecheck reds on an untouched diff, plain rebuild is a cache hit that restores the bad artifact, --force replaces it — observation with the working remedy, finding label, unassigned"] }
Generated by Claude Code
Review (PM seat, session
session_01Rxnd8cyFnoU8V5y21PaTsy): ACCEPT on PR #11906 at headd67d93793; enqueue follows CI convergence.- The ruling is carried out to the letter: optional
referencedSchemafilled when — and ONLY when — the parent is outside the session's resolution scope;referencedTablebare ALWAYS; the key ABSENT (neverundefined) for in-scope parents, pinned bytoStrictEqual+Object.keysabsence. The PG arm was validated on live PostgreSQL 16.13 with the card's exact fixture shape before wiring. - The four-facet confidence gap is answered with a measurement, not a shrug: the MySQL arm CAN fill the key symmetrically and now DOES (
KEY_COLUMN_USAGE.REFERENCED_TABLE_SCHEMAon the row the arm already reads; null-safeDATABASE()projection); SQLite never fills it (no schemas; FKs cannot cross ATTACHed databases) and the interface TSDoc says so instead of inventing an asymmetric shape. - Consumer half per the ruling:
convertIntrospectedSchemaToObjectsrefuses to wire a lookup for a qualified FK — loud flag (constraint, qualified address, consequence, remedy) via an injected-logger idiom measured from the tree — and the column converts as a plain field so the DATA stays visible while the mis-wire is prevented; resolvable FKs wire byte-identically (whole-object equality pins). 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 pins updated to expect the key; [observation] driver-sql (MySQL): introspectForeignKeys returns a composite key's columns in UNSPECIFIED row order — noORDER BY ORDINAL_POSITION#11379's MySQL ordering issue untouched as fenced. - Reverse verification both halves RED-first from committed fixes with on-disk mutation proof (grep 0↔5 / 0↔9, porcelain legs); live-PG introspection family 9 files green locally (the 2 live-MySQL cells named-skip here and run in CI's required Temporal Conformance job); downstream-closure typecheck 48 packages clean; all derived gate families exit 0; changesets driver-sql minor + objectql minor. Teardown verified (local PG stopped, worktree removed clean). Finding Local turbo cache served a DTS-less
@objectstack/plugin-authbuild in a fresh worktree withOS_SKIP_DTSunset — its own typecheck then reds on a diff that never touched it #11907 (turbo cache serving a DTS-less artifact) correctly filed separately.
Clause-②: yes (public contract shape widened); per the maintainer's 2026-08-24 self-review ruling this seat's review is the contract review. Ready + auto-merge on full green; card closes on merge; patch-scoped H17 notices go to #6009/#8740 at merge (their regions untouched — hunks are in the FK arm and interface).
Generated by Claude Code
- The ruling is carried out to the letter: optional
- added a commit that references this issue
on Sep 1, 2026
Found while implementing #11324, which repairs the join correlations in
SqlDriver.introspectForeignKeys' Postgres arm. Deliberately not fixed there: this is a shape question aboutIntrospectedForeignKey, a different class from the join-correlation defects that card owns, and its resolution changes an interface every consumer reads. #11324 is a separate subject and is not addressed by this issue.What
packages/drivers/driver-sql/src/sql-driver.ts—IntrospectedForeignKeycarrieswith no schema qualification, and every arm fills it from a bare relation name (
ccu.table_namebefore #11324,parent.relnameafter).Until #11324 that key was never observably wrong on Postgres, because a foreign key whose target lived in another schema contributed zero rows and never reached a consumer at all. Repairing that defect is what makes this one reachable: the cross-schema constraint is now returned, and it is returned naming its parent by a bare name that the session's
search_pathdoes not resolve.Measured
On live PostgreSQL 16.13,
search_path = os11324_probe:introspectForeignKeys('cross_child')now answersremote_parentis not reachable by that name fromos11324_probe. The answer is true of the constraint and unusable as an address.Why it is not merely cosmetic
packages/objectql/src/util.ts:209turns each foreign key straight into a lookup field:So the bare name becomes an object reference. Two outcomes, both silent: it points at nothing, or — the worse one, and the same collision family #11201 documented — it points at a same-named table in the current schema, i.e. a lookup wired to the wrong object with no diagnostic anywhere.
Options, not a recommendation
Recording the shape rather than choosing it, because the choice reaches every consumer of
IntrospectedTable:referencedSchemaand leavereferencedTablebare — additive, but every consumer must learn to read it or nothing changes.referencedTablewhen (and only when) the parent is outside the session'ssearch_path— no interface change, but the key's spelling becomes conditional, which is its own trap.Option 1 is the shape
introspectSchema's other keys would suggest; option 2 is the one that needs no consumer change and is therefore the one most likely to be chosen for the wrong reason. This wants a decision, not a patch.Not measured here
Whether any in-tree deployment currently declares a cross-schema foreign key. The reachability above was reproduced directly against the driver on a live server; it does not depend on a consumer to be wrong.