fix(driver-turso): refuse remote schema drift detection instead of answering "no drift" - #19891
Conversation
…swering "no drift" A remote TursoDriver inherited SqlDriver.detectManagedDrift, which reads the physical schema through the placeholder :memory: Knex connection remote mode is built with. Every table read as absent, so the answer was [] for every remote database, and the artifact-pinned boot gate of `os serve` read a remote Turso database as never drifted. The remote face now refuses with the transport's NOT_IMPLEMENTED/501 envelope, with or without explicit objects. The boot gate already reports a failed detection as a warning and continues the boot, so a remote boot is told the schema was not checked instead of being told nothing. Local and replica modes are unchanged. Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
…shape key for key check:object-def-param-keys arm C refuses an override that derives the base's object-definition parameter instead of declaring it, because the keys a caller may pass then vanish from the override's own declaration. Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
The operator reads this message long after the change that introduced it, so it states what answering from the placeholder would do rather than what the driver did "until this change". Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 6 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 a5982a406ee051d356ff901da140f96353461643 && git checkout a5982a406ee051d356ff901da140f96353461643
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dabf8d795ee9279c18b4b00bfabb322544106b2f 7d9a7e38c742305b04cb8e6b9ce611c74ff40326 && git checkout -B drift-repro dabf8d795ee9279c18b4b00bfabb322544106b2f && git merge --no-ff 7d9a7e38c742305b04cb8e6b9ce611c74ff40326
node scripts/docs-audit/affected-docs.mjs --json dabf8d795ee9279c18b4b00bfabb322544106b2f
|
Contract reviewServed-tier: Head matches the branch; merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19845
Clause-②: no (narrowing)
What changed
A remote-mode
TursoDrivernow refusesdetectManagedDrift()with the transport'sNOT_IMPLEMENTED/501envelope, with or without explicit objects, where it used to answer[]. The local and embedded-replica modes inherit the Knex detector unchanged.packages/drivers/driver-turso/src/turso-driver.ts:refuseRemoteDriftDetection()beside the deferred-DDL refusal, and adetectManagedDriftoverride in the schema-management section. The override spells the base's parameter shape key for key, ascheck:object-def-param-keysarm C requires.packages/drivers/driver-turso/src/turso-remote-drift-detection-refusal.test.ts: the pin, with local and replica controls..changeset/19845-turso-remote-drift-detection-refusal.md:minor, BREAKING banner,adr-0087: not-required (no-migration-prescription). This is the shape PR fix(driver-turso): refuse to arm deferred schema DDL on the remote face #19842 used for deferred DDL on this face.This is the dispatched landing site.
packages/cliis untouched.Zone 1: the chain is reachable
Read on base
1f89ba0d70:packages/cli/src/commands/serve.ts: withOS_ARTIFACT_URLset,pinnedArtifactboots throughcreateDefaultHostConfig, which callscreateStandaloneStack. Thecom.objectstack.cli.artifact-boot-migration-gateplugin then runsrunArtifactBootMigrationGate({ driver: findSqlDriverForKernel(kernel) })onkernel:ready.packages/runtime/src/standalone-stack.ts, turso arm: alibsql://URL declares thedefaultdatasource with the Turso factory.DefaultDatasourcePlugin.initregisters it asdriver.plus the engine's default driver name. That name is the driver'sname,com.objectstack.driver.turso.packages/cli/src/utils/schema-migrate.ts:SQL_DRIVER_SERVICESlistsdriver.com.objectstack.driver.turso. Its duck-type check needsdetectManagedDriftandapplyMigrationEntries, and the driver inherits both.Measured once and not committed. The instrument was the real
findSqlDriverForKernelandrunArtifactBootMigrationGatefrompackages/cli/src/utils/, over a kernel stub that exposes the driver underdriver.com.objectstack.driver.turso. Tabletis synced, then an extra physical columnlegacyis added.file:URL)ok: false, 1 destructive entry, so the boot is refusedok: true, 0 entries, no warningok: true, 0 entries, plus the warning⚠ Could not check the physical schema against the artifact (Schema drift detection is not supported by the Turso REMOTE transport …). Boot continues; …Not measured end to end: the
os servebinary against a live remote libSQL endpoint. The boot-sync door is the batch door, as PR #19863 measured on anObjectKernelboot.A1: reproduced
Setup: the
libsqlSQLite double; tabletdeclared withname: text, plus an extra physical columnlegacy.detectManagedDrift()t.legacy:unmapped_column, opdrop_column,destructivedetectManagedDrift()[]detectManagedDrift([{ name: 't', fields }])[]The remote table's physical columns were
id, created_at, updated_at, name, legacy, so the drift was on disk.A2: how the gate treats a driver that cannot judge
runArtifactBootMigrationGatewrapsdriver.detectManagedDrift()in atry. On any throw it warnsCould not check the physical schema against the artifact (MESSAGE). Boot continues; run 'os migrate plan' to verify.and returnsok: truewith nothing applied. The CLI's own suite pins this:artifact-boot-migration.test.ts, "warns and continues when drift detection itself fails". I found no capability flag and no sentinel. The only other channel isapplyMigrationEntries'sskippedlist, for a driver that declines an op.NOT_IMPLEMENTEDmakes the gate warn and continue. The boot does not stop and nothing crashes. This was measured after the fix; see the Zone 1 table.plain, andrichwith 16 field types, a field-leveluniqueand a declared index) were judged by a local Knex connection to the same SQLite file.detectManagedDriftreported 0 entries for each, the same as a local-face control. So the shared differ gives no false drift on remote-built tables.this.knex. That coversschema.hasTable,columnInfoplusPRAGMA table_infoordering, the SQLite arm ofintrospectIndexes(sqlite_master,index_list,index_info) andprobeNullSafeUniqueDuplicates. A remote version needs a second copy of each of those arms.applyMigrationEntrieson the safe ones. That inherited Knex path runs on the same placeholder.os migrate apply --allow-destructive. On this face that command refuses atsetDeferredDdl(PR fix(driver-turso): refuse to arm deferred schema DDL on the remote face #19842), so the refusal text would need a CLI change. The CLI is outside this card's surface.A3: the route taken
(i) exists, and a refusal through it does not stop any remote boot: the gate warns and continues. (iii) is not cheap and would pull in a CLI change. So this PR takes the loud refusal, declared as a narrowing. The refusal message names the working remedy, running
os migrate planagainst a local SQLite copy (afile:URL). The gate embeds that message in its own warning line.A4:
managedObjectFieldsYes, the no-argument
detectManagedDrift()needs it. That is the gate's call, and it iteratesmanagedObjectFields/managedObjectIndexes. On the remote face these stay empty, becauseregisterRemoteFieldMetadata→registerExternalObjectfills the read-coercion registries andremoteManagedObjectsonly. It is deliberately not fed here. The explicit-objects call answered[]too, because the KnexhasTableprobe reads the placeholder, so feeding the registry alone changes no answer.managedObjectFieldsalso has other readers:getManagedFields, the basepaginationTieBreakerandplanMediaColumnMove. A real remote detector would need a remote registry of fields plus indexes. The remote doors already receiveindexeson the object definition.Tests
turso-remote-drift-detection-refusal.test.ts, 6 cases:t.legacyasunmapped_column/drop_column/destructive;code: 'NOT_IMPLEMENTED',status: 501and the operator-facing first sentence, and sends zero statements to the database.pnpm --filter @objectstack/driver-turso test: 58 files, 1317 tests passed.pnpm --filter @objectstack/driver-turso typecheck: exit 0.tsc --listFilescompiles all 58 test files, including the new one.Ablation, on HEAD
7d9a7e38c7, throughscripts/ablation-replace.mjs:if (this.isRemote) refuseRemoteDriftDetection();goes from x1 to x0, the marker from x0 to x1, and the blob from4f2aabfcd133to87fe978db7c9.expected a refusal, got an answer: [], which is the original defect. The 4 controls stayed green.4f2aabfcd133), andgit diff HEADis empty../turso-driver.jsfrom source.Gates, on HEAD
7d9a7e38c7node scripts/pm/dispatch-gates.mjs --commands(no paths) derived 61 commands. Every one exited 0.--ranverdict:✓ dispatch-gates --ran: 61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED.check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt) first exited 3 (PREREQUISITE NOT MET). They exited 0 after a workspaceturbo run build.check:object-def-param-keyswent red once, on arm C (the override derived the parameter type). It is corrected in6d1e4e9619and green on HEAD.node scripts/check-issue-citations.mjs(live): exit 0,every citation this change adds resolves.pnpm check:driver-conformance: exit 0,OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.**/*.{ts,…}block ofeslint.config.mjs, run with--no-inline-config --format json.parserOptions.projectandprojectServiceare both unset for these files), so this diff cannot move any untouched file's verdict. The changeset.mdis outside eslint's population.origin/maingained 3 commits after the branch point (metadata, objectql,scripts/pm/close-cards.mjs). They are disjoint from this diff, and the derived list came out identical.Acceptance notes
run 'os migrate plan' to verify. Pointed at the remote URL, that command refuses, because it arms deferred DDL. Its refusal names the local-copy route, and the driver message embedded earlier in the same warning names that route first. Owner:domain:cli(packages/cli/src/utils/artifact-boot-migration.ts). Noted, not filed.applyMigrationEntriesstill runs the inherited Knex path on the placeholder. No caller in this repo reaches it: the gate gets no entries from a remote detector, andos migrate applyis refused atsetDeferredDdl. There is no repro, so this is noted only.Out-of-scope findings, for the seat to file
planMediaColumnMove()answers{ plans: [], refusals: [] }.mwithdoc: fileandpic: imagewas synced through the batch door, and its physicalTEXTcolumns are present. The remote face returned 0 plans and 0 refusals. The local control returned 2unquoteplans.managedObjectFields.os migrate files-to-referencesboots without deferral and callsstack.driver.planMediaColumnMove().turso remote planMediaColumnMove empty·files-to-references remote turso nothing to move·remote placeholder knex inherited schema read.urlplussyncUrlruns every Knex CRUD against a process-local:memory:database.detectModeanswersreplicafor this pair, and the last branch oftoKnexConfiggives it a:memory:connection.turso replica remote url syncUrl memory·embedded replica knex memory writes lost·turso replica mode libsql url syncUrl.Generated by Claude Code