Skip to content

fix(sqlite): shim db.transaction(fn) on node:sqlite DatabaseSync#123

Merged
tickernelz merged 1 commit into
tickernelz:mainfrom
leiverkus:fix/db-transaction-shim
Jun 7, 2026
Merged

fix(sqlite): shim db.transaction(fn) on node:sqlite DatabaseSync#123
tickernelz merged 1 commit into
tickernelz:mainfrom
leiverkus:fix/db-transaction-shim

Conversation

@leiverkus
Copy link
Copy Markdown
Contributor

Follow-up to #121.

Summary

bun:sqlite and better-sqlite3 both expose db.transaction(fn) which returns a callable that wraps fn in BEGIN/COMMIT (auto-ROLLBACK on throw). node:sqlite's DatabaseSync has no equivalent.

handleAddMemory (services/api-handlers.ts) and client.addMemory (services/client.ts) both use this method to atomically insert a memory row, so under Node:

  • POST /api/memories crashes with db.transaction is not a function
  • Any auto-capture path that promotes a candidate memory crashes the same way

Reproduced today on opencode 1.15.10 (anomalyco fork) + Node sidecar:

$ curl -X POST http://127.0.0.1:4747/api/memories \
    -H 'Content-Type: application/json' \
    -d '{"content": "test", "containerTag": "scope-project-xxx", "type": "fact"}'
{"success":false,"error":"TypeError: db.transaction is not a function"}

(After patching the sharp install separately — opencode-mem still ships @xenova/transformers which needs sharp's native binary, but that's a pre-existing packaging issue, not in scope here.)

Fix

Subclass DatabaseSync to add a transaction(fn) method matching the bun:sqlite / better-sqlite3 signature. Single-mode (BEGIN) semantics only — the .deferred / .immediate / .exclusive variants are not exercised by this codebase, can be added in a follow-up if needed.

transaction<Fn extends (...args: unknown[]) => unknown>(fn: Fn): Fn {
  const self = this;
  const wrapped = function (this: unknown, ...args: Parameters<Fn>): ReturnType<Fn> {
    self.exec("BEGIN");
    try {
      const result = fn.apply(this, args) as ReturnType<Fn>;
      self.exec("COMMIT");
      return result;
    } catch (err) {
      try {
        self.exec("ROLLBACK");
      } catch {
        /* best-effort rollback after partial state */
      }
      throw err;
    }
  };
  return wrapped as unknown as Fn;
}

Why it slipped through #121

My local E2E smoke test exercised CRUD via prepared statements but never called db.transaction(). The Bun test suite covers a lot of surface but doesn't reach the bun:sqlite-specific transaction path in a Node-runtime-only context. Today's session log on the live Node sidecar surfaced the gap immediately on the first POST /api/memories.

Verification

  • bun test: 143 pass / 0 fail (Bun path unchanged)
  • bun run typecheck: clean
  • bun run build: clean
  • npx prettier --check: clean
  • Node 26 E2E with commit + rollback paths:
    • Successful tx commits both inserts → SELECT * FROM m returns both rows
    • Failing tx throws after one insert → SELECT still shows the pre-tx state (rollback worked)
  • Local patch applied to the cached v2.15.0 install → POST /api/memories now returns {success:true} as expected.

Environment

Follow-up to tickernelz#121. `bun:sqlite` and `better-sqlite3` both expose
`db.transaction(fn)` which returns a callable that wraps `fn` in
BEGIN/COMMIT (auto-ROLLBACK on throw). `node:sqlite`'s `DatabaseSync`
has no equivalent.

`handleAddMemory` in `api-handlers.ts` and `client.addMemory` in
`services/client.ts` both use this method to atomically insert a memory
row, so the POST /api/memories endpoint and any auto-capture path crash
under Node with `db.transaction is not a function`. The original PR
caught the `db.run(sql)` gap but missed the transaction gap because the
local E2E smoke test exercised CRUD without atomicity.

Single-mode semantics only (BEGIN); the `.deferred` / `.immediate` /
`.exclusive` variants from better-sqlite3 are not exercised by this
codebase. Best-effort rollback (ignores secondary errors from the
ROLLBACK statement after partial state).

## Verification

- `bun test`: 143 pass / 0 fail (Bun path unchanged)
- `bun run typecheck`: clean
- `bun run build`: clean
- `npx prettier --check`: clean
- Node 26 E2E with commit + rollback:
  - tx() inserts a, b → SELECT returns [{t:"a"},{t:"b"}]
  - txFail() throws after inserting c → SELECT still returns [{t:"a"},{t:"b"}]
- Reproduced and patched the original failure path locally
  (POST /api/memories now returns success after applying this shim).
@tickernelz tickernelz merged commit 504e742 into tickernelz:main Jun 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants