Repository navigation
Commit 1f89ba0
fix(driver-turso): remote syncSchemasBatch registers read coercion and runs the canonical backfill (#19863)
Fixes #19844
Clause-②: no
## What was wrong
`ObjectQLPlugin.syncRegisteredSchemas` takes its batch branch when a
driver declares `supports.batchSchemaSync` and implements
`syncSchemasBatch`, and `TursoDriver` does both. On the remote
transport, `syncSchemasBatch` returned straight after its DDL. The two
sibling remote doors (`syncSchema`, `initObjects`) go on to call
`registerRemoteFieldMetadata` and then the canonical temporal backfill.
So the door a remote-Turso boot actually takes was the one door that
skipped all three halves: read-coercion registration, the managed-object
record, and the backfill.
## Boot measurement (taken before any edit)
One boot of an `ObjectKernel` with `ObjectQLPlugin`, a remote
`TursoDriver` registered as the `driver.turso` service over the `libsql`
SQLite double (`libsql-sqlite-stub.testkit.ts`), and an app object `w`
with `flag: boolean`, `meta: json`, `at: datetime`. It ran as a scratch
test and is not committed (see Acceptance notes). Before = `origin/main`
8cbc3c0; after = this branch at d8940d7.
| reading | before | after |
|:--|:--|:--|
| schema doors called during the boot | `syncSchemasBatch` twice (phase
1 and phase 3); no `syncSchema`, `initObjects`, `registerExternalObject`
or `registerObjectMetadata` call | `syncSchemasBatch` twice, each
followed by `registerExternalObject` for all six objects in the batch |
| `driver.findOne` flag / meta | `1` (number) / the string `{"k":1}` |
`true` (boolean) / `{"k":1}` (object) |
| engine `findOne` flag / meta | `1` (number) / a string | `true`
(boolean) / an object |
| `paginationTieBreaker('w')` | `null` | `id` |
| `booleanFields.w` / `jsonFields.w` / `datetimeFields.w` | empty /
empty / empty | `flag` / `meta` / `created_at, updated_at, at` |
| `canonicalDatetimeFields.w` | empty | `created_at, updated_at, at` |
So the card stands at p1: nothing else on the boot path populated the
registries.
## The fix
The landing site is the one the card named, the `isRemote` arm of
`TursoDriver.syncSchemasBatch` in
`packages/drivers/driver-turso/src/turso-driver.ts`. The producer is
this driver, so no consumer changes.
- A new private helper, `completeRemoteSchemaSync(objects)`, registers
each object and then runs `backfillRemoteCanonicalTemporalQuietly()`
once for the call. The registration is `registerRemoteFieldMetadata`:
the `remoteManagedObjects` record, plus the coercion, tenant and
autonumber registries `registerExternalObject` fills. An empty list is a
no-op.
- All three remote doors call the helper after their DDL resolves.
`syncSchema` passes one object, keyed by its `object` argument.
`initObjects` passes its objects. `syncSchemasBatch` now passes each
entry as `{ ...schema, name: object }`, the same strict keying
`syncSchema` uses.
- Order: the DDL is awaited first, so a DDL failure rejects before
anything is registered. Registration comes before the backfill because
the backfill reads it to learn which columns are temporal.
- The backfill runs once per batch, not once per object. It probes every
unmarked column in one round-trip, so a steady-state boot costs one
probe per sync call.
- One docblock in the deferred-DDL refusal section said the latter two
doors "also run" the backfill. It now says the batch door runs it too.
Not touched: `packages/objectql/src/plugin.ts`, which was read only.
`detectManagedDrift` in remote mode belongs to #19845. It is not in this
diff, and #19845 remains open.
## Tests
The new file
`packages/drivers/driver-turso/src/turso-remote-batch-door-registration.test.ts`
has 12 cases:
- A `describe.each` over the three remote doors, with `syncSchemasBatch`
(the boot door) as the case under test and `syncSchema` and
`initObjects` as controls. Each syncs the reproduction object `{ fields:
{ flag: boolean, meta: json } }`, creates a row and calls `findOne`.
Expected: `flag === true`, `meta` deep-equals `{ k: 1 }`, and the raw
row on disk is still `{ flag: 1, meta: '{"k":1}' }`, which proves the
test is not vacuous. `paginationTieBreaker('w')` goes from `null` to
`'id'`.
- Write side, on each of the three doors (patch round): a `datetime`
written as `2025-07-28T08:00:00+08:00` reaches disk as
`2025-07-28T00:00:00.000Z`.
- The batch door keys by `object`, never by a `schema.name` that differs
from it.
- The batch door over a pre-existing legacy row:
`backfillRemoteCanonicalTemporal` is called exactly once for a
two-object batch. The naive `2025-07-28 00:00:00` is rewritten on disk
to `2025-07-28T00:00:00.000Z`, and both columns are marked canonical.
- A failing DDL batch rejects with the injected error. No table is
created, and there is no tie-breaker, no boolean registry and no
backfill call.
### Ablation (run once, after the fix was committed)
Mutation: delete only the batch door's `completeRemoteSchemaSync(...)`
call, using `node scripts/ablation-replace.mjs`. The anchor went from 1
hit to 0, the blob changed from b33d398 to 5f1d1c909d7d, and an
on-disk grep count read 0. Command for both runs: `pnpm --filter
@objectstack/driver-turso exec vitest run --maxWorkers=2
src/turso-remote-batch-door-registration.test.ts`.
- Mutant run (re-run on `de31187a96`): exit 1, `Tests 5 failed | 7
passed (12)`. The new write pin reds on the batch door only (`expected [
{ at: '2025-07-28T08:00:00+08:00' } ] to deeply equal [ { at:
'2025-07-28T00:00:00.000Z' } ]`). The other four failures are the
batch-door cases from the first run: `expected 1 to be true`, `expected
null to be 'id'`, `expected undefined to deeply equal [ 'flag' ]`, and
`expected "backfillRemoteCanonicalTemporal" to be called 1 times, but
got 0 times`. The `syncSchema` and `initObjects` controls (including
their write pins) and the DDL-failure case stayed green.
- Restore: the blob matches HEAD (b33d398) and `git diff HEAD` is
empty; the restored file is green in the full-suite run below.
- No `dist/` is involved: the suite imports `./turso-driver.js` from
source.
## Verification (HEAD d8940d7)
**Re-run on `de31187a96` after the patch round:** `pnpm --filter
@objectstack/driver-turso test` exited 0 (57 files, 1311 tests),
`typecheck` exited 0, `node scripts/check-issue-citations.mjs` exited 0,
`node scripts/check-changeset-no-major.mjs --base origin/main` exited 0,
and `pnpm check:driver-conformance` exited 0. The `--commands`
derivation was byte-identical (61 commands), and `--ran` read 59 run and
2 NOT-MEASURED, as below.
- `pnpm --filter @objectstack/driver-turso test`: exit 0, 57 files and
1308 tests passed.
- `pnpm --filter @objectstack/driver-turso typecheck`: exit 0. The
package's tsconfig includes `src/**/*`, so the tests are type-checked
too.
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derived 61
commands. All 61 ran, each exit code captured before any pipe. The
`--ran` verdict exited 0: `61 derived famil(ies) accounted for — 59 run,
2 NOT-MEASURED`.
- NOT MEASURED: `pnpm check:dual-build-cjs-loads` and `pnpm
check:type-check-debt` both exited 3 (PREREQUISITE NOT MET), because
each needs the whole-workspace build. CI runs both over the full build.
Declared narrowing for the first: `require` of the rebuilt
`packages/drivers/driver-turso/dist/index.js` loads (14 exports,
`TursoDriver` a function), exit 0. For the second: driver-turso has no
DEBT or TEST_DEBT entry, and its own `tsc --noEmit` is clean.
- Gates the dispatch named, all green in that run: `pnpm
check:driver-conformance`, `pnpm check:object-def-param-keys`, `pnpm
check:issue-citations` and `pnpm check:nul-bytes`, each exit 0.
- `node scripts/check-issue-citations.mjs`, the live diff-scoped
verdict: exit 0, with 3 citations judged and all 3 resolving.
- Lint, narrowed: `eslint --no-inline-config --format json` over the two
changed `.ts` files reported 2 files, 0 errors, 0 warnings. The
changeset `.md` is outside eslint's configured population ("no matching
configuration"). `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so this diff cannot change the verdict on any
untouched file. The full `pnpm lint` is CI's.
## Changeset
`.changeset/19844-turso-remote-boot-read-coercion.md`,
`@objectstack/driver-turso: patch`. It was rewritten in the patch round
after the first contract review (FAIL on prose, comment 5794937369).
Every claim was re-measured on the SQLite double, including what the
pre-fix door wrote and which of those cells the backfill does and does
not converge.
## Acceptance notes
- The boot measurement is not kept as a test. `@objectstack/objectql` is
not a dependency of `@objectstack/driver-turso`, and adding one (with
its lockfile change) for a single test is outside this card's file
surface. The door-level cases make the same call the boot's batch branch
makes.
- The header table in `turso-remote-deferred-ddl.test.ts` records the
`syncSchemasBatch` row of prediction (b) as "REFUTED, no row write on
this door". It was measured before that refusal existed, so it stays
historically true; the patch round adds a one-clause footnote saying the
door now runs the backfill too.
- A behaviour change for review: an ordinary remote boot now runs the
canonical temporal backfill, which the batch door never ran before. On a
deployment that holds legacy datetime or time text, the first boot after
upgrading rewrites those cells into the canonical spelling of the same
value, as `syncSchema` and `initObjects` already did. The changeset says
so, and it names the two kinds of cells the pre-fix door wrote
unconverted that no remote backfill converges (a `date` stored as a full
timestamp; a scalar `json` stored unencoded).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b940f32 commit 1f89ba0
4 files changed
Lines changed: 270 additions & 25 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
359 | 359 | | |
360 | 360 | | |
361 | 361 | | |
362 | | - | |
363 | | - | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
364 | 365 | | |
365 | 366 | | |
366 | 367 | | |
| |||
1698 | 1699 | | |
1699 | 1700 | | |
1700 | 1701 | | |
1701 | | - | |
1702 | | - | |
1703 | | - | |
| 1702 | + | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
1704 | 1706 | | |
1705 | 1707 | | |
1706 | 1708 | | |
| |||
1711 | 1713 | | |
1712 | 1714 | | |
1713 | 1715 | | |
| 1716 | + | |
| 1717 | + | |
| 1718 | + | |
| 1719 | + | |
| 1720 | + | |
| 1721 | + | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
| 1730 | + | |
| 1731 | + | |
| 1732 | + | |
| 1733 | + | |
| 1734 | + | |
| 1735 | + | |
| 1736 | + | |
| 1737 | + | |
| 1738 | + | |
| 1739 | + | |
| 1740 | + | |
| 1741 | + | |
| 1742 | + | |
| 1743 | + | |
| 1744 | + | |
| 1745 | + | |
| 1746 | + | |
| 1747 | + | |
| 1748 | + | |
1714 | 1749 | | |
1715 | 1750 | | |
1716 | 1751 | | |
| |||
2014 | 2049 | | |
2015 | 2050 | | |
2016 | 2051 | | |
2017 | | - | |
2018 | | - | |
| 2052 | + | |
| 2053 | + | |
2019 | 2054 | | |
2020 | | - | |
2021 | | - | |
2022 | | - | |
2023 | | - | |
2024 | | - | |
| 2055 | + | |
2025 | 2056 | | |
2026 | 2057 | | |
2027 | 2058 | | |
| |||
2064 | 2095 | | |
2065 | 2096 | | |
2066 | 2097 | | |
2067 | | - | |
2068 | | - | |
2069 | | - | |
2070 | | - | |
| 2098 | + | |
| 2099 | + | |
| 2100 | + | |
2071 | 2101 | | |
2072 | | - | |
2073 | | - | |
2074 | | - | |
2075 | | - | |
2076 | | - | |
2077 | | - | |
| 2102 | + | |
2078 | 2103 | | |
2079 | 2104 | | |
2080 | 2105 | | |
| |||
2084 | 2109 | | |
2085 | 2110 | | |
2086 | 2111 | | |
2087 | | - | |
| 2112 | + | |
| 2113 | + | |
| 2114 | + | |
| 2115 | + | |
| 2116 | + | |
2088 | 2117 | | |
2089 | 2118 | | |
2090 | 2119 | | |
2091 | 2120 | | |
2092 | 2121 | | |
2093 | 2122 | | |
2094 | | - | |
| 2123 | + | |
| 2124 | + | |
| 2125 | + | |
| 2126 | + | |
| 2127 | + | |
| 2128 | + | |
| 2129 | + | |
2095 | 2130 | | |
2096 | 2131 | | |
2097 | 2132 | | |
| |||
0 commit comments