tursodatabase / tursodatabase/libsql-client-ts
Sqlite3Client.transaction() silently destroys :memory: databases
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 576
- Forks
- 69
- PR merge metrics
- No merged PRs in 30d
Description
Version: @libsql/client@0.17.4
Description:
When using an in-memory database (file::memory: or :memory:), calling Sqlite3Client.transaction() internally sets this.#db = null, which causes the next #getDb() call to create a brand new, empty :memory: database — silently discarding all tables and data.
Root cause in sqlite3.js (https://github.com/tursodatabase/libsql-client-ts/blob/main/packages/libsql-client/src/sqlite3.ts):
async transaction(mode = "write") {
const db = this.#getDb();
executeStmt(db, transactionModeToBegin(mode), this.#intMode);
this.#db = null; // ← clears the handle — fatal for :memory:
return new Sqlite3Transaction(db, this.#intMode);
}
After transaction() commits/rolls back, #db remains null. The next call to execute(), batch(), or another transaction() triggers #getDb() → new Database(":memory:", options), which creates an entirely separate, empty in-memory database.
Why this matters:
In SQLite, each new Database(":memory:") creates an independent in-memory database. Unlike file-backed databases where a new connection still sees the same data, :memory: connections are completely isolated from each other.
This means every call to transaction() effectively resets the database when using :memory:, making :memory: unusable with any code that opens more than one transaction on the same client.
Minimal reproduction:
import Database from "libsql";
import { createClient } from "@libsql/client";
const client = createClient({ url: ":memory:" });
// Create a table
await client.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)");
await client.execute("INSERT INTO t VALUES (1)");
// First transaction — works fine
const tx1 = await client.transaction();
await tx1.execute("SELECT * FROM t"); // returns [{ id: 1 }]
await tx1.commit();
// Second transaction — explodes
const tx2 = await client.transaction();
await tx2.execute("SELECT * FROM t"); // SQLITE_ERROR: no such table: t
Expected behavior:
All transactions on the same Sqlite3Client should operate on the same in-memory database. The fix would be to not set #db = null for :memory: databases, or to use savepoints instead of managing the connection handle.
Same issue also affects drizzle-orm users on top of @libsql/client, since drizzle's LibSQLSession.transaction() delegates to Sqlite3Client.transaction().
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/libsql-client/src/sqlite3.ts, focusing on Sqlite3Client.transaction() and the subsequent #getDb() path. Run the minimal reproduction from the issue and verify that repeated transactions preserve the same :memory: tables and data without breaking file-backed behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- sqlite, typescript
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100