From a2c0c8252ba9bacac83274364ff4f8c045299675 Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Sat, 1 Aug 2026 18:46:50 +0000 Subject: [PATCH 1/3] Add reproduction for libsql/LibsqlClient issue --- .../sql/libsql/test/BeginPermitRepro.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/sql/libsql/test/BeginPermitRepro.test.ts diff --git a/packages/sql/libsql/test/BeginPermitRepro.test.ts b/packages/sql/libsql/test/BeginPermitRepro.test.ts new file mode 100644 index 00000000000..117b45e0e6b --- /dev/null +++ b/packages/sql/libsql/test/BeginPermitRepro.test.ts @@ -0,0 +1,38 @@ +import { LibsqlClient } from "@effect/sql-libsql" +import { assert, it } from "@effect/vitest" +import { Effect, Exit } from "effect" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" + +let transactionCalls = 0 + +const transaction = { + execute: () => Promise.resolve({ rows: [] }), + commit: () => Promise.resolve(), + rollback: () => Promise.resolve() +} + +const liveClient = { + execute: () => Promise.resolve({ rows: [] }), + transaction: () => { + transactionCalls++ + return transactionCalls === 1 + ? Promise.reject(new Error("transient begin failure")) + : Promise.resolve(transaction) + } +} + +it.effect("releases transaction serialization after begin fails", () => + Effect.gen(function*() { + transactionCalls = 0 + const client = yield* LibsqlClient.make({ liveClient: liveClient as any }) + const first = yield* Effect.exit(client.withTransaction(Effect.void)) + assert.isTrue(Exit.isFailure(first)) + + yield* Effect.forkChild(client.withTransaction(Effect.void)) + yield* Effect.yieldNow + + assert.strictEqual(transactionCalls, 2) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + )) From ff9c4190c62cf0030a2130c225d83b05b552d9d8 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Mon, 3 Aug 2026 11:34:07 +1200 Subject: [PATCH 2/3] Fix libSQL failed transaction cleanup --- .changeset/tidy-lions-smile.md | 5 +++++ packages/sql/libsql/src/LibsqlClient.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-lions-smile.md diff --git a/.changeset/tidy-lions-smile.md b/.changeset/tidy-lions-smile.md new file mode 100644 index 00000000000..871c3ab65cc --- /dev/null +++ b/.changeset/tidy-lions-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-libsql": patch +--- + +Release transaction serialization when beginning a libSQL transaction fails, allowing later operations to retry. diff --git a/packages/sql/libsql/src/LibsqlClient.ts b/packages/sql/libsql/src/LibsqlClient.ts index 4b574e039f5..581850f08d8 100644 --- a/packages/sql/libsql/src/LibsqlClient.ts +++ b/packages/sql/libsql/src/LibsqlClient.ts @@ -13,6 +13,7 @@ import * as Libsql from "@libsql/client" import * as Config from "effect/Config" import * as Context from "effect/Context" import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Redacted from "effect/Redacted" @@ -311,7 +312,9 @@ export const make = ( const scope = Scope.makeUnsafe() yield* restore(semaphore.take(1)) yield* Scope.addFinalizer(scope, semaphore.release(1)) - const conn = yield* connection.beginTransaction + const conn = yield* connection.beginTransaction.pipe( + Effect.tapCause((cause) => Scope.close(scope, Exit.failCause(cause))) + ) return [scope, conn] as const })), begin: () => Effect.void, // already begun in acquireConnection From f34db41255bac12bed66215801b223909dc4331f Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Mon, 3 Aug 2026 11:42:26 +1200 Subject: [PATCH 3/3] Move libSQL permit regression into client suite --- .../sql/libsql/test/BeginPermitRepro.test.ts | 38 ------------------- .../libsql/test/Client.integration.test.ts | 37 +++++++++++++++++- 2 files changed, 35 insertions(+), 40 deletions(-) delete mode 100644 packages/sql/libsql/test/BeginPermitRepro.test.ts diff --git a/packages/sql/libsql/test/BeginPermitRepro.test.ts b/packages/sql/libsql/test/BeginPermitRepro.test.ts deleted file mode 100644 index 117b45e0e6b..00000000000 --- a/packages/sql/libsql/test/BeginPermitRepro.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { LibsqlClient } from "@effect/sql-libsql" -import { assert, it } from "@effect/vitest" -import { Effect, Exit } from "effect" -import * as Reactivity from "effect/unstable/reactivity/Reactivity" - -let transactionCalls = 0 - -const transaction = { - execute: () => Promise.resolve({ rows: [] }), - commit: () => Promise.resolve(), - rollback: () => Promise.resolve() -} - -const liveClient = { - execute: () => Promise.resolve({ rows: [] }), - transaction: () => { - transactionCalls++ - return transactionCalls === 1 - ? Promise.reject(new Error("transient begin failure")) - : Promise.resolve(transaction) - } -} - -it.effect("releases transaction serialization after begin fails", () => - Effect.gen(function*() { - transactionCalls = 0 - const client = yield* LibsqlClient.make({ liveClient: liveClient as any }) - const first = yield* Effect.exit(client.withTransaction(Effect.void)) - assert.isTrue(Exit.isFailure(first)) - - yield* Effect.forkChild(client.withTransaction(Effect.void)) - yield* Effect.yieldNow - - assert.strictEqual(transactionCalls, 2) - }).pipe( - Effect.scoped, - Effect.provide(Reactivity.layer) - )) diff --git a/packages/sql/libsql/test/Client.integration.test.ts b/packages/sql/libsql/test/Client.integration.test.ts index c2f925f7bd5..352f647d563 100644 --- a/packages/sql/libsql/test/Client.integration.test.ts +++ b/packages/sql/libsql/test/Client.integration.test.ts @@ -1,6 +1,7 @@ import { LibsqlClient } from "@effect/sql-libsql" -import { assert, describe, layer } from "@effect/vitest" -import { Effect, Layer } from "effect" +import { assert, describe, it, layer } from "@effect/vitest" +import { Effect, Exit, Layer } from "effect" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" import { LibsqlContainer } from "./util.ts" const Migrations = Layer.effectDiscard( @@ -15,6 +16,38 @@ const Migrations = Layer.effectDiscard( ) describe("Client", () => { + it.effect("releases transaction serialization after begin fails", () => { + let transactionCalls = 0 + const transaction = { + execute: () => Promise.resolve({ rows: [] }), + commit: () => Promise.resolve(), + rollback: () => Promise.resolve() + } + const liveClient = { + execute: () => Promise.resolve({ rows: [] }), + transaction: () => { + transactionCalls++ + return transactionCalls === 1 + ? Promise.reject(new Error("transient begin failure")) + : Promise.resolve(transaction) + } + } + + return Effect.gen(function*() { + const client = yield* LibsqlClient.make({ liveClient: liveClient as any }) + const first = yield* Effect.exit(client.withTransaction(Effect.void)) + assert.isTrue(Exit.isFailure(first)) + + yield* Effect.forkChild(client.withTransaction(Effect.void)) + yield* Effect.yieldNow + + assert.strictEqual(transactionCalls, 2) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + ) + }) + layer(LibsqlContainer.layerClient, { timeout: "30 seconds" })((it) => { it.effect("should work", () => Effect.gen(function*() {