Uh oh!
There was an error while loading. Please reload this page.
fix(driver-sql): report every member of a SQLite composite primary key, in key order - #11104
Conversation
…y, in key order `SqlDriver.introspectPrimaryKeys` filtered `PRAGMA table_info` rows on `row.pk === 1`. SQLite does not report `pk` as a boolean — it is the column's 1-based position WITHIN the primary key (`0` = not part of the key, `1` = first key column, `2` = second, ...). The filter kept only the first member of a composite key and silently dropped the rest. Both output signals were wrong together and for the same reason: `introspectSchema` derives `col.isPrimary` FROM `primaryKeys`, so a consumer could not cross-check its way back to the dropped member. Repairing the list repairs the flag with it. The rows are now also ordered by the `pk` ordinal instead of being taken in `table_info` row order (which is COLUMN order). The two differ whenever a key is declared out of column sequence, and `primaryKeys` is consumed as an addressing / upsert-conflict-target key where order is load-bearing. `SqliteWasmDriver` and `TursoDriver` extend `SqlDriver` and override neither method, so they inherit the repair. The Postgres and MySQL arms did not carry this defect and are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 9 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 bea128b440481ff3542cd5a40d69528fb7b9ae79 && git checkout bea128b440481ff3542cd5a40d69528fb7b9ae79
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47f6974a275586355f06abd83a0975b8637d12 035bcfa735dcf40eed5c9d7562b8eb783d72527c && git checkout -B drift-repro ab47f6974a275586355f06abd83a0975b8637d12 && git merge --no-ff 035bcfa735dcf40eed5c9d7562b8eb783d72527c
node scripts/docs-audit/affected-docs.mjs --json ab47f6974a275586355f06abd83a0975b8637d12 |
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32587559799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…in after the driver aligned to spec The `primaryKeyReader` docblock is the one place this seam's reasoning is written down, and two of its statements went false when the driver was aligned to the `packages/spec` introspection contract (`95437e7d2d`, #10676/#10998) and when the SQLite composite-key truncation was repaired (#10997, PR #11104). Comment-only. No executable line changes. - The producer table said `SqlDriver` spells the per-column signal `isPrimary`. It emits `primaryKey` and no `isPrimary` at all. - The `#10997` note said the SQLite composite-key truncation "is upstream of this seam and is not repaired here". It was repaired; `introspectPrimaryKeys` now reports every member of a composite key in declared key order. - The closing note said reading the extra spellings structurally was needed "because reconciling `objectql/src/util.ts` with the spec contract is a spec-owned change". That reconciliation has happened. - The `refreshCatalog` seam comment repeated the stale claim that "a real driver spells this `isPrimary` / `primaryKeys`, never `primaryKey`". - The seam suite's second `describe` said `SqlDriver` derives `isPrimary` FROM `primaryKeys`; it derives `primaryKey`. The union read is UNCHANGED — all three arms stay. `table.primaryKeys` is live and independent: after #10997 it is the only signal here that carries a composite key in declared key order, which a per-column boolean cannot express. The `isPrimary` arm is now recorded as a deliberate compatibility belt rather than a bridge to a live producer, with the measurement that decided it, so the next reader does not re-derive it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
…in after the driver aligned to spec (objectstack-ai#11206) The `primaryKeyReader` docblock is the one place this seam's reasoning is written down, and two of its statements went false when the driver was aligned to the `packages/spec` introspection contract (`95437e7d2d`, objectstack-ai#10676/objectstack-ai#10998) and when the SQLite composite-key truncation was repaired (objectstack-ai#10997, PR objectstack-ai#11104). Comment-only. No executable line changes. - The producer table said `SqlDriver` spells the per-column signal `isPrimary`. It emits `primaryKey` and no `isPrimary` at all. - The `objectstack-ai#10997` note said the SQLite composite-key truncation "is upstream of this seam and is not repaired here". It was repaired; `introspectPrimaryKeys` now reports every member of a composite key in declared key order. - The closing note said reading the extra spellings structurally was needed "because reconciling `objectql/src/util.ts` with the spec contract is a spec-owned change". That reconciliation has happened. - The `refreshCatalog` seam comment repeated the stale claim that "a real driver spells this `isPrimary` / `primaryKeys`, never `primaryKey`". - The seam suite's second `describe` said `SqlDriver` derives `isPrimary` FROM `primaryKeys`; it derives `primaryKey`. The union read is UNCHANGED — all three arms stay. `table.primaryKeys` is live and independent: after objectstack-ai#10997 it is the only signal here that carries a composite key in declared key order, which a per-column boolean cannot express. The `isPrimary` arm is now recorded as a deliberate compatibility belt rather than a bridge to a live producer, with the measurement that decided it, so the next reader does not re-derive it. Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4 Co-authored-by: Claude <noreply@anthropic.com>
Fixes#10997
SqlDriver.introspectPrimaryKeyscollected SQLite primary-key columns withif (row.pk === 1).PRAGMA table_infodoes not reportpkas a boolean — it is the column's 1-based position within the primary key (0= not part of the key,1= first key column,2= second, and so on). The filter therefore kept only the first member of a composite key and silently dropped the rest.Both output signals were wrong together and for the same reason:
introspectSchemaderivescol.isPrimaryfromprimaryKeys(if (primaryKeys.includes(col.name)) col.isPrimary = true), so a consumer could not cross-check its way back to the dropped member. Repairing the list repairs the flag with it — verified in the pin rather than assumed.The rows are now also ordered by the
pkordinal instead of being taken intable_inforow order, which is column order. The two differ whenever a key is declared out of column sequence, andprimaryKeysis consumed as an addressing / upsert-conflict-target key where the order is load-bearing.Measured
Red baseline — the new pin run against the unrepaired driver:
That second failure is independent evidence for the ordinal reading: on a table with columns
(carrier_code, shipment_id, leg_seq)andprimary key (shipment_id, carrier_code), the oldpk === 1filter selectedshipment_id— the second column but the first key member.pktracks key position, not column position.After the repair, same suite:
Tests 1792 passed | 72 skipped (1864).What the pin asserts
New file
packages/drivers/driver-sql/src/sql-driver-composite-primary-key-introspection.test.ts, over real in-memory SQLite (better-sqlite3):PRAGMA table_info(order_lines)reports{ order_id: 1, line_no: 2, sku: 0 }. If SQLite ever reportedpkas a boolean, the repair would be wrong, and this says so out loud.primaryKeysdeep-equals['order_id', 'line_no'], andisPrimaryis{ order_id: true, line_no: true, sku: false }.missing_key_member/unexpected_key_memberfor a genuinely different key, andkey_order_mismatchfor a reordered one — so the empty result is a measurement rather than a vacuous assertion.['id'], and an unkeyed table reports[](that last one is what would catch the repair drifting topk >= 0).Dialect arms actually executed
Only the SQLite arm was executed (better-sqlite3, in-memory). The Postgres and MySQL arms were read, not run: this container has no listener on 5432/3306, no DSN in the environment, no
mysqlclient, and the Docker CLI is present but has no daemon socket. Nothing here claims either arm was verified.Confirming what the issue asked about them: neither orders by key position —
ORDER BYis absent from both. The Postgres arm testsa.attnum = ANY(i.indkey), which reads the orderedint2vectoras a set and discards the position; the MySQL arm never selects or orders byKEY_COLUMN_USAGE.ORDINAL_POSITION. Out of scope for this PR and filed separately as #11101, deliberately rather than for convenience: the whole method body sits insidecatch { /* silently ignore */ }returning[], so a rewritten query that is invalid on some server version would degrade to no primary key at all with no diagnostic. Unexecuted SQL behind a silent catch is the wrong risk to take for an ordering improvement.Inheritance
SqliteWasmDriverandTursoDriverextendSqlDriverand override neitherintrospectPrimaryKeysnorintrospectSchema(SqliteWasmDriveradditionally overridesisSqlitetotrue, since it passes a dialect class asclientrather than a string, so the SQLite arm is genuinely selected). Both inherit the repair. Their suites pass: driver-sqlite-wasm 395, driver-turso 1006.Output shape
Unchanged —
IntrospectedTablestill{ name, columns, foreignKeys, primaryKeys },primaryKeysstillstring[]. Only the completeness and order of the values change, so the downstreamprimaryKeyReaderunion seam inpackages/services/service-datasourceneeds no repricing; the truncation its docblock records as "upstream of this seam and is not repaired here" is now repaired upstream.service-datasourcesuite passes: 559.Verification
All at
035bcfa73.pnpm --filter @objectstack/driver-sql testTests 1792 passed | 72 skipped (1864),TEST_EXIT=0pnpm --filter @objectstack/driver-sql typecheckTYPECHECK_EXIT=0pnpm lint(repo-wide,eslint . --no-inline-config)pnpm check:driver-conformanceOK — 45 covered cell(s), 0 in the DEBT ledger, 0 exempt.pnpm check:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none newpnpm check:test-source-aliasOK — 72 packages with tests scannedpnpm check:type-source-resolutionOK — 77 packages with a tsconfig.json scannednode scripts/check-ci-filter-parity.mjsOK: all 83 declared cross-package glob(s) (72 unique) are coverednode scripts/check-plugin-teardown-shape.mjs✓ 63 Plugin implementation(s) across 4457 source(s)node scripts/docs-audit/check-affected-docs.mjs✓ affected-docs self-test: 339 cases pass.pnpm check:nul-bytes✓ self-test: 75 assertions, exit 0pnpm check:query-options-erasure✓ ratchet holds: 67 unswept non-test site(s) in 17 file(s), none newpnpm check:engine-double-contractOK — 377 pinned, 133 in the DEBT ledger, 2 exempt.pnpm check:where-matcher✓ 278 matcher(s) discovered, 278 answer the combinator battery correctly or refuse it loudlypnpm check:type-check-coverageOK — 65/78 workspace packages type-checkedpnpm check:type-check-debt--re-measure: OK — 33 ledger entr(ies) re-measured in 388.8s, 1908 raw tsc error(s) total, none above its recorded number.pnpm check:changeset-gate-self-testspnpm check:objectui-changeset✓ objectui-changeset-digest --self-test: all checks passednode scripts/check-adr-0087-registration.mjsnode scripts/check-changeset-no-major.mjsnode scripts/check-empty-changeset.mjsGate families were re-derived from the actual diff with
node scripts/pm/dispatch-gates.mjs(no paths — it takes its own change set from the merge base). That added five convention-triggered families beyond the dispatch lead, all of them triggered by adding a test file:check:query-options-erasure,check:type-check-coverage,check:type-check-debt,check:engine-double-contract,check:where-matcher. Thetype-check-debtratchet was run against a fully built workspace closure (turbo run build, 70/70 successful), as it refuses to measure otherwise.Changeset:
.changeset/sqlite-composite-primary-key-introspection.md(@objectstack/driver-sql: patch).Generated by Claude Code