Repository navigation
fix(cli,driver-turso): os migrate plan on a Turso remote database lists the changes it cannot apply with the transport's limit, not --allow-destructive - #24014
Conversation
…ver's apply limit (#23925) Red on the base: the plan ignores the reporting driver's declaration and prints the shared differ's --allow-destructive remedy for an entry the Turso REMOTE transport cannot apply. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
#23925) Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
… and never offers --allow-destructive for a change a Turso REMOTE database cannot apply (#23925) The REMOTE face declares, once (REMOTE_DRIFT_OP_ANSWERS), which drift ops its apply door performs; applyMigrationEntries dispatches by it and TursoDriver.driftApplyLimit answers the plan from it. The plan asks the driver that reported each entry (SchemaStack.driftApplyLimit), lists an entry it cannot apply with its finding and that limit instead of the --allow-destructive sentence, and offers --allow-destructive only for a destructive entry the database can apply. A driver that declares nothing reads as before; the --json document is unchanged. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
…mote-plan-remedy Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
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 e307d1a65f6111aeede2c075b5da07e8e7dddacf && git checkout e307d1a65f6111aeede2c075b5da07e8e7dddacf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cf31c34823214dccabbf6bd7b8db446d03c61d71 512414eef4be39dfe89cc74c5cee27c43a3976b3 && git checkout -B drift-repro cf31c34823214dccabbf6bd7b8db446d03c61d71 && git merge --no-ff 512414eef4be39dfe89cc74c5cee27c43a3976b3
node scripts/docs-audit/affected-docs.mjs --json cf31c34823214dccabbf6bd7b8db446d03c61d71
|
… the driver's apply limit (#23925) Red on 59f489f: the apply's pre-confirmation listing still prints the shared --allow-destructive remedy for an entry the Turso REMOTE transport cannot apply, and its held-back count offers a re-run for it. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
…ver's apply limit, as the plan does (#23925) The listing apply prints before its prompt passes renderPlan the plan's describe (now one helper, describeWithApplyLimits, beside collectDriftApplyLimits), and the held-back count offers a re-run with --allow-destructive only for the destructive entries the database can apply, naming the rest as drift a re-run cannot apply. Nothing after the prompt and nothing in --json changes. The changeset moves to Clause-② yes (widening): driver-sql and driver-turso minor, cli patch. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
…ts human case asserts the pre-flight ran (#23925) Without the SchemaStack member the apply listing now reads, the human case still exited 1, but from a TypeError before the pre-flight: a pass for the wrong reason. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
…he driver's apply limit (#23925) Red on 455c7f2: after the apply runs, 'Skipped N destructive change(s) (re-run with --allow-destructive to include them).' still counts a tightening a re-run cannot apply on a Turso REMOTE primary. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
…driver's apply limit; docs name the Turso remote caveat (#23925) The post-apply 'Skipped N destructive change(s) (re-run with --allow-destructive to include them).' now counts only the held-back entries a re-run could apply on this database, and names the rest as drift a re-run could not apply either, by the same limits map the pre-prompt count reads. notApplied, PARTIAL, the exit code and --json are unchanged; a driver that declares nothing reads as before. The migrate category table in the CLI docs gains one caveat for a Turso remote primary. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · ✅ ACCEPT: PR #24014 at
|
Fixes #23925
Clause-②: yes (widening)
What changes
On a Turso REMOTE primary (
libsql://...), neitheros migrate plannoros migrate apply(its listing before the prompt, and both of its held-back lines) offers--allow-destructivefor a drift entry that database cannot apply. The driver that reported each entry declares what its apply door cannot perform, and both commands read that declaration. Neither branches on the dialect, the transport name, the URL or the driver's class.The declaration (driver-turso).
REMOTE_DRIFT_OP_ANSWERSinturso-driver.tsanswers every drift op the shared differ emits withapplied,needs-table-rebuildorreport-only. Itssatisfies Record(DriftOp type, answer)clause fails the package's typecheck when an op is added toDriftOpand left unclassified, theREMOTE_FACE_ANSWERSpattern. The REMOTEapplyMigrationEntriesdispatches by it: onlyappliedrows are sent, and every other entry is skipped before any DDL and logged with its reason.TursoDriver.driftApplyLimit(opType)answers from the same rows: the limit sentence for aneeds-table-rebuildop,nullfor every other op, andnullfor every op on the local and embedded-replica faces. The commands and the apply door therefore read one object and cannot disagree about an entry.The seam (cli,
schema-migrate.ts).SchemaStack.driftApplyLimit(entry)asks the driver that reported the entry. That is thedriftOwnersmapapplyMigrationEntriesalready routes by, read through the optionalSqlDriverLike.driftApplyLimit. A driver that declares nothing has no limit.collectDriftApplyLimits(stack, drift)gathers the entries that have a limit.describeWithApplyLimits(limits)is the one spelling of the line printed under such an entry: its finding, then"os migrate apply" cannot apply it on this database: LIMIT., in place of the--allow-destructivesentence. Both commands read it.os migrate plan. An entry with a limit is still listed and counted (it is still drift), under that line. The footer offers--allow-destructiveonly for the destructive entries the database can apply, drops the apply line when it can apply nothing listed, and says how many listed changes remain drift.os migrate apply: its listing and its two held-back lines.renderPlanthe samedescribe.N destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them)., now counts only the destructive entries a re-run could apply. The rest get their own line:N destructive change(s) remain drift that a re-run with --allow-destructive cannot apply on this database either (each says why above).Skipped N destructive change(s) (re-run with --allow-destructive to include them).for those a re-run could apply, andSkipped N destructive change(s) that a re-run could not apply on this database either: they remain drift (each says why above).for the rest.notApplied, not PARTIAL, not the exit code, and not the--jsondocument.The remedy sentence (driver-sql).
tighten_not_null,narrow_varcharanddrop_columnmessages end with anos migrate apply --allow-destructivesentence. That sentence is now spelled once (DESTRUCTIVE_COLUMN_REMEDY), with the same bytes.driftFindingWithoutRemedy(entry)takes exactly that tail off, and only the limited entries use it. Every other reader keeps the message as written.Public surface: the widening the declaration line names.
TursoDriver.driftApplyLimit(@objectstack/driver-turso) anddriftFindingWithoutRemedy(the@objectstack/driver-sqlentry) are new and additive, so those two packages gominor.@objectstack/clistayspatch. Nopackages/speccontract and no accept set moves.The CLI docs (
content/docs/deployment/cli.mdx). One caveat under the migrate category table:No declaration, no change. When no entry has a limit, both commands take the old code path: the same
renderPlan(drift)call, the same footer string and the same held-back lines. That is every database but a Turso REMOTE one.Each drift op, and what the REMOTE face does with it
drop_columndrop_indexcreate_indexrecreate_indexreplace_unique_indexrelax_not_nulltighten_not_nulldrop_column_defaultwiden_varcharnarrow_varcharmanual_column_type_changemanual_widen_varchar_to_textunbuildable_indexA report-only op answers
nullfromdriftApplyLimit. No face applies it, so its skip is not a limit of this transport, and its finding already names its remedy. The declaration is therefore a per-op answer rather than a set of appliable ops: a plain set would print this transport's rebuild reason against anunbuildable_index.Who reads the entry's
message, and why each one stays truthfulThe message keeps its bytes on every face.
schema-drift.remedy-sentence.test.tspins the three destructive column messages character for character. The report stays identical across faces, soturso-remote-drift-detection.test.tskeeps holding the local and remote reports equal. Only the human listings change what they print, and only for an entry whose driver declares a limit.os migrate plan(human,renderPlan): changed as above.os migrate apply(human, the listing before the prompt and the two held-back lines): changed as above.os migrate plan --jsonandos migrate apply --json: unchanged (acceptance note 1).[schema-drift]warning (SqlDriver.reconcileAndWarnDrift): runs only on the Knex faces, where the apply door does rebuild, so its remedy is true.OS_ARTIFACT_URLboot gate (artifact-boot-migration.ts): returns on the REMOTE face'sdriftReportCaveatbefore it reads a message.REMOTE_DRIFT_REPORT_CAVEAT: re-read, and still true ("applies those and names the rest as not applied"). Untouched.messageOf): reads messages only for its own column drops, which the REMOTE face applies. Its remote pin is green.Verification
All of it runs against a real
sqld(libsql-server 0.24.32,libsql://127.0.0.1:PORT?tls=0, archive sha256 checked). The fixture isapply.remote-face.integration.test.ts's own: a hand-madeos23848_notetable with three drifts,legacy_23848(drop_column),code(tighten_not_null) andmemo(relax_not_null). Each remote case has a local SQLite file CONTROL. CI has nosqld, so the unit pins hold the same rules against a stubbed stack whose driver double carries the declaration:plan.apply-limit.test.tsand the new block inapply.remote-posture.test.ts.Red, then green: the plan
a512b914f. That commit holds the pins alone; its production code equals the base2e1562091.Received: "os23848_note.code: metadata declares ... must be backfilled. Run "os migrate apply --allow-destructive".". The local-file CONTROL is green.--jsoncase are green.d99ee347e. sqld 7 of 7, unit 4 of 4.Red, then green: the apply's listing
ca87de9ee. That commit holds the pins alone; its production code equals59f489f6d.apply --yeslisting case and the listing assertion in the remoteapply --allow-destructivehuman case, both receiving... must be backfilled. Run "os migrate apply --allow-destructive".. Both local-file CONTROLs are green:apply --yeson a file keeps the remedy and2 destructive change(s) will be SKIPPED (re-run ...).apply.remote-posture.test.ts): 2 failed, 10 passed. The CONTROL and the--jsonequality case are green.48ac51325.organization-ownership.remote-face.integration.test.ts1 of 1 andartifact-boot-migration.unbuildable-index.test.ts3 of 3.--jsonpin compares the apply's document under a declaring driver with the one a driver that declares nothing gets, key for key and entry for entry.Red, then green: the apply's post-apply held-back line
db6056e76. That commit holds the pins alone; its production code equals455c7f288.apply.remote-posture.test.ts): 1 failed, 11 passed. The failure is the limited case: it received[" ⚠ Skipped 2 destructive change(s) (re-run with --allow-destructive to include them)."]where two lines were expected. The CONTROL (Skipped 2 ..., unchanged) is green.apply --yescase, with the same received line. The local-file CONTROL (Skipped 2 ...) is green.512414eef.apply.remote-posture,apply.account-issuer-json-refusal,plan.apply-limit,schema-migrate.lazy-driver-import).apply.remote-face.integration.test.ts9 of 9 andorganization-ownership.remote-face.integration.test.ts1 of 1.A pass for the wrong reason, closed
apply.account-issuer-json-refusal.test.tsstubs the stack without the newdriftApplyLimitmember. Its human case ("the human face of the same refusal exits 1") therefore still exited 1, but from a TypeError in the listing, before the pre-flight ran. The stub now declares no limit, and the case asserts the pre-flight ran.Ablations
d99ee347e,tighten_not_nullwas changed from'needs-table-rebuild'to'applied'inREMOTE_DRIFT_OP_ANSWERS.scripts/ablation-replace.mjsin WRAP mode, inside a script with atraprestore, and was proven on disk: anchor 1 to 0, blob1a3a5461to37038760.turso-remote-drift-apply-limit.test.tsfailed 1 and passed 3.git diff HEADempty.455c7f288, the stub'sdriftApplyLimit: () => null,was deleted the same way: anchor 1 to 0, blobfcd562cdto7509aeb0.expected "vi.fn()" to be called 1 times, but got 0 times: 1 failed, 3 passed.git diff HEADempty.src/through relative imports.End to end through the CLI's own entry
Both runs went through
tsx bin/run-dev.js, on asqldthis run started and stopped by its PID, with the same three drifts onos23925_note.migrate apply --yesat512414eefexited 0, and read:migrate planatd99ee347eexited 0, and its footer read:Tests and gates
Recorded at
512414eefunless a line says otherwise.@objectstack/cli(tsc --noEmit, thencheck:test-typecheckOK): exit 0 at512414eef.@objectstack/driver-tursoand@objectstack/driver-sql: exit 0 at59f489f6d. No driver file has changed since.@objectstack/cliunit tier. 3 shards at455c7f288: 103 + 102 + 102 files and 1754 + 1335 + 1486 tests, all passed. Since then the diff changes only the post-apply line inapply.tsand two test files, and the unit pins that reach it ran at512414eef: 4 files, 23 tests.@objectstack/cliintegration, run locally.512414eef, onsqld:apply.remote-face.integration.test.ts9 of 9, andorganization-ownership.remote-face.integration.test.ts1 of 1.48ac51325:artifact-boot-migration.unbuildable-index.test.ts3 of 3.59f489f6d:plan.telemetry-sibling,plan.deferred-readsandplan.reads-env-files, 3 files, 10 tests.d99ee347e. No driver file has changed since.@objectstack/driver-turso: 92 files, 2429 passed, 33 skipped.@objectstack/driver-sql: 223 files passed and 11 skipped; 3677 tests passed and 208 skipped.dispatch-gates --commands --repo objectstack-ai/objectstackgives 99 commands. That is the 69 from before plus 30 the docs path adds (the doc, docs and spec docs families). Each exit code was captured before any pipe.--ranreconciliation: 99 derived, 97 run with exit 0, 2 NOT MEASURED, 0 UNRUN.check:dual-build-cjs-loads(packages outside this build closure have nodist/) andcheck:i18n-coverage(a partial round). Both exited 3, PREREQUISITE NOT MET.pnpm --filter @objectstack/spec run check:skill-examplesfirst exited 3, because@objectstack/client-reactwas not built. After building the client SDK closure (all cache hits) it exited 0: 263 prose examples type-check across 3 surfaces. That exit 0 is the one recorded.pnpm check:doc-authoring. Exit 0.check-changeset-no-major.mjs --base origin/mainexits 0 with no event, and exits 0 with an event carrying this body. With this body it readsLEVEL AXIS: this PR declares clause-② yes (widening) ... at minor or above, naming@objectstack/driver-sql: minorand@objectstack/driver-turso: minor, with@objectstack/cli: patchnot refused.check-adr-0087-registration.mjs --base origin/mainexits 0:this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).512414eef, read 20:17Z.Build Docsand four of the five required contexts (Lint & Repo Gates,TypeScript Type Check,Build CoreandGoverned Surface Queue Guard).Test Core (3/6)was still in progress, so theTest Coreroll-up had not reported yet.File surface
The claim's surface, and its amendments 6222387090, 6223299622 and 6223389541:
packages/cli/src/commands/migrate/apply.ts: only the listing before the prompt and the held-back count line (amendment 6222387090).packages/cli/src/utils/schema-migrate.ts: thedriftApplyLimitseam, the two shared helpers, and an optionaldescribeargument torenderPlan.packages/drivers/driver-sql/src/index.ts: one re-export line.packages/cli/src/commands/migrate/apply.ts, by amendment 2 (6223299622): the post-apply held-back line as well.content/docs/deployment/cli.mdx, by amendment 3 (6223389541): one caveat under the migrate category table, and nothing else on the page.plan.apply-limit.test.ts;apply.remote-posture.test.ts, whose stub stack gains the member, plus a new block;apply.account-issuer-json-refusal.test.ts, whose stub gains the member, plus one assertion;apply.remote-face.integration.test.ts, which holds the sqld fixture;schema-drift.remedy-sentence.test.ts;turso-remote-drift-apply-limit.test.ts.Nothing under
packages/spec/**, and no published driver contract (packages/spec/src/contracts/**) gains a member. Neither command readsdialect, the transport, the URL or a class name.Acceptance notes
--jsondocuments are unchanged. Inos migrate plan --json,changes[].messagekeeps the shared remedy, and no field marks an entry the database cannot apply.os migrate apply --jsonis pinned equal to what a driver that declares nothing produces. Adding a mark widens a document, and no consumer of either JSON document was measured to need one.drop_column_default. Its message carriesrun "os migrate apply"mid-sentence rather than as a tail. On a remote primary, the listing line therefore shows that sentence with the limit after it. That entry needs a literal runtime-token DEFAULT written by an old build, so a database the current sync created does not reach it.... skipped: it needs the SQLite table rebuild, which the Turso REMOTE transport does not run, from the declaration's sentence. It used to endwhich this transport does not run. A report-onlyunbuildable_indexon that face is now logged as skipped; it used to be skipped with no log line. No test pins either log line.Generated by Claude Code