Repository navigation
Commit a0920b4
fix(driver-turso): refuse media column move planning on the remote face instead of answering "nothing to move" (#20014)
Fixes #19894
Clause-②: yes (narrowing)
A remote `TursoDriver` now refuses `planMediaColumnMove()` with the
transport's `NOT_IMPLEMENTED` / 501 envelope instead of answering an
empty scan. `os migrate files-to-references` reports that refusal as a
column step it could not judge, beside its full backfill, verify and
flag report, instead of printing "nothing to move".
## Why `Clause-②: yes (narrowing)` and not the claim's `no`
The claim (comment 5819382636) carries `Clause-②: no`, set for the
driver-only surface. H2 below put the command file in scope, and the
command's `--json` document gains one key, `columnMoveRefused`. This
repo reads a new `--json` key as a clause-② widening:
`.changeset/17080-per-release-spec-changes.md` declares "`Clause-②: yes
(widening)` — … plus one new `os validate --json` key". The driver
refusal is the narrowing. A diff that widens one surface and narrows
another is the `yes (narrowing)` combination
`scripts/pm/clause2-line.mjs` names. Both packages are graded `minor`,
so the level axis and the ADR-0087 gate hold under either reading. The
seat decides which line stands; if it is `no (narrowing)`, the changeset
line changes with it.
## What changed
- `packages/drivers/driver-turso/src/turso-driver.ts`: a
`refuseRemoteMediaColumnMove()` builder, the same shape as the four
sibling remote refusals, and a `planMediaColumnMove()` override. The
remote face refuses. The local and replica faces call `super` unchanged.
The constructor guard, `detectMode` and `toKnexConfig` are untouched.
- `packages/cli/src/commands/migrate/files-to-references.ts`:
`runColumnStep` catches a planner throw whose `code` is
`StandardErrorCode.enum.NOT_IMPLEMENTED`. It returns a stated skip,
`driver_cannot_plan`, carrying `{ error, code }` built by
`errorCodeFields`. Any other throw is re-thrown to the outer `catch`, as
before. The text face prints one `Column step: NOT JUDGED` warning that
carries the driver's message. `--json` gains `columnMoveRefused`, which
is `null` unless the driver refused. The exit code stays the
self-check's, as it already is for the command's other column-step skips
(`runColumnStep`'s own docblock: "a deployment whose driver cannot plan
a column move is not a deployment whose backfill failed").
- Two pins and a changeset (`@objectstack/driver-turso` and
`@objectstack/cli`, both `minor`; BREAKING banner; `adr-0087:
not-required (no-migration-prescription)`).
## Measurements (tree `61609edf8`, this branch's base)
**H1: confirmed.** A table `m` with `doc: file` and `pic: image`, synced
on the transport's SQLite-backed double. On each remote door the
physical columns were `id, created_at, updated_at, title, doc, pic`, all
TEXT.
| face | door | `managedObjectFields` | placeholder `hasTable('m')` |
scan |
|:--|:--|:--|:--|:--|
| local `:memory:` | `initObjects` | `m` | true | 2 `unquote` plans |
| replica | `initObjects` | `m` | true | 2 `unquote` plans |
| remote | `syncSchemasBatch` | empty | false | `{ plans: [], refusals:
[] }` |
| remote | `syncSchema` | empty | false | `{ plans: [], refusals: [] }`
|
| remote | `initObjects` | empty | false | `{ plans: [], refusals: [] }`
|
`mediaFields` read `{ m: [doc, pic] }` on every remote door. The planner
walks `managedObjectFields` and probes through the placeholder, and
either one alone empties the answer.
**H2: the command misreported a driver refusal, so the command file is
in scope.** Measured on the base command with this PR's CLI doubles, a
planner that throws `NOT_IMPLEMENTED`:
- `--apply --yes --json`: exit 1, and stdout was the error alone:
`{"error":"Planning the media column move is not supported by this
transport.","code":"NOT_IMPLEMENTED"}`. There was no backfill, verify or
`flag` key, although `runFilesToReferencesMigration` records the flag
before the column step runs. There was no status either:
`errorCodeFields` forwards `httpStatus`, and the driver stamps `status`.
- `--apply --yes`, text face: exit 1. Only the header, the boot line and
one error line printed: no backfill report and no "deployment flag
recorded" line.
- The pre-fix remote answer, an empty scan: exit 0, and the text face
printed "Column step: nothing to move — this datastore declares no
single-value media column." In `--json`, `columnMove` was `null`.
**H3: confirmed and reused.** `turso-driver.ts` at base has four
`NOT_IMPLEMENTED` / 501 refusal builders: autonumber, transaction,
deferred DDL and drift detection. This one copies their shape. It adds
no new code.
**H4: a remote `TursoDriver` cannot reach the moved state.**
`fileColumnsMoved` is set in exactly two places: the constructor, from
`SqlDriverConfig.fileColumnsMoved`, and `resolveFileColumnsMoved()`.
`toKnexConfig` builds the base config from fixed keys and forwards no
`fileColumnsMoved` on any arm. `resolveFileColumnsMoved()` is called
only from `SqlDriver.initObjects`, which none of the three remote doors
calls. Measured with a stand-in resolver that answers `true`:
`setFileColumnsMovedResolver` returned `true` (taken), and on every
remote door the resolver was asked 0 times. `fileColumnsMoved` stayed
`false`. A remote write of `file_abc` stored `"file_abc"` (the JSON
encoding) and read back as `file_abc`. The
`SqlDriverConfig.fileColumnsMoved` docblock names this posture as the
intended fail-toward: "A host outside that path … simply never resolves
it and stays on the JSON arm, which is again the correct fail-toward."
This PR stops the one in-repo route that could have stamped
`columns_moved_at` through a remote datasource, which is this command's
column step. Whether a remote database can hold that stamp by an
out-of-band route, such as one imported from a moved local file, was not
measured. No finding filed; see Acceptance notes.
**H5: no in-repo doc, skill or example tells an operator to run the
command against a remote Turso database, and none describes
`nothing_to_move`.** Searches at `61609edf8`, over `content/docs`,
`skills`, `docs`, `examples` and `.claude/skills`:
- `files-to-references`: 10 files. Of these, 4 also mention turso or
libsql. In each of the 4 the mention is outside the command's section:
`content/docs/deployment/cli.mdx` in its `os serve` / `os start` flag
tables, and `types.mdx`, `releases/v17/17-0.mdx` and the QA checklist in
unrelated lines. The positive control is
`content/docs/deployment/cli.mdx`, which lists the command (lines 955 to
965).
- `nothing_to_move` or "nothing to move": 0 hits. Positive control: 6
hits in `packages/cli/src`.
- `columnMove`, `columnsMovedAt` or "column step": 1 hit, in ADR-0104
line 1272, which is about boot refusal and not this output. Positive
control: `columnsMovedAt` has 1 hit in `packages/cli/src`.
- turso or libsql on the same line as `os migrate`: 2 hits, both
unrelated (a removed turso config key and the `mongo` driver-id rename).
Positive control: 65 `os migrate` lines in `cli.mdx`.
No doc documents the command's `--json` key set, so the new key needs no
doc edit.
## Verification (HEAD `672da75c7`)
- **driver-turso**: `typecheck` exit 0. It compiles test files: its
first run caught a TS2339 in the new pin, now fixed. `vitest run`: 63
files, 1417 tests passed.
- **New driver pin** `turso-remote-media-column-move-refusal.test.ts`, 9
tests. The controls: `:memory:` and `file:` local faces equal a plain
`SqlDriver` scan of the same declaration, and the replica face plans 2
`unquote` moves. The remote refusal runs on all three doors, with the
media columns asserted on disk. It asserts `code`, `status`, the first
sentence, and that no statement was sent. A last pin checks the encoding
sentence the refusal ends with.
- **Driver ablation** via `scripts/ablation-replace.mjs`, on the guard
line. Anchor 1 to 0, blob `9919ee7a80bd` to `b4b6eb8df1b1`. The 3 remote
cases went red with `expected a refusal, got an answer:
{"dialect":"sqlite","plans":[],"refusals":[]}`, which is the pre-fix
answer. The 6 other cases stayed green. Restore was proven: blob equal
to HEAD, `git diff HEAD` empty. The subject resolves through the
relative `./turso-driver.js` import, so no `dist/` is in the path.
- **CLI**: `typecheck` exit 0 (`tsc --noEmit`, then
`check:test-typecheck` OK with the debt ledger unchanged). The unit
layer ran 224 files, with 3129 tests passed and 29 skipped. 2 files
failed only on a prerequisite, "packages/cli is not built
(./dist/index.js is absent)". After `pnpm --filter @objectstack/cli
build`, those 2 files plus the new pin ran green: 3 files, 34 tests. The
integration layer is declared to CI. The diff touches no
integration-tier file and no spawn entry, and the new pin is unit-tier:
it mocks `bootSchemaStack` through `vi.hoisted` and value-imports no
driver package.
- **New CLI pin** `files-to-references.column-step-refusal.test.ts`, 5
tests, end to end through `MigrateFilesToReferences.run`. It covers the
exit code and `--json` for `--apply --yes --json` and for the dry run,
plus the text face. Controls: an empty scan still reads "nothing to
move" and has `columnMoveRefused: null`. A throw without a capability
code still exits 1 with `{"error":"connection
reset","code":"ECONNRESET"}`.
- **CLI ablation** on the `NOT_IMPLEMENTED` catch line, anchor 1 to 0:
the 3 refusal cases went red (exit 1, the pre-change shape), the 2
controls stayed green, and the restore was proven.
- **Gates**: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `672da75c7` derived 64 commands. All 64
exit 0. `check:dual-build-cjs-loads`, `check:i18n`,
`check:i18n-coverage` and `check:i18n-walk-parity` first exited 3
(PREREQUISITE NOT MET, workspace not built) and were re-run green after
the build. `--ran`: "✓ dispatch-gates --ran: 64 derived famil(ies)
accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded
an exit code and none of them is 3)."
- `check-adr-0087-registration --base origin/main`: "✓
check-adr-0087-registration: 1 declared-breaking changeset(s), each
carrying an ADR-0087 disposition." The changeset is read as
`[BREAKING+clause-②-narrowing]`, `not-required
(no-migration-prescription)`.
- `check-changeset-no-major --base origin/main`: "✓ This diff introduces
no `major` bump." Its level axis reads "NOT APPLICABLE" locally, with no
`pull_request` payload; see the `--event` run below.
- `check:driver-conformance`, before (at `61609edf8`) and after: "OK —
50 covered cell(s), 0 in the DEBT ledger, 0 exempt."
- The diff-scoped `node scripts/check-issue-citations.mjs --base
61609ed`: "✅ check-issue-citations: every citation this change adds
resolves".
- `pnpm check:error-code-casing`: "✓ no unlisted lowercase error codes
in 6509 scanned file(s) (ADR-0112)."
- **Lint** was narrowed to the 4 changed `.ts` files with `eslint
--no-inline-config --format json`: 4 files, 0 errors, 0 warnings. The
population is eslint's own: `--print-config` resolves a config for each
file, and none reports "File ignored". This narrowing cannot move any
untouched file's verdict, because `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`, no typed rules; its own
comment says so). The repo-wide `pnpm lint` is CI's.
- NOT MEASURED locally, and owned by CI: the path-scheduled jobs (Test
Core shards, Temporal Conformance, Dogfood, Build Core) and the
workspace type-check lanes.
## Acceptance notes
- **Why the refusal does not point at the local or replica transport**
(the suggested route asked for that, like the siblings). Moving a local
SQLite copy does not move the remote database. For a remote database
that did hold moved columns, H4 measured that the remote face keeps
writing the JSON encoding. Whether an embedded replica's column step
reaches the primary was not measured. So the message says what the
remote datasource keeps, and names no remedy that was not measured.
- **README left untouched.** It documents none of the four sibling
remote refusals, and adding only this one would be partial. Separately,
line 25 of `packages/drivers/driver-turso/README.md` lists
`RemoteTransport: beginTransaction, commit, rollback`, while the remote
face refuses all three with `NOT_IMPLEMENTED` / 501
(`refuseRemoteTransaction`). That is reported to the seat as a finding
and not changed here.
- **Observation, not filed**: `errorCodeFields` forwards `httpStatus`
and drops `status`, so a driver refusal reaching any CLI `--json` error
envelope carries its `code` without a status. The new
`columnMoveRefused` uses the same helper, which keeps it consistent with
the command's own error envelope.
- **Observation, not filed**: any other throw after the backfill of an
`--apply` run still takes the outer `catch` and replaces the report with
the error. This PR leaves that path as it was. There is no named
reproduction of a non-refusal throw at that point.
- **H4 posture, not filed**: the remote face takes the engine's ADR-0104
resolver and never asks it. That is the documented fail-toward, and
after this PR no in-repo command can stamp a remote database as moved.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent fc6ddb8 commit a0920b4
5 files changed
Lines changed: 590 additions & 2 deletions
File tree
- .changeset
- packages
- cli/src/commands/migrate
- drivers/driver-turso/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
| 19 | + | |
18 | 20 | | |
19 | 21 | | |
20 | 22 | | |
| |||
37 | 39 | | |
38 | 40 | | |
39 | 41 | | |
40 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
41 | 49 | | |
42 | 50 | | |
43 | 51 | | |
44 | 52 | | |
45 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
46 | 60 | | |
47 | 61 | | |
48 | 62 | | |
| |||
308 | 322 | | |
309 | 323 | | |
310 | 324 | | |
| 325 | + | |
311 | 326 | | |
312 | 327 | | |
313 | 328 | | |
| |||
391 | 406 | | |
392 | 407 | | |
393 | 408 | | |
394 | | - | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
395 | 430 | | |
396 | 431 | | |
397 | 432 | | |
| |||
473 | 508 | | |
474 | 509 | | |
475 | 510 | | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
476 | 518 | | |
477 | 519 | | |
478 | 520 | | |
| |||
0 commit comments