Repository navigation
fix(driver-sql,cli): a unique index its rows violate is listed as not appliable, never offered --allow-destructive - #25217
Conversation
…appliable, per entry Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRXQdFH1TrCyDQ5L1xDQRg
…-entry apply limit; changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRXQdFH1TrCyDQ5L1xDQRg
📓 Docs Drift Check4 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 — 37 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 1acb4f970752aa2ed4cd2c96d6bbacfb7b4776ae && git checkout 1acb4f970752aa2ed4cd2c96d6bbacfb7b4776ae
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 15764cd723e750c01416cfe91b44bbd0a4c28f37 fa53728a843bc719b9781082da7f958a89c51d75 && git checkout -B drift-repro 15764cd723e750c01416cfe91b44bbd0a4c28f37 && git merge --no-ff fa53728a843bc719b9781082da7f958a89c51d75
node scripts/docs-audit/affected-docs.mjs --json 15764cd723e750c01416cfe91b44bbd0a4c28f37 |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对ACCEPT ·
|
Requested by Bill · project thread
Fixes #25031
Clause-②: yes (widening)
Metadata-protocol: none
Card thread of
domain:engineseat 2 (seat post #20966), sessionsession_01YRXQdFH1TrCyDQ5L1xDQRg, claim 6340150456. Cross-lane declaration for the one CLI line: comment 6340209215 on #22648.What changes
driver-sql's ADR-0120 D4 duplicate pre-flight now declares a blocked unique index's apply limit on the entry: a new optionalManagedDriftEntry.applyLimit, set in the same branch that marks the entry BLOCKED, with the conflicting groups as the reason. The CLI's apply-limit seam (SchemaStack.driftApplyLimit,schema-migrate.ts) reads the entry's own limit first, then the owning driver's per-op one. One line of CLI code, as routing correction 6307307994 invited.Nothing in
plan.ts/apply.tschanges. The rendering PR #24954 and PR #25029 built for declared limits takes over: the entry is listed with its reason, the footer and the skip lines stop offering--allow-destructivefor it, and it is counted among the changes that remain drift.Measured (SQLite file, rows duplicating a plain and an organization-scoped unique index)
main@e5f194bos migrate planApply with: os migrate apply (add --allow-destructive for drops / tightenings), exit 0"os migrate apply" cannot apply it on this database: existing rows already violate the unique key: (code="A") × 2 rows.; footerApply with: os migrate applyand2 change(s) above cannot be applied on this database …, exit 0os migrate apply --yes2 destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them).andSkipped 2 destructive change(s) (re-run …), exit 02 destructive change(s) remain drift that a re-run with --allow-destructive cannot apply on this database eitherandSkipped 2 … that a re-run could not apply on this database either, exit 0os migrate apply --allow-destructive --yesPARTIAL apply, exit 1PARTIAL apply, exit 1 (unchanged, see below)plandriver-turso, measured at the driver: on the local and the remote face alike the blockedcreate_indexwasdestructivewith BLOCKED in the message, no structured field, anddriftApplyLimit('create_index')answerednull. Both faces inherit the pre-flight (detectTableDrift→applyNullSafeUniquePreflight; the remote face overrides only the probe's statement), so both now carryapplyLimit.TursoDriveris untouched and still answers by op type.Why a structured field rather than a widened
driftApplyLimit(entry)driftApplyLimitwould still need the driver to remember it per entry (a field or a side table) and add a second door to the same fact.yes (widening), measured:applyLimitappears once inpackages/drivers/driver-sql/dist/index.d.ts). Widening the parameter ofTursoDriver.driftApplyLimit(opType), which is published, would narrow it for every existing caller, and an older CLI calling a newer driver with an op-type string would read.opof a string.--jsondocuments now carryapplyLimiton a blocked entry. That is an additive key, and no other entry gains it.Not changed, deliberately: the PARTIAL verdict under
--allow-destructiveThe seat's mechanism note expected
apply --allow-destructiveto stop ending PARTIAL on the blocked entry's account. It still does, exit 1.apply.tsdocuments thatnotApplied, PARTIAL and the exit code never read declared limits: an entry the operator confirmed and the driver did not apply makes the run PARTIAL, because the database still differs from its metadata. It is the same verdict the Turso REMOTE face gives a tightening (apply.remote-face.integration.test.ts). Excluding declared limits from PARTIAL would change the CLI's exit-code contract for both cases, which is thedomain:clilane's call and more than the one line this card carries. The new integration pin records today's verdict, so a later ruling flips one assertion. Raised in the report as an open question.Tests
packages/drivers/driver-sql/src/sql-driver-25031-blocked-unique-apply-limit.test.ts(new): plain and organization-scoped blocked entries carryapplyLimitnaming the conflicting group, and the apply skips them underallowDestructive. Controls: a duplicate-free unique index has noapplyLimitand a plain apply creates it, and deduplicating withdraws the limit.packages/drivers/driver-turso/src/turso-remote-drift-apply-limit.test.ts(+1 describe): local and remote faces each declare the limit on the entry, whiledriftApplyLimit('create_index')staysnull.packages/cli/src/commands/migrate/apply.blocked-unique-index.integration.test.ts(new, integration tier): real boot, realplan/applyover a SQLite file. Each blocked entry lists its conflicting group as the limit. No line offers--allow-destructiveinplan,apply --yesorapply --allow-destructive --yes, and the last still reports PARTIAL with exit 1. Control: duplicate-free unique indexes keep the unchanged footer, and a plain apply creates them.Runs at
fa53728a8:driver-sqlvitest 232 files / 3752 passed, 12 files skipped;driver-turso94 / 2485 passed;cli --project unit324 / 4777 passed; the new integration file 4/4;typecheckgreen for all three packages.Ablation (
scripts/ablation-replace.mjsdeleting thed.applyLimit = …line,driver-sqlrebuilt,ablation-dist-preflight --absentpassing): the driver-sql pins went red (3 of 4: both pins plus the dedup control, which first asserts the limit), with the duplicate-free control green. The Turso pins went red (2 of 6), with the 4 pre-existing cases green. The CLI pins went red (3 of 4), with the control green. Restored: blob equalsHEAD,git diff HEADempty,driver-sqlrebuilt, and the preflight marker is present again indist/.Gates
dispatch-gates --commandsderived 73 families atfa53728a8. 71 exited 0.check-plugin-teardown-shape --self-testexited 3 on the shallow clone, then 0 after fetching its fixture commit. NOT MEASURED locally:check:dual-build-cjs-loadsandcheck:i18n-coverage(exit 3, PREREQUISITE NOT MET: they need the whole workspace built). CI runs both.--ranreconciliation: 73 derived, 71 run, 2 NOT-MEASURED.check:driver-conformance: OK, 55 covered cells, 0 DEBT, 0 exempt. No ledger file is touched.Acceptance notes
Destructive (requires --allow-destructive), becauserenderPlangroups bycategory. The entry's own line and the footer now say it cannot be applied. The heading is CLI rendering and was out of scope. The Turso REMOTE tightening reads the same way today.category: 'destructive'itself is unchanged. It is what keeps a plain apply, devautoMigrate: 'safe'and the artifact boot gate from attempting the CREATE. Reclassifying it is a separate question, and this PR does not answer it.recreate_indexwithtightenNullSafeOnly) sets the same field in the same branch. Since ADR-0131 D1 made the organization key part plain, no fixture here reaches that arm, so it is covered by construction, not by a pin.[sql-driver] cannot create unique index …at everyos migrate apply, as onmain.Generated by Claude Code