diff --git a/packages/adapter-libsql/src/libsql.test.ts b/packages/adapter-libsql/src/libsql.test.ts index 182d06bd7f73..baed733a2006 100644 --- a/packages/adapter-libsql/src/libsql.test.ts +++ b/packages/adapter-libsql/src/libsql.test.ts @@ -208,6 +208,39 @@ describe.each([ await expect(tx.rollback()).resolves.toBeUndefined() expect(factory.transaction.rollback).toHaveBeenCalledTimes(1) }) + + // https://github.com/prisma/prisma/issues/30028: a failed COMMIT must not leave the + // underlying connection inside an open write transaction, since the transaction manager + // never sends a real ROLLBACK for phantom-query adapters after a failed commit. + test('attempts a rollback and rethrows the original error when commit fails', async () => { + const factory = new PrismaLibSqlAdapterFactoryMock({ url: ':memory:' }) + const conn = await connect(factory) + const tx = await conn.startTransaction('SERIALIZABLE') + + const commitError = new Error('commit boom') + factory.transaction.commit.mockRejectedValueOnce(commitError) + + await expect(tx.commit()).rejects.toBe(commitError) + expect(factory.transaction.rollback).toHaveBeenCalledTimes(1) + expect(factory.transaction.close).not.toHaveBeenCalled() + }) + + test('closes the transaction and preserves the commit error when cleanup rollback fails', async () => { + const factory = new PrismaLibSqlAdapterFactoryMock({ url: ':memory:' }) + const conn = await connect(factory) + const tx = await conn.startTransaction('SERIALIZABLE') + + const commitError = new Error('commit boom') + factory.transaction.commit.mockRejectedValueOnce(commitError) + factory.transaction.rollback.mockRejectedValueOnce(new Error('cleanup boom')) + factory.transaction.close.mockImplementationOnce(() => { + throw new Error('close boom') + }) + + await expect(tx.commit()).rejects.toBe(commitError) + expect(factory.transaction.rollback).toHaveBeenCalledTimes(1) + expect(factory.transaction.close).toHaveBeenCalledTimes(1) + }) }) class PrismaLibSqlAdapterFactoryMock extends PrismaLibSqlAdapterFactoryBase { diff --git a/packages/adapter-libsql/src/libsql.ts b/packages/adapter-libsql/src/libsql.ts index 8eab92839e86..b21bf4411a95 100644 --- a/packages/adapter-libsql/src/libsql.ts +++ b/packages/adapter-libsql/src/libsql.ts @@ -113,6 +113,23 @@ class LibSqlTransaction extends LibSqlQueryable implements Tr try { await this.client.commit() + } catch (error) { + // A failed COMMIT leaves the underlying connection inside an open write + // transaction, holding the SQLite write lock indefinitely: the transaction + // manager does not send a real COMMIT/ROLLBACK for phantom-query adapters, + // so this is the only place that can clean it up. Best-effort: a secondary + // rollback failure must not replace the original commit error. + try { + await this.client.rollback() + } catch (rollbackError) { + debug('Error while cleaning up connection after failed commit: %O', rollbackError) + try { + this.client.close() + } catch (closeError) { + debug('Error while closing transaction after failed cleanup rollback: %O', closeError) + } + } + throw error } finally { this.#unlockParent() }