Commit 19fc8d6
fix(driver-turso): a declared index the remote face skips for a missing key column is logged at error on the durability sink (#20650)
Fixes #20537
Clause-②: no
The remote Turso face now reports a declared index it skips, because a
key column never materializes, at `error` on its existing durability
sink. It used to report it at `warn` on the diagnostic sink. This is the
remote half of what PR #20519 did for the local face.
## What was wrong (read on `cd901d7a5`)
`RemoteTransport.buildDeclaredIndexDDL`
(`packages/drivers/driver-turso/src/remote-transport.ts`) plans no DDL
for a declared index whose key column is not a stored column. That
covers a misspelt name (`statsu` on a table whose column is `status`)
and a virtual `formula` field. Skipping it is right, because DDL naming
a missing column would fail the whole sync. But the skip was reported
through `this.diagnosticSink`, which `TursoDriver` wires to
`logger.warn`:
[RemoteTransport] skipping declared index "NAME" on "TABLE" — column(s)
not materialized: statsu
For a UNIQUE index, every write keeps succeeding and duplicates are
accepted, and the only trace was a `warn`. The same transport already
has a second sink for this class, `durabilitySink` (wired to
`logger.error`). Its retrofit arm uses it for a unique AND a plain index
it could not create. The local face (`SqlDriver.syncDeclaredIndexes`)
logs this same skip through `logDurabilityFailure` for unique and plain
indexes alike.
## What changed
- **`remote-transport.ts`, `buildDeclaredIndexDDL`.** The
not-materialised arm now calls `this.durabilitySink?.(…)`, not
`this.diagnosticSink?.(…)`. That is triage's direction: no new sink and
no second rule. It writes one line per skipped index per sync, as the
local face does. The line gives the index name, the object and each
missing column, and says whether the index was UNIQUE. It follows the
AGENTS.md "Degradation log levels" shape:
- the consequence: for UNIQUE, "the uniqueness it declares is NOT
enforced: duplicate rows are accepted, and nothing looks broken from the
outside"; for plain, every query the index serves is a full scan while
results stay correct;
- the fix: make every key column a stored field of the object, or remove
the index.
- The doc comments on `durabilitySink` and `buildDeclaredIndexDDL` now
name this arm.
- `turso-driver.ts` is **not** edited. Both sinks were already wired
there.
- No DDL, accept set or refusal changes. The same indexes are created
and the same ones are skipped.
- Changeset: `.changeset/20537-remote-skipped-index-durability.md`,
`@objectstack/driver-turso` **patch**.
## The dispatch's hypotheses, measured
- **H1 held**, with one correction. The skip is in
`buildDeclaredIndexDDL`, the planner, not the sync itself, and all four
sync paths call it: `syncSchema` (new and existing table) and
`syncSchemasBatch` (new and existing table). One edit covers all four,
and the pins drive all four.
- **H2 held.** `turso-driver.ts` has `setDiagnosticSink` going to
`this.logger.warn` and `setDurabilitySink` going to `(this.logger.error
?? this.logger.warn)`, at `:1656` and `:1667` on this base. No edit was
needed.
- **H3 held.** The local text is in `SqlDriver.syncDeclaredIndexes`,
through `logDurabilityFailure`, for unique and plain alike. The remote
line uses the same consequence-then-fix order.
- **H4: the plain index goes on the durability sink too.** There are two
pieces of evidence:
- the local face routes the plain skip through `logDurabilityFailure`,
and its doc says "A plain index is DDL that was supposed to run and did
not, so it takes the same channel";
- the remote retrofit arm already reports a plain index it could not
create on `durabilitySink`, with the #17609 rationale: queries answer
correctly by scanning, nothing looks wrong, and the cost arrives as read
volume.
- **H5: `check:durability-log-level` does not see this site, and no
vocabulary entry belongs there.** Its header says it judges
`try`/`catch` blocks whose `try` calls a declared durability-critical
operation. This arm has no `catch` and runs no operation: it plans no
DDL. It reports through a sink receiver, which `LOGGER_RECEIVERS` does
not cover by a recorded decision. So no `DURABILITY_CRITICAL_CALLEES`
entry can make it visible. The gate reads 38 seams, all loud, on this
head (exit 0).
## Tests
`packages/drivers/driver-turso/src/remote-transport-unbuildable-declared-index.test.ts`
has 18 cases. They use the real `@libsql/client` over `file::memory:`,
as the declared-index parity suite does:
- **The card's matrix (16 cases).** {misspelt `statsu`, `formula`
column} × {UNIQUE, plain}, each over `syncSchema` and
`syncSchemasBatch`, against a new table and an existing one. Each case
asserts:
- exactly one durability-sink line names the index, with the table and
the column quoted;
- `UNIQUE` is present for the unique cells and absent for the plain
ones;
- no diagnostic-sink line names the column or the index;
- the sync resolves;
- the index is absent from `sqlite_master`.
- **End to end (2 cases).** `TursoDriver` in remote mode (`libsql://…`
with a supplied client), through `initObjects`. The line lands on
`logger.error` and never on `logger.warn`.
The prose is not pinned beyond the named subjects and the `UNIQUE` word,
as in the local #20432 suite.
Commands, run on `90e8f132f`:
- `pnpm --filter @objectstack/driver-turso exec vitest run
--maxWorkers=2 src/remote-transport-unbuildable-declared-index.test.ts`:
1 file, **18 passed**.
- `pnpm --filter @objectstack/driver-turso exec vitest run
--maxWorkers=2` (whole package): **79 files passed, 2126 passed, 22
skipped**, verdict `command-exit 0`.
- `pnpm --filter @objectstack/driver-turso typecheck`: `command-exit 0`.
The package `tsconfig` includes `src/**/*`, and `tsc --noEmit
--listFiles` lists the new test file once.
**Reverse verification (ablation).** The fix was committed first. The
mutation went through `scripts/ablation-replace.mjs` in WRAP mode, with
its own trap and restore. It turned the new arm's
`this.durabilitySink?.(` back into `this.diagnosticSink?.(`, and nothing
else.
- **Landed on disk:** anchor count went 1 to 0, and `durabilitySink?.(`
/ `diagnosticSink?.(` counts went 2/3 to 1/4. The blob went
`1c7cb45f2828` to `672bd32cd8c6`.
- **Result:** predicted all 18 red. Observed **18 failed of 18**, on a
comparison (`expected [] to have a length of 1 but got +0`).
- **Restored:** blob equals HEAD (`1c7cb45f2828`), `git diff HEAD` is
empty, and the tree is clean.
- **No `dist` step:** the suite imports the subject by a relative path
(`./remote-transport.js`, `./turso-driver.js`), so vitest reads `src`.
## Gates
- **Derived gates.** Derived after the final commit with `node
scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` at `90e8f132f`: 61 commands. The dispatch-time list had 48;
the 13 added come from the changeset and test-file families. All 61 ran
and exited 0. `--ran` with the exit codes recorded reads: "61 derived,
61 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)".
- **Three gates needed a full build first.**
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt` first exited 3 (`PREREQUISITE NOT MET`, not
measured). After a full build (`turbo run build --filter='./packages/*'
--filter='./packages/*/*'`, 71 of 71 tasks, with the tree clean
afterwards), each exited 0.
- **`pnpm check:driver-conformance`, before and after:** "50 covered
cell(s), 0 in the DEBT ledger, 0 exempt" both times.
- **`pnpm check:durability-log-level`:** not in the derived set; run for
H5. Exit 0.
- **Lint**, narrowed to the two touched `.ts` files with `eslint
--no-inline-config --format json`: 2 files, 0 errors, 0 warnings.
- Both files are matched by the config and neither is ignored.
- `eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`), so this diff cannot move the verdict on any
file it does not touch.
- The full `pnpm lint` run is CI's.
## Acceptance notes
- **The reason for a missing column is not given per column.** The local
line says, per column, why it has no column ("not a field of the object"
or "a formula field"), through `describeMissingIndexColumns`. That
helper is exported from `schema-drift.ts` but not from
`@objectstack/driver-sql`'s package entry. Reusing it would widen that
package's public surface, outside this card's claimed files. A copy here
would be the second copy this file's shared-normalizer imports exist to
prevent. So the remote line names the missing columns and gives both
possible reasons in one clause. Carrier: none.
- **With no durability sink set, the skip is not logged at all.**
Before, it went to the diagnostic sink. `TursoDriver` wires both sinks
together at construction, so no composition in this repo changes. The
retrofit arm already works this way, and the `durabilitySink` doc says
what still surfaces a missing UNIQUE (the enveloped `conflictKeys`
refusal).
- **Remote drift detection is not in this card.** The remote face
refuses it by design, so `os migrate plan` still cannot show the skipped
index on a remote datasource. Only the log line changes here.
- **The branch is two commits behind `main`** (`89801cd96`, `0cb72cfc7`:
service-automation and spec migration text). Neither touches
`packages/drivers`, so no merge was made. CI validates the merge ref.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9b402db commit 19fc8d6
3 files changed
Lines changed: 268 additions & 8 deletions
File tree
- .changeset
- packages/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 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
Lines changed: 210 additions & 0 deletions
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1248 | 1248 | | |
1249 | 1249 | | |
1250 | 1250 | | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
1251 | 1258 | | |
1252 | 1259 | | |
1253 | 1260 | | |
| |||
2425 | 2432 | | |
2426 | 2433 | | |
2427 | 2434 | | |
2428 | | - | |
2429 | | - | |
2430 | | - | |
2431 | | - | |
2432 | | - | |
| 2435 | + | |
| 2436 | + | |
| 2437 | + | |
| 2438 | + | |
| 2439 | + | |
| 2440 | + | |
| 2441 | + | |
2433 | 2442 | | |
2434 | 2443 | | |
2435 | 2444 | | |
| |||
2453 | 2462 | | |
2454 | 2463 | | |
2455 | 2464 | | |
2456 | | - | |
2457 | | - | |
2458 | | - | |
| 2465 | + | |
| 2466 | + | |
| 2467 | + | |
| 2468 | + | |
| 2469 | + | |
| 2470 | + | |
| 2471 | + | |
| 2472 | + | |
| 2473 | + | |
| 2474 | + | |
| 2475 | + | |
| 2476 | + | |
| 2477 | + | |
| 2478 | + | |
| 2479 | + | |
| 2480 | + | |
| 2481 | + | |
| 2482 | + | |
| 2483 | + | |
| 2484 | + | |
2459 | 2485 | | |
2460 | 2486 | | |
2461 | 2487 | | |
| |||
0 commit comments