You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Usage of transactions in :memory: database was broken with #105 #229
#105 regressed in memory databases when transactions are used. Once a transaction is created, the entire in memory database will be effectively discarded for subsequent db operations that the client makes.
From the regressing PR.
A single connection is created lazily whenever user executes a statement
When a transaction starts, the Transaction object takes ownership of the current transaction.
Subsequent queries will create a new connection.
In particular, this line of code from within transaction, null's out this.#db.
Subsequent calls to #getDb will create a new database here. Because it's purely in memory, this essentially creates a new empty database.
I wonder if the fix would be as simple as skipping the dropping/recreating of the Database if path === ':memory'.
asynctransaction(mode: TransactionMode="write"): Promise<Transaction>{constdb=this.#getDb();executeStmt(db,transactionModeToBegin(mode),this.#intMode);if(this.#path !==':memory:'){this.#db =null;// A new connection will be lazily created on next use}returnnewSqlite3Transaction(db,this.#intMode);}// Lazily creates the database connection and returns it
#getDb(): Database.Database{if(this.#db ===null&&this.$path!==':memory:'){this.#db =newDatabase(this.#path,this.#options);}returnthis.#db;}
@khuezy, how does 0.9.0 fix this? Could you point out the relevant part in the diff?
file::memory:?cache=shared
For me, this revealed another problem: closing the database connection did not appear to be working. New connections were still using the old database, even after closing. Has anyone else encountered this too? @thewilkybarkid is this what you're referring to in the commit you've linked?
Based on @epatey's comment, I used the following patch to resolve:
diff --git a/node_modules/@libsql/client/lib-cjs/sqlite3.js b/node_modules/@libsql/client/lib-cjs/sqlite3.js
index f479612..5bb7f4d 100644
--- a/node_modules/@libsql/client/lib-cjs/sqlite3.js+++ b/node_modules/@libsql/client/lib-cjs/sqlite3.js@@ -145,7 +145,11 @@ class Sqlite3Client {
async transaction(mode = "write") {
const db = this.#getDb();
executeStmt(db, (0, util_1.transactionModeToBegin)(mode), this.#intMode);
- this.#db = null; // A new connection will be lazily created on next use++ if (!this.#path.includes(":memory:")) {+ this.#db = null; // A new connection will be lazily created on next use+ }+
return new Sqlite3Transaction(db, this.#intMode);
}
async executeMultiple(sql) {
diff --git a/node_modules/@libsql/client/lib-esm/sqlite3.js b/node_modules/@libsql/client/lib-esm/sqlite3.js
index 8aa7047..22b6781 100644
--- a/node_modules/@libsql/client/lib-esm/sqlite3.js+++ b/node_modules/@libsql/client/lib-esm/sqlite3.js@@ -123,7 +123,11 @@ export class Sqlite3Client {
async transaction(mode = "write") {
const db = this.#getDb();
executeStmt(db, transactionModeToBegin(mode), this.#intMode);
- this.#db = null; // A new connection will be lazily created on next use++ if (!this.#path.includes(":memory:")) {+ this.#db = null; // A new connection will be lazily created on next use+ }+
return new Sqlite3Transaction(db, this.#intMode);
}
async executeMultiple(sql) {
this.#path.includes(':memory:') handles the existence of the file scheme and potential query parameters:
Yea it's more of a workaround. Was probably fixed around 0.8, my original comment was meant to say "upgrade to the latest 0.9.0 where the workaround was introduced after 0.7.0". I only use :memory: in my tests so the work around was fine for me.
Confirming this is still broken on the current main branch and @libsql/client@0.17.2, with the canonical :memory: URL (no cache=shared workaround).
Minimal repro (17 lines):
import{createClient}from"@libsql/client";constclient=createClient({url: ":memory:"});awaitclient.execute("CREATE TABLE things (id INTEGER PRIMARY KEY, name TEXT)");awaitclient.execute("INSERT INTO things VALUES (1, 'first')");constbefore=awaitclient.execute("SELECT * FROM things");console.log("before tx :",before.rows.length,"row(s)");// 1consttx=awaitclient.transaction("write");awaittx.execute("INSERT INTO things VALUES (2, 'second')");awaittx.commit();constafter=awaitclient.execute("SELECT * FROM things");console.log("after tx :",after.rows.length,"row(s)");// Throws: LibsqlError: SQLITE_ERROR: no such table: things
I hit this while porting a test harness to Drizzle + @libsql/client. A typical createTestDb() helper using :memory: silently loses its schema mid-test the first time any code path goes through a Drizzle db.transaction(...) — a nasty footgun for anyone using a Drizzle-style in-memory test database.
I've opened #342 with a fix that takes @epatey's original suggestion (this.#db = null guarded on in-memory URLs) and uses the existing isInMemoryConfig helper from @libsql/core/config instead of a path-string check, so all three URL forms (:memory:, file::memory:, file::memory:?cache=private) are handled uniformly.
The PR updates the three existing tests under describe("execute()") that were asserting rejects.toThrow() — those were documenting the broken behaviour rather than guarding against it — and adds two positive withInMemoryClient tests for commit-preserves-data and rollback-preserves-original-state. Full in-memory suite (14 tests) and full file-backed suite (171 tests) both pass with no regressions.
Activity
epatey commentedon Jun 24, 2024
erkannt commentedon Jun 28, 2024
Related? tursodatabase/libsql#1411
khuezy commentedon Aug 15, 2024
This is fixed in 0.9.0
file::memory:?cache=sharedMitigate problems using an in-memory database
winghouchan commentedon Dec 21, 2024
@khuezy, how does
0.9.0fix this? Could you point out the relevant part in the diff?For me, this revealed another problem: closing the database connection did not appear to be working. New connections were still using the old database, even after closing. Has anyone else encountered this too? @thewilkybarkid is this what you're referring to in the commit you've linked?
Based on @epatey's comment, I used the following patch to resolve:
this.#path.includes(':memory:')handles the existence of the file scheme and potential query parameters::memory:file::memory:file::memory:?cache=sharedThe modification in
getDbdid not seem necessary.khuezy commentedon Dec 23, 2024
@winghouchan Hi, my comment wasn't clear, the OP was in June so the diff was on an older version:
v0.7.0...v0.9.0
I'm not sure of the issue you're having, perhaps messaging the team on the discord will get their attention if there's an issue.
winghouchan commentedon Dec 23, 2024
@khuezy, so it wasn't
0.9.0then?Taking a look through PRs, are you referring to #220 (released in
0.8.0(commit))?winghouchan commentedon Dec 23, 2024
Ah, I see #220 intended to fix tursodatabase/libsql#1411 which is a duplicate of this issue.
winghouchan commentedon Dec 23, 2024
I don't think it actually fixes the issue but introduces a workaround (cache mode of
shared) which itself has weird behaviours. I've created an MCVE here: https://github.com/winghouchan/libsql-in-memory-transactions-broken-mcvewinghouchan commentedon Dec 23, 2024
@sivukhin, what are your thoughts as the author of #220?
khuezy commentedon Dec 23, 2024
Yea it's more of a workaround. Was probably fixed around
0.8, my original comment was meant to say "upgrade to the latest 0.9.0 where the workaround was introduced after0.7.0". I only use:memory:in my tests so the work around was fine for me.refactor(database): migrate from expo sqlite to op sqlite
fix(data): patch @libsql/client to replay PRAGMAs after transaction()
fix(sqlite3): keep connection handle for in-memory databases across t…
BartWaardenburg commentedon Apr 10, 2026
Confirming this is still broken on the current
mainbranch and@libsql/client@0.17.2, with the canonical:memory:URL (nocache=sharedworkaround).Minimal repro (17 lines):
I hit this while porting a test harness to Drizzle +
@libsql/client. A typicalcreateTestDb()helper using:memory:silently loses its schema mid-test the first time any code path goes through a Drizzledb.transaction(...)— a nasty footgun for anyone using a Drizzle-style in-memory test database.I've opened #342 with a fix that takes @epatey's original suggestion (
this.#db = nullguarded on in-memory URLs) and uses the existingisInMemoryConfighelper from@libsql/core/configinstead of a path-string check, so all three URL forms (:memory:,file::memory:,file::memory:?cache=private) are handled uniformly.Also folds in #140, which is the same bug.
The PR updates the three existing tests under
describe("execute()")that were assertingrejects.toThrow()— those were documenting the broken behaviour rather than guarding against it — and adds two positivewithInMemoryClienttests for commit-preserves-data and rollback-preserves-original-state. Full in-memory suite (14 tests) and full file-backed suite (171 tests) both pass with no regressions.fix(backend): keep local libsql databases intact across transactions …
feat: provenance retraction on a composable, atomic write pipeline (#188
Version Packages (#193)
fix(sqlite3): keep connection handle for in-memory databases across t…
fix(sqlite3): give a client one connection, for file and memory alike
fix(sqlite3): give a client a connection pool, like the hrana clients
fix(sqlite3): give a client a connection pool, like the hrana clients
sqlite3: keep connection handle for in-memory databases across transa…