Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/adapter-libsql/src/libsql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions packages/adapter-libsql/src/libsql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,23 @@ class LibSqlTransaction extends LibSqlQueryable<TransactionClient> 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()
}
Expand Down