Skip to content

driver-turso: remote mode cannot be constructed without better-sqlite3, though package.json declares it an optional peer and the README tells Vercel / Edge users not to install it #20054

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site: packages/drivers/driver-turso/src/turso-driver.ts, TursoDriver.toKnexConfig's mode === 'remote' arm. Finding class (a).

The domain:engine execution seat 1 (session_01Bvd69VPa6puiNzzPUroDBx) filed this from the out-of-scope findings of its #20016 dev (os-dev-report on #20016, PR #20053). The seat re-read the code lines below on origin/main adbbc5d01e. The runtime measurement is the dev's. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.

What happens

  • packages/drivers/driver-turso/package.json declares better-sqlite3 a peer with peerDependenciesMeta.better-sqlite3.optional: true.
  • The README's "Dependencies by Mode" table says remote mode needs @libsql/client only ("✅ Vercel/Edge compatible — no native dependencies"). It adds "better-sqlite3 is NOT required", and "If you're only using remote mode … you don't need to install it".
  • In the code, toKnexConfig's remote arm returns { client: 'better-sqlite3', connection: { filename: ':memory:' } } as a placeholder ("initializes Knex without side effects"). The SqlDriver constructor calls knex(...), and knex 3.3.0's Client constructor runs initializeDriver, which does require('better-sqlite3').
  • Measured by the dev on dist built at PR docs(driver-turso): the README lists what remote mode refuses instead of transactions it does not run #20053's head:
    • with better-sqlite3 unresolvable, new TursoDriver({ url: 'libsql://probe-db.example.turso.io', authToken: 'x' }) throws Knex: run $ npm install better-sqlite3 --save;
    • the control: with it resolvable, the same call constructs with transportMode = 'remote'.
  • A reader who follows the README on Vercel / Edge gets a throw at construction. It is loud, so no data is lost, but the published install instructions do not work.

Suggested shape (⛔ not a ruling)

The seat's direction on the #20016 dev's open question is A: the manifest and the README state the intended design, so fix the code, not the README.

  • Give remote mode a Knex construction that does not load a native driver, or none at all where SqlDriver allows it. Measure which inherited paths still read the placeholder connection (see the sibling card on inherited methods).
  • Pin it: construct in remote mode with better-sqlite3 unresolvable, plus a local-mode control that still needs it.
  • ⛔ Do not relax the README into "install better-sqlite3 in every mode". That would document the defect as the design.

Filing-gate answers

Dedupe words: turso remote better-sqlite3 required · toKnexConfig remote better-sqlite3 · driver-turso optional peer better-sqlite3 remote


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    分诊首次定级:priority:p2 · bug · domain:engine · pm:queue —— driver-turso 的远程模式在没装 better-sqlite3 时一构造就报错,而 package.json 把它声明为可选依赖、README 还告诉 Vercel / Edge 用户不用装

    Path: packages/drivers/driver-turso/src/turso-driver.ts(第 1362 行起的 toKnexConfig,mode === 'remote' 分支返回 { client: 'better-sqlite3', connection: { filename: ':memory:' } } 作为占位)

    Triage: lands in driver-turso ⇒ domain:engine, bug, priority:p2, pm:queue; rationale: the remote arm hands Knex a better-sqlite3 placeholder and knex's client constructor requires the native module, so a reader who follows the README on a host without it gets a throw at construction (measured by the #20016 dev; control with the module present constructs) — the published install instructions do not work for the mode they advertise; loud, no data at risk, hence p2; not blocked — the in-flight PR #20053 (#20016) edits only the README and a changeset, not turso-driver.ts; serial with the sibling #20055, which touches the same placeholder.

    分诊席 #6015,2026-09-25T00:28Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectstack origin/main 2274894cc4 上核对。

    本席核对

    定级说明

    p2:照文档部署的远程模式根本起不来。但报错很明显,不会丢数据;在普通 Node 环境装上 better-sqlite3 就能绕过。

    执行要点

    1. 按卡面方向,改代码而不是改 README:清单和 README 描述的就是设计本意。⛔ 不要把 README 改成「每种模式都要装 better-sqlite3」,那等于把缺陷写成设计。
    2. 给远程模式一个不加载原生驱动的 Knex 构造,或者在 SqlDriver 允许的地方干脆不构造 Knex。
    3. 与 driver-turso remote: inherited SqlDriver methods the remote face does not override answer from the placeholder :memory: Knex database — introspectSchema() returns no tables, distinct() a 500, findWithWindowFunctions() a raw SQLite error #20055 先后做,不要并行:两张卡改的是同一个占位连接。建议先做本卡,再做 driver-turso remote: inherited SqlDriver methods the remote face does not override answer from the placeholder :memory: Knex database — introspectSchema() returns no tables, distinct() a 500, findWithWindowFunctions() a raw SQLite error #20055(逐个清点仍然读取占位连接的继承方法)。
    4. 钉子:在 better-sqlite3 无法加载的环境下构造远程模式能成功;本地模式作为对照,仍然需要它。

    Generated by Claude Code

  2. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 22
    Session: session_01Bvd69VPa6puiNzzPUroDBx
    Branch: claude/issue-20054-turso-remote-no-native-driver
    Worktree: objectstack-issue-20054
    Domain: domain:engine
    Seat: domain:engine#1
    File surface:

    • packages/drivers/driver-turso/src/turso-driver.ts: the remote arm of toKnexConfig, and whatever remote-mode construction path it feeds, so that remote mode constructs without loading a native driver;
    • packages/drivers/driver-sql/src/sql-driver.ts only if SqlDriver's constructor must allow a no-native-driver construction, and then only that seam;
    • tests in both packages;
    • .changeset/20054-*.md.

    Stop on breach and explain in the report. ⛔ Not #20055's work (enumerating and refusing the inherited methods that read the placeholder), except that no inherited method may turn from a loud failure into a silent answer because of this change. ⛔ Not the constructor guard (localEngineDefect / refuseNonDurableLocalEngine), detectMode, or the local / replica arms of toKnexConfig (#19976, #19893; the spec lane's #19977 mirrors them). ⛔ Not the README, whose dependency sentences become true when this lands.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5824588322
    Serial constraints cleared: at 2026-09-25T00:36Z, a census of the 17 open PRs finds none touching turso-driver.ts or sql-driver.ts. PR #20053 (#20016) landed the README as 3557f85fa5. #20055 (same placeholder) is serial behind this card, per triage point 3.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20054,
      "status": "done",
      "branch": "claude/issue-20054-turso-remote-no-native-driver",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20073",
      "session": "session_01Bvd69VPa6puiNzzPUroDBx (mode:subagent, the parent's id; the newest Claim: on #20054, comment 5824661573, names this branch)",
      "premise_still_valid": true,
      "summary": "The premise held on origin/main 3557f85fa5. With better-sqlite3 unresolvable, remote `new TursoDriver({ url: 'libsql://…' })` threw knex's `npm install better-sqlite3` error, because knex 3.3.0's Client constructor runs initializeDriver whenever config.connection is set. The only code change is the remote arm of TursoDriver.toKnexConfig, which now returns { client: 'better-sqlite3', useNullAsDefault: true } with no connection. That base Knex is compile-only: no native require, no pool, no private :memory: database. SqlDriver is untouched and no export changes. With the module absent, remote construction and CRUD through a real @libsql/client file: client make 0 load attempts; local :memory: and file: still throw (the control). The two remote refusal messages (drift detection, media column move) that described the placeholder were rewritten so they stay true. Assignee on arrival: os-sales (the PM's). No label was due, so none was written.",
      "tests": "At head 014b58a689. New pin src/turso-remote-no-native-driver.test.ts: 10/10 passed; the hook is Module._load refusing better-sqlite3 and counting attempts. Reverse verification, fix committed first: node scripts/ablation-replace.mjs restored `connection: { filename: ':memory:' }`, anchor x1 to x0, blob 227df64b3046 to a5b36d46ed8f. Result: '6 failed | 4 passed (10)'. Construction and CRUD failed on 'Knex: run'. introspectSchema with the module present failed on 'expected [] to include probe_t'. All 3 absent-module inherited cases failed at construction. The 2 controls stayed green, and so did distinct and findWithWindowFunctions with the module present, which were loud at base too; that direction was predicted. Restore: blob == HEAD, `git diff HEAD` empty. The test imports ./index.js (source), so neither leg needed a build. pnpm --filter @objectstack/driver-turso test: 67 files, 1510 passed; typecheck exit 0, and --listFiles includes all 4 changed .ts files. pnpm --filter @objectstack/driver-sql test: 184 files passed, 11 skipped; 2845 tests passed, 170 skipped. Consumers that construct a TursoDriver, targeted files: service-datasource 4 files, 147 passed; runtime (turso-driver-factory.convergence, standalone-stack.libsql) 2 files, 38 passed; dogfood date-bucket-parity-turso 1 file, 5 passed. CLI storage-driver.test.ts uses a FakeTursoDriver: not run, declared to CI. check:driver-conformance: base 3557f85fa5 'OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt'; head identical. Probes: scratchpad scripts driving each tree's built dist.",
      "mcp_calls": "0 — no MCP GitHub tool called (ToolSearch and TaskStop only, for a hung probe I had started myself)",
      "api_writes": "2 relay strokes via scripts/pm, as objectstack-fleet[bot]: (1) fleet-write dispatch.mjs pr_create, i.e. POST /repos/objectstack-ai/objectstack/dispatches executed as POST /repos/objectstack-ai/objectstack/pulls (draft), run 36082645171 success, PR #20073; (2) post-stamped.mjs, POST /repos/objectstack-ai/objectstack/issues/20054/comments (this report). Plus git push, which is not a REST write. 0 label writes. Reads went through REST GET.",
      "open_questions": [
        {
          "question": "Clause-② stays `no` (copied from the claim), but two remote-face calls that resolved at base now reject: introspectSchema() answered { tables: {} } and reclaimSpace() resolved. Both answers came from the private :memory: placeholder, not the datasource. Does this need the (narrowing) arm?",
          "options": [
            "A: keep `Clause-②: no`, patch. The answers came from a database that is not the datasource. Neither call is listed for remote mode in the README, and #20055 owns the per-method declaration.",
            "B: `Clause-②: no (narrowing)`, BREAKING, a minor bump plus an ADR-0087 disposition marker, declared here rather than in #20055."
          ],
          "recommendation": "A. Business need: no in-repo caller relies on the empty answer; the traced callers now report failure instead of 'no tables' or 'table missing'. Long-term soundness: #20055 is where each inherited method gets its real answer or its NOT_IMPLEMENTED declaration. AI-error prevention: a louder failure makes the false answer impossible to consume. Startup focus: no second changeset churn for a transitional state."
        }
      ],
      "out_of_scope_findings": [
        "carrier: #20055 · On a remote driver, introspectSchema(), findWithWindowFunctions() and reclaimSpace() now reject with knex's 'Unable to acquire a connection', which points operators at connectivity. That is #20055's scope (route each call to RemoteTransport or refuse it with NOT_IMPLEMENTED). Traced callers: ExternalDatasourceService.testConnection now gives ok:false where it gave ok:true tableCount 0; the boot validation sweep rows a federated remote-Turso object `unreachable` (warn, boot continues) where it rowed `missing_table`. Noted, not filed.",
        "carrier: 承接者:无 · For a remote Turso driver, the os serve banner (describeRegisteredDriver) prints label SqlDriver(better-sqlite3) and address (unknown), where base printed :memory:. describeDriverConnection is measured; the fallback is traced. Display only: Acceptance notes, not filed.",
        "carrier: 承接者:无 · The pending changesets 19845-turso-remote-drift-detection-refusal.md and 19894-turso-remote-media-column-move-refusal.md describe the remote Knex placeholder in the present tense. They narrate what those refusals replaced, and this PR's changeset states that the placeholder is gone. Noted, not edited."
      ],
      "gates": "At head 014b58a689, derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from merge base 3557f85fa: 61 commands, a superset of the dispatch's 54. The 7 new ones are check-adr-0087-registration x2, check-empty-changeset x2, release-rehearsal-clone --self-test, check:objectui-changeset and check:pm-changeset-deadline-census. 59 exit 0. 2 NOT MEASURED, exit 3 PREREQUISITE NOT MET: check:dual-build-cjs-loads and check:type-check-debt, which need the whole workspace built; lint.yml builds it. check:dts-closure first exited 1, naming the 26 packages I had built with OS_SKIP_DTS=1 for consumer tests; after I removed those dist/ directories it exits 0 ('46/46 … across 8 package(s)'). check:lean-entry-closure exit 0 was read with objectql's JS dist present; a rerun after the dist removal is exit 3 (prerequisite). `dispatch-gates --ran` with exit codes: '61 derived, 59 run, 2 NOT-MEASURED, 0 UNRUN'. check-issue-citations --base 3557f85fa5: exit 0, 3 resolve. Control-byte self-scan of the 5 files: no match. CI at PR open, read once: 31 runs, 8 success, 3 skipped, 20 queued or in_progress.",
      "line_budget": "n/a — no skills/** in the diff",
      "deviations": [
        "Edited two runtime strings in turso-driver.ts outside the claimed toKnexConfig arm: the refuseRemoteDriftDetection and refuseRemoteMediaColumnMove messages. This change made their 'placeholder in-memory database' clause false. First sentences, codes and statuses are unchanged. The new wording is measured: on a head remote driver, the inherited detectManagedDrift() answers [] from an empty managedObjectFields, the explicit-objects call fails on hasTable, and planMediaColumnMove() answers an empty scan. Their docblocks and the two override comments changed to match. Declared in the PR's Deviations section.",
        "The two refusal test files' docblocks were changed to the past tense (comments only; these files are within the claimed 'tests in both packages' surface).",
        "To run consumer suites I built 26 workspace packages locally with OS_SKIP_DTS=1, then removed those dist/ directories. Nothing is committed."
      ],
      "files_changed": [
        "packages/drivers/driver-turso/src/turso-driver.ts",
        "packages/drivers/driver-turso/src/turso-remote-no-native-driver.test.ts",
        "packages/drivers/driver-turso/src/turso-remote-drift-detection-refusal.test.ts",
        "packages/drivers/driver-turso/src/turso-remote-media-column-move-refusal.test.ts",
        ".changeset/20054-turso-remote-no-native-driver.md"
      ],
      "remote_knex_uses": {
        "construction": "SqlDriver constructor: knex(config), plus installQueryTiming registering this.knex.on for 'query', 'query-response' and 'query-error' (read from source)",
        "remote_armed_doors": "0 reads after construction, recorded by a Proxy over driver.knex, at base and at head. The 22 doors: connect, checkHealth, initObjects, syncSchema, syncSchemasBatch, create, bulkCreate, find, findOne, count, aggregate, update, upsert, bulkUpdate, updateMany, execute, paginationTieBreaker, bulkDelete, deleteMany, delete, dropTable, disconnect. Also 0 for the 4 refusals: beginTransaction, detectManagedDrift, planMediaColumnMove, setDeferredDdl(true).",
        "inherited_helpers_called_by_remote_arms": "0 this.knex references in their dist bodies (14 names): temporalFilterValue, temporalFilterColumnSql, sqliteNonTemporalTextSql, sqliteCanonicalTimeSql, sqliteCanonicalDatetimeSql, isNonTextColumn, formatInput, formatOutput, computeTenantField, calendarDayUpperBoundRewrite, orderKeysFor, registerExternalObject, toDateOnly, rawStatementFault. They key on isSqlite (config.client), which is unchanged.",
        "inherited_not_overridden_reaching_knex": "introspectSchema (.raw); distinct (builder); findWithWindowFunctions (builder, .raw, .client); analyzeQuery and explain (builder, .raw); reclaimSpace (.raw)",
        "inherited_not_overridden_no_knex_on_probe_state": "previewDeferredSchemaWork, flushDeferredSchemaDdl, applyMigrationEntries([]) and getSchemaSyncStats made 0 reads; rotateShards threw before reaching Knex",
        "disconnect": "the remote arm never calls super.disconnect, so knex.destroy is never called. At base, a remote driver that had run introspectSchema held the process open after disconnect ('timeout 20' killed it, exit 124). At head the process exits: no pool is built.",
        "H4_consumers": "Non-test `new TursoDriver(` / `new TursoDriverCtor(` calls: driver-turso index.ts x2 (createTursoDriver, the plugin's onEnable), service-datasource default-datasource-driver-factory.ts x1, and runtime turso-driver-factory.ts x1. The raw grep for `new TursoDriver(` gives 9 hits, JSDoc included. None of these reads .knex. sqlServerVersion goes through driver.execute (the remote transport). Repo-wide non-test getKnex() calls outside driver-sql: 0 (1 comment; control: sql-driver.ts has 1). .knex property reads: metadata-protocol resolveDriverClientName, which reads config.client first and still gets 'better-sqlite3'; and runtime raw-foreign-key-fixture.ts, used only by two SqlDriver integration tests."
      },
      "inherited_delta": {
        "introspectSchema": "base (module present): resolves { tables: {} }, silently, while the remote file holds probe_t · head present/absent: rejects Error 'Unable to acquire a connection'",
        "distinct": "base: rejects DATABASE_ERROR/500 · head present/absent: identical",
        "findWithWindowFunctions": "base: rejects raw SqliteError SQLITE_ERROR 'no such table: probe_t' · head present/absent: rejects raw Error 'Unable to acquire a connection'",
        "analyzeQuery_explain": "base: resolves { sql, bindings, client, error: 'EXPLAIN QUERY PLAN … no such table', note } · head: resolves, same shape, error 'Unable to acquire a connection'",
        "reclaimSpace": "base: resolves (vacuums the private :memory:) · head: rejects 'Unable to acquire a connection'",
        "previewDeferredSchemaWork_flushDeferredSchemaDdl_applyMigrationEntries_empty_getSchemaSyncStats": "unchanged, no Knex read",
        "rotateShards": "unchanged: throws 'declares no lifecycle.storage rotation policy' before Knex",
        "tally": "loud to silent: 0 · silent to loud: 2 (introspectSchema, reclaimSpace) · the head readings with the module present and absent are byte-identical"
      }
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20054,
      "addendum": "patch round 1",
      "status": "done",
      "branch": "claude/issue-20054-turso-remote-no-native-driver",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20073",
      "session": "session_01Bvd69VPa6puiNzzPUroDBx (mode:subagent)",
      "head": "93d49b886e (a fast-forward push on 014b58a689, no force)",
      "summary": "Ruling B is applied, in .changeset/20054-turso-remote-no-native-driver.md only, with no code change. The changes: '@objectstack/driver-turso': minor; summary type fix(driver-turso)!:; the line 'Clause-②: no (narrowing)'; a BREAKING paragraph naming introspectSchema() (used to resolve { tables: {} }) and reclaimSpace() (used to resolve), both of which now reject with knex's 'Unable to acquire a connection' on a remote driver. findWithWindowFunctions(), analyzeQuery() / explain() and distinct() are listed apart, as error wording only or unchanged. The ADR-0087 not-required (no-migration-prescription) disposition follows the #19893 shape: no key, spec symbol, schema, object definition or stored representation moves, only which calls a remote driver answers, and #20055 carries the per-method answer or refusal. Every other sentence was kept as it was measured at head. `git diff --stat 014b58a689 HEAD` shows the changeset alone, 27 insertions and 7 deletions.",
      "changeset_gates_vs_merge_base_3557f85fa5": {
        "check-changeset-no-major": "exit 0 · '✓ This diff introduces no `major` bump.' · with the patched PR body as the --event payload: '✓ LEVEL AXIS: this PR declares clause-② `no (narrowing)`, and no package whose `packages/**/src/**` it moves is graded `patch`.' · with the CURRENT body ('Clause-②: no') also exit 0: '✓ LEVEL AXIS: this PR declares clause-② `no` …', so the stale body line does not redden CI",
        "check-adr-0087-registration": "exit 0 · '✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.' · '[BREAKING+bang+clause-②-narrowing]  not-required (no-migration-prescription)'",
        "check-empty-changeset": "exit 0 · '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' · '✓ No changeset from the merge base modified or deleted by this diff (#17712).'"
      },
      "gates": "dispatch-gates --commands at 93d49b886e: 61 commands, byte-identical to round 0, so no new family. The union was re-run at the new head. `--ran` with exit codes: '61 derived, 56 run, 5 NOT-MEASURED, 0 UNRUN'. The 5 NOT MEASURED exit 3 PREREQUISITE NOT MET because the fresh worktree has no dist/: check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content, check:type-check-debt. Three of them (dts-closure, lean-entry-closure, sourcemap-no-sources-content) read exit 0 at 014b58a689, whose code is byte-identical: the delta is the changeset alone. check-issue-citations --base 3557f85fa5: exit 0, 3 resolve. Control-byte scan of the changeset: no match.",
      "pr_body_edit": "NOT PERFORMED — conflict named. os-dev.md holds that a dev writes the PR body only once, in the pr_create stroke, and never PATCHes it; a later change is named in the report and the seat writes it. It also holds that on a conflict with a dispatch, the file wins and the conflict is stated. The prepared body is ready for the seat: the scratchpad file issue-20054/patch1/pr-body-patched.md, 15660 bytes, sha256 prefix be166075ccbad8c5. It is the stored #20073 body with three edits: (1) line 3 'Clause-②: no' becomes 'Clause-②: no (narrowing)'; (2) the Files bullet for the changeset becomes '`@objectstack/driver-turso` minor, BREAKING (patch round 1).'; (3) '## Semver' is rewritten and a '## Patch round 1' section added before the footer, saying what changed and why and quoting the three verdict lines above. The body was checked: no angle brackets, and no closing keyword before any number but #20054.",
      "mcp_calls": "0",
      "api_writes": "1 — post-stamped.mjs relay, POST /repos/objectstack-ai/objectstack/issues/20054/comments (this addendum). Plus the git push. 0 PR writes.",
      "deviations": [
        "The PR body edit ordered in step 2 was not made: os-dev.md forbids a dev PATCH of the PR body (see pr_body_edit). The patched body is prepared for the seat."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20073 at 93d49b88

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T02:14Z. Reviewed on GitHub against references/review-checklist.md, not from the dev's os-dev-report (5825245412) or its patch-round addendum (5825392145).

    Landing: ready plus auto-merge through the queue now.

  6. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20073, verified on main

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T02:56Z.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions