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
44 changes: 44 additions & 0 deletions example/tests/unit/specs/operations/transaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,52 @@ import { describe, it } from '@tests/TestApi'
import type { User } from '@/model/User'
import { testDb } from '@tests/db'

const FOREIGN_KEY_COMMIT_ERROR = 'FOREIGN KEY constraint failed'

async function expectDeferredForeignKeyCommitFailure(
finalize?: (
tx: Parameters<Parameters<typeof testDb.transaction>[0]>[0],
) => void,
) {
testDb.execute('PRAGMA foreign_keys = ON')
testDb.execute('DROP TABLE IF EXISTS Child')
testDb.execute('DROP TABLE IF EXISTS Parent')
testDb.execute('CREATE TABLE Parent (id INTEGER PRIMARY KEY)')
testDb.execute(
'CREATE TABLE Child (parentId INTEGER REFERENCES Parent(id) DEFERRABLE INITIALLY DEFERRED)',
)

try {
await testDb.transaction(async (tx) => {
tx.execute('INSERT INTO Child (parentId) VALUES (1)')
finalize?.(tx)
})
throw new Error(TEST_ERROR_CODES.EXPECT_PROMISE_REJECTION)
} catch (error) {
if (isNitroSQLiteError(error)) {
expect(error.message).toContain(FOREIGN_KEY_COMMIT_ERROR)
} else {
throw new Error(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR)
}
}

expect(testDb.execute('SELECT * FROM Child').rows?._array).toEqual([])

await testDb.transaction(async (tx) => {
tx.execute('INSERT INTO Parent (id) VALUES (1)')
})
}

export default function registerTransactionUnitTests() {
describe('transaction', () => {
it('Transaction, rolls back after automatic deferred foreign key commit failure', async () => {
await expectDeferredForeignKeyCommitFailure()
})

it('Transaction, rolls back after manual deferred foreign key commit failure', async () => {
await expectDeferredForeignKeyCommitFailure((tx) => tx.commit())
})

it('Transaction, auto commit', async () => {
const id = chance.integer()
const name = chance.name()
Expand Down
53 changes: 41 additions & 12 deletions packages/react-native-nitro-sqlite/src/operations/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,27 @@ import type {
import { execute, executeAsync } from './execute'
import NitroSQLiteError from '../NitroSQLiteError'

type TransactionState =
| 'active'
| 'committed'
| 'rolledBack'
| 'commitFinalizationFailed'
| 'rollbackFailed'

export const transaction = async <Result = void>(
dbName: string,
transactionCallback: (tx: Transaction) => Promise<Result>,
isExclusive = false,
) => {
throwIfDatabaseIsNotOpen(dbName)

let isFinished = false
let state: TransactionState = 'active'

const executeOnTransaction = <Row extends QueryResultRow = never>(
query: string,
params?: SQLiteQueryParams,
): QueryResult<Row> => {
if (isFinished) {
if (state !== 'active') {
throw new NitroSQLiteError(
`Cannot execute query on finalized transaction: ${dbName}`,
)
Expand All @@ -33,7 +40,7 @@ export const transaction = async <Result = void>(
query: string,
params?: SQLiteQueryParams,
): Promise<QueryResult<Row>> => {
if (isFinished) {
if (state !== 'active') {
throw new NitroSQLiteError(
`Cannot execute query on finalized transaction: ${dbName}`,
)
Expand All @@ -42,23 +49,35 @@ export const transaction = async <Result = void>(
}

const commit = () => {
if (isFinished) {
if (state !== 'active') {
throw new NitroSQLiteError(
`Cannot execute commit on finalized transaction: ${dbName}`,
)
}
isFinished = true
return execute(dbName, 'COMMIT')
try {
const result = execute(dbName, 'COMMIT')
state = 'committed'
return result
} catch (error) {
state = 'commitFinalizationFailed'
throw error
}
}

const rollback = () => {
if (isFinished) {
if (state !== 'active' && state !== 'commitFinalizationFailed') {
throw new NitroSQLiteError(
`Cannot execute rollback on finalized transaction: ${dbName}`,
)
}
isFinished = true
return execute(dbName, 'ROLLBACK')
try {
const result = execute(dbName, 'ROLLBACK')
state = 'rolledBack'
return result
} catch (error) {
state = 'rollbackFailed'
throw error
}
}

return await queueOperationAsync(dbName, async () => {
Expand All @@ -75,15 +94,25 @@ export const transaction = async <Result = void>(
rollback,
})

if (!isFinished) commit()
if (state === 'active') commit()

return result
} catch (executionError) {
if (!isFinished) {
if (state === 'active' || state === 'commitFinalizationFailed') {
try {
rollback()
} catch (rollbackError) {
throw NitroSQLiteError.fromError(rollbackError)
const primaryError = NitroSQLiteError.fromError(executionError)
const finalizationError = NitroSQLiteError.fromError(rollbackError)
throw new NitroSQLiteError(
`${primaryError.message}\nRollback failed: ${finalizationError.message}`,
{
cause: new AggregateError(
[primaryError, finalizationError],
'Transaction finalization failed',
),
},
)
}
}

Expand Down