Repository navigation
feat(driver-turso): the remote transport issues auto_number values from the shared persistent sequence (#21113) - #21160
Conversation
…e shared persistent sequence Disposition A of the remote-autonumber question, executed: a create, bulkCreate or upsert on the Turso REMOTE transport that leaves an auto_number slot empty now gets a generated value, atomically in the database, from the same `_objectstack_sequences` counter the local and embedded-replica faces draw from. - sql-driver: the rules a second face must share are lifted into protected members (`resolveSequenceTenantId`, `defineSequencesTable`, `maxAutonumberCounter`, `escapeLikePrefix`, the table name and retry budget). No behaviour change on the local faces. - turso-driver: `getNextSequenceValue` routes by transport; the remote face moves the counter in one statement (`UPDATE … RETURNING`, cold path `INSERT … ON CONFLICT DO UPDATE … RETURNING`), bootstraps from the data table by the shared reading, and carries the #5495 re-seed. A legacy (pre-key_hash) table is refused loudly, not keyed by a second rule. - remote-transport: `upsert` takes insert-only columns so a merge keeps the number already in the row. - The refusal suite is converted to the generation suite; the two resync boundary pins are rewritten to pin generation; a cross-process pin runs two writers in two processes against one database file. Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…oncurrency pin reports its measurement The README's remote-mode refusal table loses its "Record numbers" row and gains a section on how remote mode issues them. The changeset declares the widening for @objectstack/driver-turso (minor) and the lifted protected members for @objectstack/driver-sql (patch). The cross-process pin asks for two writer changes in the global order rather than a non-contiguous run per writer, which the scheduler does not guarantee, and prints what it measured. Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…blation in the concurrency pin Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…rso-remote-autonumber
…tonumber suites `bypassTenantAudit` is a declared DriverOptions key and the read-back order is a DriverQuery, so neither needs an `as any`; the query-options-erasure ratchet counted the one on the concurrency pin's find(). Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…te it in the working tree and a blanket add carried it into the previous commit Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…rso-remote-autonumber
… un-overlapped run NOT MEASURED Whether two processes overlap is the scheduler's decision: under another seat's full-package run one writer wrote all of its numbers before the other was scheduled past the barrier. The hard pins (distinct, complete, monotonic, no failed write) hold on every round; the overlap evidence ends the loop on the first round that shows it, and a run with none is skipped with a note rather than failed or passed. Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 14 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 036addac90cebbce841dcd1c32c9e531c3be3620 && git checkout 036addac90cebbce841dcd1c32c9e531c3be3620
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fde553c5091f9b7ac70b18c624ed7cd5df7c5f3a 5d09309a65a19d50631d0a349e0e002b1129d6b8 && git checkout -B drift-repro fde553c5091f9b7ac70b18c624ed7cd5df7c5f3a && git merge --no-ff 5d09309a65a19d50631d0a349e0e002b1129d6b8
node scripts/docs-audit/affected-docs.mjs --json fde553c5091f9b7ac70b18c624ed7cd5df7c5f3a
|
Contract reviewServed-tier: Inputs: card #21113 (body, the claim comment, the dev report 5929937746), PR #21160 (body, the 14-file list, the net diff against the merge-base ① Derived judgmentsAccept-set, driver-turso remote face. A Atomicity (Acceptance 2). Warm path: What the concurrency pin proves, and what it does not. Two One semantics, shared not copied. Format and precedence: the inherited The converted test (Acceptance 3). The refusal suite's 22 cases map onto the generation suite's 27. Every refusal became its generation counterpart (create, bulkCreate, the mixed batch, the no-id upsert, the upsert and bulkCreate. Legacy pre- Public surface. AGENTS.md. Absent from the net diff: 14 paths, none is AGENTS.md, and the blob is tsx. Check-runs on the head (32). Success: Build Core, Build Docs, Type Check source gates, Type Check debt ledger, Temporal Conformance (live PG + MySQL), Dogfood Regression Gate 2/3 and 3/3, Dogfood Verify CLI, Check Changeset, Validate Package Dependencies, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the four claim and path guards, Auto Label, filter. Skipped: Console Pin Gate, Packed-tarball smoke (opt-in). Still ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL One blocking item, ②: Generated by Claude Code |
…rso-remote-autonumber
…; the overrides read it off the protected members Review 5930100091: a root type re-export levels driver-sql at minor, and patch was declared. The re-export is dropped and the two re-seed overrides on the remote face take their parameter types through Parameters indexed over SqlDriver's protected members, so driver-sql's export list is unchanged and patch stands. Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Round-1 delta review of FAIL 5930100091 @ What moved since ① Derived judgmentsThe four questions of this round.
Every ① judgment of 5930100091, re-confirmed on this head. The hunks they were made on are unchanged by patch-id, so each holds unless the fix touched it:
Check-runs on the head (39 entries, read once at 11:22:44Z). Success (19): Type Check source gates, Build Docs, Check Changeset (both runs), Validate Package Dependencies, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Flag docs affected by code changes, The card this PR closes must claim this branch (both), No other open PR may claim the same issue (both), No other open PR may claim the same single-writer path (both), Part-of PR must not also close its card (both), Auto Label, filter. Skipped (4): Console Pin Gate, Packed-tarball smoke (opt-in), and one Auto Label and one Check PR Size duplicate. Still ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS The one blocking item of 5930100091 is closed by dropping the root export and typing the two overrides through the indexed Generated by Claude Code |
… better-auth family (objectstack-ai#21094) (objectstack-ai#21162) Fixes objectstack-ai#21094 Clause-②: no Takes the 2026-10 production-dependency group that Dependabot opened as objectstack-ai#21029 (closed in favour of this card), without the better-auth family and without `next`. Maintainer ruling, verbatim: > 按你的推荐:关掉21029立卡,better-auth先放着,21024授权你落地 ## What lands - **43 manifests, 64 range moves.** The 63 range moves objectstack-ai#21029 made (diffed from its merge base `9b0de7de73` to its head `c0b7dd923e`) were re-applied one line at a time onto current `main`. The 64th is the `tsx` devDependency objectstack-ai#21160 added to `packages/drivers/driver-turso/package.json`, lifted from `^4.23.12` to `^4.23.15` after objectstack-ai#21160 was merged in. Each move matched the exact old spelling or the script refused. Excluded: `apps/docs/package.json` (`next` belongs to objectstack-ai#21055 / objectstack-ai#21083) and the five better-auth family lines in `packages/plugins/plugin-auth/package.json`, which stay at `1.7.3`. `pnpm-workspace.yaml` is not touched. Check, taken before the objectstack-ai#21160 merge: the sorted `-`/`+` lines of this branch's manifest diff equal objectstack-ai#21029's lines minus those exclusions (`diff` exit 0, 126 lines). - **`pnpm-lock.yaml`**, regenerated with the tooling in three steps (pnpm 10.31.0, the `packageManager` pin), never by hand: 1. `pnpm install --lockfile-only`, starting from main's lockfile. 2. `pnpm --filter @objectstack/plugin-hono-server --filter @objectstack/plugin-auth --filter @objectstack/hono update --lockfile-only --no-save hono`. Step 1 left `hono` at 4.13.7, below the `^4.13.9` that `plugin-hono-server` now publishes. The workspace override for `hono` (selector below 5.0.0) rewrites every declaration to `^4.13.5`, which 4.13.7 still satisfies, so lockfile inertia kept the old version. CI would have certified 4.13.7 while a downstream install gets 4.13.12. That is the declared-versus-tested split the card names for better-auth. Step 2 moves the single `hono` copy to 4.13.12 without editing the override or any manifest. An unfiltered `pnpm update -r` was tried and discarded: it also dropped `knex`'s `mysql2` / `pg` / `tedious` peer links in two importers. 3. After objectstack-ai#21160 landed: merge `main` (`e35c40a52`), lift driver-turso's new `tsx` line, and run `pnpm install --lockfile-only` again. - The result is a fixed point: a second `pnpm install --lockfile-only` leaves it byte-identical, and `pnpm install --frozen-lockfile` passes. Lockfile blob `30ba2147`. The resolved set is unchanged from the set the OSV scan below covered: 0 names differ. - **`packages/drivers/driver-turso/src/**`**: the item-4 restamp, 21 lines in 7 files (below). - **`packages/spec/src/data/driver/common.zod.ts` and `driver-credential-refusal.test.ts`**: the `@libsql/core` version label is restamped from 0.17.4 to 0.18.0. The TSDoc of `CREDENTIAL_URL_QUERY_PARAMS` says it was measured "in the versions pinned by this tree", so leaving 0.17.4 would have made it false. The behaviour was re-measured identical on the installed 0.18.0: `?authToken=` overrides the config token, `auth%54oken` is decoded, `AuthToken` and `token` give `URL_PARAM_NOT_SUPPORTED`, and the control without a query keeps the config token. - **`.changeset/21094-prod-deps-group.md`**: `patch` for the 19 published packages whose `dependencies` range moves. The list was re-derived from this diff and matches the PM correction on the card, `plugin-auth` included (`@noble/hashes`, `jose`). ## Resolved versions against the merge base Every changed `name@version` pair in the `packages:` section, compared with `main` at `e35c40a52`: 25 names change. **DOWN: 0.** | package | merge base | this branch | |:--|:--|:--| | `@libsql/client` / `@libsql/core` | 0.17.4 | 0.18.0 | | `zod` | 4.6.1 | 4.6.5 (4.6.1 kept by `apps/docs` only) | | `@modelcontextprotocol/sdk` | 1.30.0 | 1.31.0 | | `hono` | 4.13.7 | 4.13.12 | | `mongodb` | 7.5.0 | 7.7.0 (7.5.0 kept by `mongodb-memory-server-core` only) | | `jose` | 6.2.7, 6.2.8 | 6.2.12 | | `@noble/hashes` | 2.3.0 | 2.4.0 | | `@noble/ciphers` | 2.3.0 | 2.4.0 (2.3.0 kept by better-auth 1.7.3 only) | | `react` / `react-dom` / `@types/react` / `@types/react-dom` | 19.2.x | 19.3.0 (19.2.x kept by `apps/docs` only) | | `tsx` | 4.23.12 | 4.23.15 | | `yaml` | 2.9.0 | 2.9.1 (2.9.0 kept by the `apps/docs` fumadocs tree only) | | `chalk` | 6.0.0 | 6.0.1 | | `sql.js` | 1.14.1 | 1.14.2 | | `pinyin-pro` | 3.29.1 | 3.29.4 | | deduped onto a newer copy already present | `@hono/node-server` 2.0.12, `ws` 8.21.1, `@types/ws` 8.18.1, `eventsource-parser` 3.1.0 | removed (2.1.1, 8.22.0, 8.18.2, 3.1.1 stay) | | new transitive copies beside the old ones | none | `bson` 7.3.3 and `@mongodb-js/saslprep` 1.5.5 (with `mongodb` 7.7.0), `scheduler` 0.28.0 (with `react-dom` 19.3.0) | `nodemailer` stays at **10.0.13**, main's version and at least the required 10.0.12. Dependabot's regeneration had moved it down to 10.0.11. ## Item 4: `@libsql/client` lifted to `^0.18.0`, because the premise holds The premise was measured before any driver edit, three ways. 1. **Package diff.** `npm pack` of both releases. `@libsql/core` 0.17.4 and 0.18.0 differ only in `package.json` (the version). `@libsql/client` differs only in `lib-esm/sqlite3.js`, `lib-cjs/sqlite3.js`, `lib-esm/sqlite3.d.ts` and `package.json`. The change is a connection pool for the local `file:` client. `http.js`, `ws.js` and `node.js` are byte-identical. Installed side by side, `@libsql/hrana-client` 0.10.0 and native `libsql` 0.5.29 (with `@libsql/linux-x64-gnu`) are byte-identical too. 2. **Executed probe, same script against both installs.** Output is identical except for the version strings and one count: `syncUrl` occurrences in `sqlite3.js` go from 3 to 4 (the new pool-size line). 3. **The driver's own suite.** At base with 0.17.4: 81 files, 2210 passed, 33 skipped. With 0.18.0, before the restamp: 2209 passed, 1 failed, 33 skipped. Per-test outcomes are identical except the version pin (`expected '0.18.0' to be '0.17.4'`). After the restamp: 2210 passed, 33 skipped. At `0f87e8488`, with objectstack-ai#21160's tests merged in: 2218 passed, 33 skipped, 0 failed. | site (at base) | claim | 0.18.0 reading | verdict | |:--|:--|:--|:--| | `turso-authtoken-url-channel.test.ts:17-18` | client / core / native versions, range | client 0.18.0, core 0.18.0, native `libsql` 0.5.29; range now `^0.18.0`. Answers 1-4 (live wire, child-process replica endpoint) pass, 7/7 non-pin cases, as 8/8 did on 0.17.4 | held, restamped | | `turso-authtoken-url-channel.test.ts:281` | version pin | `0.18.0` | pin moved | | `turso-driver-remote-url-replica-refusal.test.ts:28`, `turso-driver.ts:1288`, `:1384` (message) | no embedded replica for a remote url | `syncUrl` lines: `http.js` 0, `ws.js` 0, control `authToken` lines 6 / 6. `https` and `ws` clients' `sync()` reject `SYNC_NOT_SUPPORTED`. `:memory:` + `syncUrl` throws `URL_INVALID` ("Embedded replica must use file for local db"). File 27/27 on both versions | held | | `turso-driver-timeout.test.ts:9`, `turso-driver.ts:152` | HTTP arm rides `Config.fetch`; replica `sync()` is native | `http.js` (one `config.fetch` site) and hrana-client byte-identical. Timeout file 5/5, supplied-client refusal file 13/13 on both | held | | `turso-driver.ts:871` | `Config.timeout` is the busy timeout; "remote clients ignore it" | core `api.d.ts` docblock byte-identical | held | | `turso-driver-unrecognised-url-refusal.test.ts:26`, `turso-driver.ts:1313`, `:1417` (message) | refusals of urls the client rejects | `URL_INVALID` for `./data/app.db`, `data/app.db`, `/abs/app.db`, `:MEMORY:`, empty, `libsql:host` (bare paths "not in a valid format"). `URL_SCHEME_NOT_SUPPORTED` for `sqlite:`, `memory://`, `C:\data\app.db`. File 42/42 on both | held | | `turso-driver-uppercase-ws-scheme-timeout-refusal.test.ts:15`, `:26`, `turso-driver.ts:932` | `expandConfig` lowercases the scheme before the switch | `WSS://…` gives `wss`, `Ws://…` gives `ws`, control `LIBSQL://…` gives `https`. `_createClient(expandConfig(config, true))` present (1 hit). File 20/20 on both | held | | `turso-driver-ws-timeout-refusal.test.ts:10`, `turso-driver.ts:963`, `:1001` (message) | the WS client takes no `fetch` and no timeout | `ws.js`: `fetch` 0, `timeout` 0. hrana `ws/*.js` + `index.js` `timeout` 0, control `fetch` over `http/*.js` 15. File 11/11 on both | held | | `turso-driver.ts:1061` (message) | a built client's transport cannot be re-seamed | `config.fetch` is read once, inside `_createClient` in `http.js` (byte-identical) | held | | `turso-driver.ts:1212` | the client folds scheme case and opens each spelling | `FILE:./x.db` gives `file`, `Wss://` gives `wss`, `LIBSQL://` gives `https`. `createClient` opens each (`http` / `ws` / `file`) | held | | `turso-driver.ts:1244` | `:memory:` expands to `file::memory:`; `isInMemoryConfig` | `file::memory:`. `true` for `file::memory:` and `file::memory:?cache=shared`, control `false` for `file:./x.db` | held | After the edit, `git grep -n -E "0\.17\.[0-9]" -- packages/drivers/driver-turso/src` returns 0 lines (exit 1). The control is the 23 `0.18.0` occurrences in the same 7 files. ## What objectstack-ai#21029 never measured - **Test Core shard 4's eight unreached packages**, measured at `cf1d700b`, before the `main` merge: `rest` 250 files, 4728 passed / 248 skipped. `driver-sql` 205 files, 3303 passed / 188 skipped. `plugin-email` 31 files, 510 passed. `plugin-approvals` 52 files, 804 passed. `plugin-webhooks` 13 files, 160 passed. `trigger-schedule` 8 files, 170 passed. `connector-mcp` 3 files, 23 passed. `client-react` 3 files, 34 passed. Turbo ran 45 of 45 tasks, exit 0. - **`Lint & Repo Gates` from `check:vendor-export-contract` on**: `pnpm check:vendor-export-contract` reads `VERDICT: PASS — vendor export contract (installed workspace), 1 edge(s) verified` (better-auth 1.7.3), and the `-resolve` variant passes on the registry. The 72 later steps of that job were not run here. They belong to CI's run on this PR. The families this diff derives are below. - **OSV.** `osv-scanner` v2.3.8, built from the Go module proxy because this container's egress refuses `api.osv.dev`. It ran on the offline npm database over lockfile blob `70547c67` (its resolved set is identical to the current `30ba2147`), with the repo's `osv-scanner.toml` loaded: 1388 packages, `No issues found`, exit 0. Positive control: the same lockfile with `hono` rewritten to 4.12.32 gives exit 1 and 8 advisories on `hono`, among them GHSA-8j4g-w8fx-2239. CI's online step is the authoritative reading. - **`zod` 4.6.5 and `z.properties()`**: `git grep -n -E "z\.properties\(" -- packages/` returns 0 lines; control `z.object(`, 1825 lines. - **`mongodb` live suites**: NOT MEASURED. They need a `mongod` download from `fastdl.mongodb.org`, which this container's egress refuses (CONNECT 403). The default `driver-mongodb` suite passes: 30 files, 675 passed, 172 skipped (the five opt-in live files). ## Gates and tests Gate families derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths, change set from git) at HEAD **`0f87e8488`** (54 paths against merge base `e35c40a52`): 117 commands. All 117 exit 0, and so does `pnpm check:vendor-export-contract`. Exit codes were captured before any pipe. `--ran` reconciliation: `117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN`, a derived zero with an exit code on every line. Package suites, all exit 0: - At `cf1d700b`: `driver-mongodb`, `mcp` 32/344, `plugin-hono-server` 27/324, `plugin-auth` 115/2472, `service-settings` 33/584, `plugin-pinyin-search` 2/21, `driver-sqlite-wasm` 36/675, `driver-memory` 69/1613, `service-analytics` 155/3526 (10 skipped), `create-objectstack` 16/247, `@objectstack/hono` 5/122. Also `@objectstack/cli` `--project unit` 242/3432 and `@objectstack/spec` `--project local` 591 files / 17368 passed. - `typecheck`: the 19 moving packages plus `client-react`, `@objectstack/hono` and `lint`. Turbo ran 80 of 80 tasks. - At `0f87e8488`, after the objectstack-ai#21160 merge and a full rebuild: the `driver-turso` suite (82 files) gives 2218 passed / 33 skipped / 0 failed. That includes objectstack-ai#21160's new tests, run on 0.18.0; its two-process concurrency test overlapped, with 400 writes and 400 distinct numbers. `typecheck` passes, and `pnpm install --frozen-lockfile` passes. ## Acceptance notes - **Duplicate copies left by the exclusions**: `zod` 4.6.1, `react` 19.2.8 and `yaml` 2.9.0 are kept only by the excluded `apps/docs` tree. `@noble/ciphers` 2.3.0 is kept only by the excluded better-auth 1.7.3. `mongodb` 7.5.0 is kept only by the test harness `mongodb-memory-server-core` 11.3.0. - **libsql 0.18.0's one behaviour change, from reading the code (not measured):** the local client now pools connections, with one connection for `:memory:` and for embedded replicas. On a replica, a second `sync()` therefore waits for the first to release the connection. On 0.17.4 the two ran at once on the same native handle. With `timeout` set, a sync that outlives the window keeps running natively, uncancelled, so the next periodic sync queues behind it. No driver docblock claims either behaviour. - **The remaining 0.17.4 stamps stay**: outside `CHANGELOG.md`, only `service-package` `index.ts:192` and `mysql2-tuple.test.ts:172` name 0.17.4 ("Measured on `@libsql/client` 0.17.4"). That is a dated attestation and stays true; the `result.rows` shape was re-measured identical on 0.18.0. - **Environment side effect, nothing committed**: turbo 2.11.5 appends a managed block to `AGENTS.md` whenever an agent runs a repository-scoped turbo command. It was restored after each turbo run with `git restore --source=HEAD --staged --worktree AGENTS.md`. No commit on this branch touches `AGENTS.md`. - **Merge-queue ejection and its fix**: objectstack-ai#21160 added driver-turso's `tsx` at 4.23.12 while this branch removes `tsx@4.23.12`. The two lockfiles merged into one that `pnpm install --frozen-lockfile` refuses. Fixed by merging `main` at `e35c40a52`, lifting that `tsx` line, and regenerating the lockfile with pnpm. --- _Generated by [Claude Code](https://claude.ai/code/session_018gA1pE6eJtwHhqx72G8U9X)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21113
Clause-②: yes (widening)
Patch round 1 (review 5930100091)
The one blocking item was
@objectstack/driver-sqlatpatchbeside a roottypere-export (AutoNumberReservation). Branch taken: the re-export is dropped and the two re-seed overrides inturso-driver.tsare typedParametersindexed overSqlDriver'sautoNumberValueExists(its second parameter) andresyncSequenceToDataMax(its first) — the premise (the protected member's parameter type is reachable from the subclass without the export) measured true:driver-sqltypecheck exit 0,driver-tursotypecheck exit 0, no structural copy.driver-sql's export list is byte-identical tomain, sopatchstands; the changeset sentence that named the export now reads "No export is added".origin/mainmerged again (fde553c50, which also lands theagentGuidance: falseopt-out of #21151); re-run on the merged tree: closure build exit 0, both typechecks exit 0,driver-turso82 passed (82)files /2218 passed | 33 skipped(83 files / 2219 with the throwaway probe present),driver-sqlautonumber suites66 passed | 2 skipped, and the changeset, ADR-0087, nul-bytes and published-files gates all exit 0. Finding (b) was measured on the remote face as the review asked and reproduces; it is in the round-1 report on #21113 for the seat to file.What this does
Disposition A of the remote-autonumber question, executed: a
create,bulkCreateorupserton the Turso REMOTE transport that leaves anauto_numberslot empty now gets a generated value, issued atomically in the database from the same persistent_objectstack_sequencescounter the local and embedded-replica faces draw from.supports.autonumberstaystrueon the remote face and is now honoured. TheNOT_IMPLEMENTED/ 501 refusal is gone; its suite is converted into the generation suite.Why now: the appetite door the refusal left shut (「for want of measured demand」) was met by objectstack-ai/cloud#2531 —
POST /api/v1/data/crm_accountanswers501on a hosted tenant, whose database is on this transport, so no object declaring anauto_numberfield could get a new record on the hosted product. That card carries the cloud half,Blocked-by:this one.How, and what is shared rather than copied
Where the slots are filled. On the driver, one layer above the statement builder, exactly where the refusal was raised:
RemoteTransport.create(object, data)takes no schema and caches none (pinned), whileTursoDriverholdsautoNumberFieldsfrom remote schema sync. The remotecreate/upsert/bulkCreatecall the inheritedSqlDriver.fillAutoNumberFieldson a copy of the caller's row, then hand the filled row to the transport, which builds the sameINSERTit built for a caller-supplied number.One semantics (H3). The rules that decide WHICH counter a value is drawn from and WHERE a cold counter starts are
SqlDriver's and are called, never copied. Lifted intoprotectedmembers in the sequence region ofsql-driver.ts(:7532–:8190on this head; nothing else in that file):resolveSequenceTenantId(replaces three inline copies),defineSequencesTable(the table's one definition — the remote face compiles it to text with the connection-less Knex it already holds and sends that),maxAutonumberCounter+escapeLikePrefix(the bootstrap reading, suffix included, split from the Knex statement that fetches the values),sequencesTableName,autoNumberCollisionRetries. The two re-seed overrides take their parameter types throughParametersindexed overSqlDriver's protected members, sodriver-sql's export list is unchanged. The format rendering and precedence were already shared (resolveAutonumberFormatat registration,renderAutonumber/missingFieldValuesinfillAutoNumberFields), and are reached by calling that method.What is this face's own: how the counter moves (H2).
TursoDriver.getNextSequenceValueroutes by transport. Local and replica keep the inherited Knex transaction. Remote moves the counter in ONE statement:Each is a single SQLite statement serialised by the database's write lock across connections and processes; no in-process state takes part (
remoteSequencesTableEnsuredremembers only that the TABLE exists). Two cold writers both scan and both INSERT; one wins the row, the other'sON CONFLICTarm increments it.H2 falsified on one point, reported rather than followed: the PM expected the seed folded into the statement as a SQL
MAX(…). It is not, because the bootstrap reading strips a declared suffix and reads the digit run after the prefix (readAutonumberCounter, #6468), which a dialect expression would be a second copy of — the exact collision H3 forbids. A Hrana batch was not needed either: the JS reading has to sit between the scan and the insert, and both insert arms are atomic on their own.The legs (H4).
create,bulkCreateandupsertall generate.bulkCreateon the remote face now runs through the driver's owncreateper row, which is whatRemoteTransport.bulkCreatealways did underneath (it loops the transport'screate: one INSERT and one read-back per row, never one statement — so the old comment's 「all-or-nothing on this transport too」 was not true, and the statements sent and the mid-batch failure state are unchanged).upsertreserves before the statement, asSqlDriver.upsertdoes, and names the autonumber columns to the transport as insert-only (RemoteTransport.upsertgains an optional fifth argument, schema-free: WHICH columns, not WHY), so a merge keeps the number already in the row (#7011) and the insert leg — the one #7099 recorded as writing NULL — gets its number. The reservation a merge does not use is a gap, never a renumbering, the local face's rule byte for byte (pinned: seeded 42, merge, next create is 44).#7099's post-write warning is removed with the leg it reported.Legacy shape and collision re-seed (H5), decided from the code. The pre-
key_hashtable:SqlDriver.ensureSequencesKeyHashShaperebuilds it through a live Knex connection, which this face has none of, and a raw-SQL rewrite would be a second copy of a migration; keying by the legacy(object, tenant_id, field)rule would be a second keying rule beside the shared one. So it is refused,DATABASE_ERROR/ 500 with the remedy in the message (open the database once through the local or replica face), not cached as a verdict, caller-supplied numbers still written. It cannot arise on a database this face created. The #5495 re-seed is needed on this face — the bypass paths that create a stale counter (isSystemseed replay,preserveAuditimport) are exactly the seed/import paths, andbulkCreateis their common door — and it is carried:autoNumberValueExistsandresyncSequenceToDataMaxare overridden for the remote face (the forward-only move is one atomic UPDATE guarded bylast_valuebeing below the observed MAX), andcollidingAutoNumberReservationsis the inherited discriminator, so a duplicate on a value the caller typed stays the caller's 409 (pinned).H6.
Clause-②: yes (widening)holds as measured: a create refused 501 today is accepted; no spec key moves; no export is added (the six lifted members areprotected, and the overrides are typed through indexed types rather than a root re-export);RemoteTransport.upsertgains an optional parameter. Changeset:@objectstack/driver-tursominor,@objectstack/driver-sqlpatch.Tests
Head for every number below:
52ab7ad76(the merge oforigin/main1bd14c984), unless stated.turso-remote-autonumber-refusal.test.ts→turso-remote-autonumber-generation.test.ts(git mvthen rewritten; git's rename detection does not fire at this similarity, so the diff reads D+A — the three-face structure, the controls, theupdate()/ plain-object / caller-supplied cases and the layer pins are the refusal suite's own). 27 cases: generation on every leg incl. the Turso remote:带 id/conflictKeys 但没匹配上的 upsert 仍会静默写入 NULL 自增号(#6944 拒绝闸门覆盖不到的那条腿) #7099 leg; caller-supplied numbers, merge, update, plain object untouched; the two-face block (local Knex face and remote native-libsql face on ONE file: numbers interleave 1,2,3,4 on onekey_hashrow equal to the local face'ssequenceKeyHash; per-tenant buckets; the 自增号格式带「序号槽之后的后缀」时,播种解析读错数字段:引擎读成年份(2026),driver-sql 读成拼接串(12026),两侧还互不一致 #6468 suffix seed read as 5 not 52026; the{field}refusal is the same sentence on both faces and writes nothing); the Autonumber counter neither syncs to MAX(existing) per tenant nor re-checks on collision — warm-DB creates 409 in bursts, each failure burning a number (25 retries observed) #5495 re-seed oncreateandbulkCreate; the caller's own 409; the legacy-shape refusal withcode+status; local and replica still issue;RemoteTransport.prototypestill has no autonumber member andcreate.length === 2;supports.autonumber === true.turso-autonumber-resync.test.ts,turso-autonumber-batch-resync.test.ts): the transport-surface probe verbatim, and the refusal half replaced by generation + re-seed on the remote face.turso-remote-autonumber-concurrency.test.ts: two child processes (tsxover the package source, aspackages/spec's process-boundary pins do;tsxadded as a devDependency ofdriver-turso), each with its own@libsql/clientnativefile:connection to one database file and its own remote-modeTursoDriver, released together by a file barrier; 2 × 200 creates per round → 400 distinct, exactly 1..400, strictly increasing per writer, no failed write, the table and the counter row agreeing. Those are the hard pins, asserted on every round. The overlap EVIDENCE (the writer changes at least twice in the global order) ends the loop on the first round that shows it; after three rounds without one the test is skipped with a note that says NOT MEASURED — because whether two processes overlap is the scheduler's decision, measured here once as a strictly serial run (1 change) under another seat's full-package run on this shared box. Final-head runs: 3/3 green standalone, measuring400 writes · 400 distinctwith 31, 21 and 23 writer changes; inside the full package run, 9. Said plainly: nosqldruns in this container; what is measured is two processes on libSQL's engine under its write lock, not an HTTP server.50 covered cell(s), 0 in the DEBT ledger, 0 exempt; the matrix has no autonumber cell for any driver, so none moved.Runs, all under
scripts/pm/os-verify-lock.shafter the merge:pnpm --filter '@objectstack/driver-turso^...' buildexit 0 ·driver-sqltypecheck exit 0 ·driver-tursotypecheck exit 0 ·driver-tursovitest run:82 passed (82)files,2218 passed | 33 skipped (2251)·driver-sqlvitest runin two halves of 108 files:103 passed | 5 skipped,1389 passed | 100 skipped;102 passed | 6 skipped,1914 passed | 88 skipped(every file; the autonumber-named nine also run on their own:66 passed | 2 skipped).Reverse verification (predicted before each run; mutation and restore proven on disk by
scripts/ablation-replace.mjs, blob == HEAD andgit diff HEADempty after each leg)RemoteTransport.upsertignoresinsertOnlyColumns). Predicted: exactly the two[#7011]merge pins red. Measured:2 failed | 25 passed, both merge pins,expected 'CASE-00043' to be 'CASE-00042'andexpected 'CASE-00099' to be 'CASE-00042'— the renumbering the exclusion exists to prevent.last_value, then UPDATE to+1). Predicted: the distinct count in the concurrency pin drops below N; the stub-backed generation suite stays green. Measured — direction reversed, kept rather than tidied: the generation suite stayed green (27 passed) as predicted, but the concurrency pin went red on its 「no write failed」 assertion, not on the distinct count: the fixture's tenant-scoped unique index refused the second row carrying a duplicated number, and the forward-only re-seed could not outrun a writer that kept reading the same stale value, so writes failed withSQLITE_CONSTRAINT: UNIQUE constraint failed: index 'uniq_crm_case_organization_id_case_number'— 2 of 3 runs with no gap between the two statements, 3 of 3 with a 1 ms gap (the order of one HTTP round trip). On a declared-unique column a counter that hands out a number twice surfaces as refused writes before duplicate rows; recorded in the pin's docblock.driver-sql(maxAutonumberCounter, reached throughdist/: rebuilt,ablation-dist-preflightpresent in 2 built files, exit 0). Predicted: the bootstrap and re-seed pins red on BOTH faces. Measured:10 failed | 25 passedacross the three suites — generation: explicit-then-continues-above, merge-then-44, 自增号格式带「序号槽之后的后缀」时,播种解析读错数字段:引擎读成年份(2026),driver-sql 读成拼接串(12026),两侧还互不一致 #6468 suffix, re-seedcreate, re-seedbulkCreate; resync: LOCAL and REMOTE re-seed; batch-resync: LOCALbulkCreate, LOCALupsert, REMOTE batch. Restore: rebuilt, preflight--absentexit 0. A first attempt of this ablation was a null operation (the tool refused because the replacement restated the anchor; the preflight correctly reported the marker absent and that green run is void) and was redone with a different anchor.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon52ab7ad76(no paths; 73 commands). Run on the final tree (2c9d2985a, every file identical to the commit): the 73 derived commands — 72 exited 0, incl.check:driver-conformance,check:query-options-erasure(holds: 67 unswept non-test sites, none new — my first head had added oneas anyon afind()query in the new concurrency pin, retyped asDriverQuery),check:test-source-alias,check:undeclared-dep-imports,check:doc-authoring,check:nul-bytes,check:lean-entry-closure(measured after building@objectstack/objectql: 15 packages, 201/199 modules, admitted set held),check:type-check-debt(399.5 s re-measure, none above record),check:pm-skill-ratchet(0 with AGENTS.md at the merge-base blob — the turbo 2.11.5 agent-rules block, #21146, had rewritten it once in this worktree and is restored).check:dts-closureexited 1 on its first final-tree run namingplugin-emailandplugin-security(neither in this diff nor in its closure) withdist/index.d.tsabsent at that moment; a forced rebuild of the two and a rerun exits 0 (71 packages, 167/167 declarations) — concurrent turbo activity in the same tree, not this change. NOT MEASURED (1):check:dual-build-cjs-loadsexits 3 PREREQUISITE NOT MET locally (78 packages withoutdist/in this worktree) — CI's.dispatch-gates --ran: 73 derived, 72 run, 1 NOT-MEASURED declared, 0 UNRUN (exit 0). Seven further families (agent-test-spelling,docs-audit-scope,pm-governed-merges,pm-governed-prose,pm-skill-id-lint,pm-skill-ratchet,required-contexts) were derived only while the turbo block sat in AGENTS.md; they were run anyway and all exit 0. The derivation itself still lists what no local run covers: the 53 artifact-roster families, 11 wide-population families, 6 path-scheduled CI jobs and the type-check lanes.pnpm lintnarrowed, as a measurement: ① population = the lint script's owneslint . --no-inline-config, under oneeslint.config.mjs; ②--format jsonon the changed source files: 9 files, 0 errors, 0 warnings; ③ invariance: the config enables no type-aware linting for any file (eslint.config.mjsstates it, noparserOptions.project), so this diff moves no untouched file's verdict.Acceptance notes
LIKEhas no escape character unlessESCAPEis declared, and the sharedescapeLikePrefixwrites backslashes. The remote statement declaresESCAPE '\'; the local Knex builder inscanMaxNumericTaildoes not (knex emitslike ?with the escaped binding and noESCAPE, measured on the sqlite and pg dialects), so on the local SQLite faces a format whose prefix contains_or%seeds from 0 (measured:SO\_%matches nothing on SQLite withoutESCAPE). Pre-existing, outside this card's class; filed in the report as a finding, not fixed here.RemoteTransport.upsertwith callerconflictKeysleavesidin the merge set ("id" = excluded."id") — the drivers(sql): an upsert that merges on a non-PK conflict key silently REWRITES the existing row's primary key — measured on SQLite and MySQL alike #8622 shape the local face closed. Not touched; noted for the lane.content/docs/**carries no statement of the remote refusal (grepped).维护者速读(草稿)
create/bulkCreate/upsert留空的auto_number字段现在会生成编号,编号来自与本地面相同的持久计数表,在数据库里原子递增;原先的 501 拒绝改为生成。auto_number字段的对象都无法新建记录(cloud#2531 实测:HotCRM 建客户答 501)。这正是 TursoDriver remote 面根本不生成自增号:RemoteTransport.create 自建 INSERT,auto_number 只是个 TEXT 列 #6944 当初留下的「等实测需求」的门,现在门开了。Generated by Claude Code