Uh oh!
There was an error while loading. Please reload this page.
fix(driver-sql): introspectSchema() emits the spec introspection contract — primaryKey, dialect, introspectedAt (#10676, #10998) - #11124
Conversation
…rimary` (#10676) `SqlDriver.introspectSchema` declared its own `IntrospectedColumn` and spelled primary-key membership `isPrimary`, while everything downstream of `plugin.ts` is typed against `packages/spec/src/contracts/schema-diff-service.ts`, which spells it `primaryKey`. The value crossed between the two contracts untyped, so `ExternalDatasourceService.generateObjectDraft` — which reads `col.primaryKey` — saw `undefined` on every real driver result and every federated object drafted from a remote table silently lost its remote primary key. Maintainer ruling, 2026-08-22 (live session, 「同意所有」 item 9 = 驱动侧对齐 spec 契约): `packages/spec` is the one contract and the driver aligns to it. - `driver-sql` and `objectql/src/util.ts` now DERIVE `IntrospectedColumn` from the spec import instead of re-declaring it, so a key added to the contract fails their `tsc` until the producer emits it. Two divergences are kept explicitly (`defaultValue` stays `unknown` — Knex reports `null`; `isUnique` / `maxLength` are SQL extras the spec does not declare). - The driver emits `primaryKey` and no longer emits `isPrimary`: one spelling, so no consumer can key off the wrong one again. - New pin `external-object-draft-real-introspection.test.ts` drives `generateObjectDraft` off a REAL `introspectSchema()` result, which is what the ruling requires and what the hand-written fixtures could never do. It fails on the pre-fix driver (measured: the `// Remote primary key:` line is absent) and asserts the key does not return as the unauthorable `fields.<f>.primaryKey`. - `external-introspection-seam.test.ts` asserted the producer's OLD spelling as a deliberate landmine; its producer-spelling case is flipped to the new direction. The service's three-signal union read is untouched. Fixes#10676 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
#10998) `introspectSchema()` returned `{ tables }` and nothing else, while `packages/spec/src/contracts/schema-diff-service.ts` declares `dialect` and a REQUIRED `introspectedAt`. Measured live on in-memory SQLite before this change: `Object.keys()` was `["tables"]`. Two silent consequences — type mapping ran with `dialect: undefined` on the whole federation path, so every per-dialect alias in `suggestFieldTypeForSqlType` was unreachable there, and `refreshCatalog` persisted `dialect: undefined` into the `external_catalog` record Studio's schema browser and the boot gate read back. Same ruling as #10676 (2026-08-22, 「同意所有」 item 9): the driver aligns to the one contract. - `IntrospectedTable` / `IntrospectedSchema` now derive from the spec import in both `driver-sql` and `objectql/src/util.ts`, so `introspectedAt` being required is enforced by `tsc` rather than by remembering. `indexes` is `Omit`ted rather than emitted empty: this driver does not introspect indexes, and `[]` would claim a table has none — filed separately, not guessed. - `dialect` is `this.dialectName` (`sqlite` / `postgres` / `mysql` / `unknown`), NOT the raw Knex client and NOT the spec's `SQLDialectSchema` enum: the only in-tree consumer keys `DIALECT_ALIASES` on the `SqlDialect` vocabulary of `type-compat.ts`, which spells PostgreSQL `postgres`. Emitting `postgresql` would satisfy `Object.keys()` while leaving the aliases just as unreachable. - `introspectedAt` is stamped before the reads begin, so it never claims to cover a moment later than the first table actually read. Measured after: `Object.keys()` is `["tables","dialect","introspectedAt"]`; the persisted catalog records `dialect: 'sqlite'`. On the SQLite arm the suggested field types are byte-identical before and after (its alias map overlaps the base map); the per-dialect payoff is on Postgres/MySQL, which are unreachable from this container and are therefore not claimed. Fixes#10998 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
…10676, #10998) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
📓 Docs Drift CheckThis PR changes 2 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 5609c9c58a93bb7a6a57b1d60102fdec107d2fec && git checkout 5609c9c58a93bb7a6a57b1d60102fdec107d2fec
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 422f997d50817d3a1992ad2321b63d516f65fbcf 86ccc458cc50b6b5705cfe8a68b884eed803a6d2 && git checkout -B drift-repro 422f997d50817d3a1992ad2321b63d516f65fbcf && git merge --no-ff 86ccc458cc50b6b5705cfe8a68b884eed803a6d2
node scripts/docs-audit/affected-docs.mjs --json 422f997d50817d3a1992ad2321b63d516f65fbcf
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32593706263 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10676
Fixes#10998
Folded family: both cards are one clause of one ruling — the driver emits the spec shape — landing in one method. One commit per member, so neither half can be silently under-delivered.
⛔
packages/specitself is not edited. This is conformance to a contract it already declares.The defect, measured before the change
Live in-memory SQLite, the driver's own bytes for a
primary key (id)table onorigin/main@b302aabc7:Three declarations described one idea in two vocabularies —
packages/spec/src/contracts/schema-diff-service.ts,packages/drivers/driver-sql/src/sql-driver.tsandpackages/objectql/src/util.ts. Each side compiled against its own, and the value crossed between them with no compiler in the middle.What changed
primaryKeyon columns (#10676). The driver emits the spec spelling and no longer emitsisPrimary— one spelling, so no consumer can key off the wrong one again.IntrospectedColumnis now derived from the spec import in bothdriver-sqlandobjectql, so a key added to the contract fails theirtscuntil the producer emits it.dialectand the requiredintrospectedAt(#10998).IntrospectedTable/IntrospectedSchemaderive from the spec import too, which is what makesintrospectedAtenforced rather than remembered.dialectisthis.dialectName(sqlite/postgres/mysql/unknown) — deliberately not the raw Knex client and not the spec'sSQLDialectSchemaenum. The only in-tree consumer issuggestFieldTypeForSqlType(col.type, schema.dialect as SqlDialect), whose vocabulary intype-compat.tsspells PostgreSQLpostgres; emitting the enum'spostgresqlwould have put the key inObject.keys()while leaving every per-dialect alias exactly as unreachable as before — the omission wearing a fix's clothes.introspectedAtis stamped before the reads begin, so it never claims to cover a moment later than the first table actually read.Two divergences are kept, explicitly and visibly (spelled as
Omitfrom the spec type rather than left to drift):defaultValuestaysunknownat the SQL layer because Knex reportsnull, andindexesis omitted rather than emitted empty, becauseindexes: []tells a schema differ a table HAS no indexes when it merely was not asked. Both are filed, not guessed: #11122.Acceptance, answered separately
86ccc458cgenerateObjectDraftoff a realintrospectSchema()carriesprimaryKey// Remote primary key: id; composite keys name every member in declared key orderObject.keys()of a live result containsdialectandintrospectedAt["tables","dialect","introspectedAt"],dialect: "sqlite",introspectedAta real ISO instantfields.FIELD.primaryKeyis not authorable, sogenerateObjectDraft/renderObjectSourceno longer render it and the introspected key survives as a comment in the generated source. So "the draft carries the key" is asserted ondraft.source, and the new pin also asserts the definition stays free of the unauthorable key — an implementation that re-emitted it onto the field would satisfy the first half while re-opening a defect the platform's own validator refuses.The pin the ruling requires
external-object-draft-real-introspection.test.tsdrivesgenerateObjectDraftoff a realSqlDriver.introspectSchema()against a live database — the case the original suite could not be, because every other draft fixture hand-writes its remote schema in the spec spelling and is therefore blind to the seam. Reverse-verified: against the pre-fix driverdistit fails on exactly the two assertions the card is about (expected undefined to be '// Remote primary key: id'), and its negative case — a table declaring no key — passes in both directions.The driver-side pin
sql-driver-introspection-spec-contract.test.tsasserts the emitted bytes:primaryKeytrue on the key column and false on the others,'isPrimary' in col === false, and the two schema keys with their values.Cross-lane touch, and its exact boundary
packages/services/service-datasource/src/__tests__/external-introspection-seam.test.tswas built by the services seat as a deliberate landmine — it asserts the producer's spelling, so it reddens on purpose when the driver aligns. It did, exactly once (1 failed / 561 passed). Changed there, and nowhere else in that package:id.primaryKey === true,id.isPrimary === undefined);isPrimaryarm now has no in-tree producer.⛔ Not touched:
primaryKeyReaderand its three-signal union read (col.primaryKey/col.isPrimary/table.primaryKeys). That belt is the services lane's, on its own one-release clock. Its docblock is now stale in two ways — it still namesSqlDriveras theisPrimaryproducer, and its#10997note still says the composite-key truncation is unrepaired upstream — recorded for that lane as #11123 rather than edited here.Not in this family
#11000 remains open and untouched — a different key (
fields.FIELD.primaryKeyin a rendered definition) in a different place; aligning the driver does not make it authorable. #11009 (compare-and-setwhere, same file) and #11101 (PG/MySQL key ordering plus the silentcatch … => []) are out of scope. #10997 is already merged.Verification
All at
86ccc458c, all green, each read from the command's own verdict line:pnpm --filter @objectstack/driver-sql exec vitest run— 1795 passed | 72 skippedpnpm --filter @objectstack/objectql exec vitest run— 4034 passedpnpm --filter @objectstack/service-datasource exec vitest run— 563 passedtypecheckfor all three packages — cleanpnpm lint(repo-wideeslint . --no-inline-config) — clean, 127s, no narrowing needednode scripts/pm/dispatch-gates.mjsagainst the real change set, all green:check:driver-conformance·check:durability-log-level·check:slot-lookup·check:test-source-alias·check:type-source-resolution·check-ci-filter-parity·check-engine-split-ratio(97.5%, no shallow-clone refusal) ·check-plugin-teardown-shape·docs-audit/check-affected-docs·check:nul-bytescheck:query-options-erasure·check:type-check-coverage·check:type-check-debt --re-measure(33 ledger entries, none above its recorded number) ·check:engine-double-contract·check:where-matchercheck-adr-0087-registration(dispositionruntime-interface-only, mechanically verified) ·check-changeset-no-major·check-empty-changeset·check:changeset-gate-self-tests·check:objectui-changesetDialects: only SQLite is executed here — Postgres and MySQL are unreachable from this container (no listener, no DSN). Both repaired values are built at a single dialect-independent site in
introspectSchema, downstream of the per-dialect helpers, so the shape cannot vary by dialect. No per-dialect claim aboutprimaryKeyscontent is made. Measured and worth stating plainly: on the SQLite arm, supplying the dialect leaves the suggested field types byte-identical for all 10 columns of a wide fixture — its alias map overlaps the base map — so the type-mapping payoff ofdialectis on Postgres/MySQL and is not claimed here.Generated by Claude Code