From 2a0eb4cf418498b838dfe3ac568487e86207123e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 3 Nov 2025 10:53:33 +0000 Subject: [PATCH 01/28] Update bun.lockb --- bun.lockb | Bin 540064 -> 550320 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bun.lockb b/bun.lockb index fb326774965052003a19c053d424e0b6c995f5da..6eeeaa6b3dc28070857ee57832a80b7219159757 100755 GIT binary patch delta 19524 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

8`; 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 E19*&HXaE2J delta 16353 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 DA60Xu From ad478a3b711d979bfab1d50aad58ec7feffcf050 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 3 Nov 2025 15:54:27 +0000 Subject: [PATCH 02/28] feat: implement locks for `executeBatch` --- package/src/concurrency.ts | 8 ++++ package/src/nitro.ts | 6 --- package/src/operations/executeBatch.ts | 54 ++++++++++++++++++++++---- package/src/operations/transaction.ts | 32 +++++---------- 4 files changed, 65 insertions(+), 35 deletions(-) create mode 100644 package/src/concurrency.ts diff --git a/package/src/concurrency.ts b/package/src/concurrency.ts new file mode 100644 index 00000000..77248e2d --- /dev/null +++ b/package/src/concurrency.ts @@ -0,0 +1,8 @@ +export interface QueuedOperation { + start: () => void +} + +export const locks: Record< + string, + { queue: QueuedOperation[]; inProgress: boolean } +> = {} diff --git a/package/src/nitro.ts b/package/src/nitro.ts index 293e5c35..c1c552e2 100644 --- a/package/src/nitro.ts +++ b/package/src/nitro.ts @@ -1,11 +1,5 @@ import { NitroModules } from 'react-native-nitro-modules' import type { NitroSQLite as NitroSQLiteSpec } from './specs/NitroSQLite.nitro' -import type { PendingTransaction } from './operations/transaction' export const HybridNitroSQLite = NitroModules.createHybridObject('NitroSQLite') - -export const locks: Record< - string, - { queue: PendingTransaction[]; inProgress: boolean } -> = {} diff --git a/package/src/operations/executeBatch.ts b/package/src/operations/executeBatch.ts index b31aafee..cde12926 100644 --- a/package/src/operations/executeBatch.ts +++ b/package/src/operations/executeBatch.ts @@ -3,38 +3,78 @@ import { replaceWithNativeNullValue, } from '../nullHandling' import { HybridNitroSQLite } from '../nitro' +import { locks, type QueuedOperation } from '../concurrency' import type { NativeSQLiteQueryParams, BatchQueryResult, BatchQueryCommand, NativeBatchQueryCommand, } from '../types' +import { startNextOperation } from './transaction' export function executeBatch( dbName: string, commands: BatchQueryCommand[] ): BatchQueryResult { + if (locks[dbName] == null) + throw Error(`Nitro SQLite Error: No lock found on db: ${dbName}`) + const transformedCommands = isSimpleNullHandlingEnabled() ? toNativeBatchQueryCommands(commands) : (commands as NativeBatchQueryCommand[]) - const result = HybridNitroSQLite.executeBatch(dbName, transformedCommands) - return result + // If lock is immediately available, execute synchronously + if (!locks[dbName].inProgress && locks[dbName].queue.length === 0) { + locks[dbName].inProgress = true + try { + const result = HybridNitroSQLite.executeBatch(dbName, transformedCommands) + return result + } finally { + locks[dbName].inProgress = false + startNextOperation(dbName) + } + } + + // Lock is busy - cannot execute synchronously + throw Error( + `Nitro SQLite Error: Database ${dbName} is busy with another operation. Use executeBatchAsync for queued execution.` + ) } export async function executeBatchAsync( dbName: string, commands: BatchQueryCommand[] ): Promise { + if (locks[dbName] == null) + throw Error(`Nitro SQLite Error: No lock found on db: ${dbName}`) + const transformedCommands = isSimpleNullHandlingEnabled() ? toNativeBatchQueryCommands(commands) : (commands as NativeBatchQueryCommand[]) - const result = await HybridNitroSQLite.executeBatchAsync( - dbName, - transformedCommands - ) - return result + async function run() { + try { + const result = await HybridNitroSQLite.executeBatchAsync( + dbName, + transformedCommands + ) + return result + } finally { + locks[dbName]!.inProgress = false + startNextOperation(dbName) + } + } + + return new Promise((resolve, reject) => { + const operation: QueuedOperation = { + start: () => { + run().then(resolve).catch(reject) + }, + } + + locks[dbName]?.queue.push(operation) + startNextOperation(dbName) + }) } function toNativeBatchQueryCommands( diff --git a/package/src/operations/transaction.ts b/package/src/operations/transaction.ts index b4b07ad2..112d709e 100644 --- a/package/src/operations/transaction.ts +++ b/package/src/operations/transaction.ts @@ -1,4 +1,5 @@ -import { locks, HybridNitroSQLite } from '../nitro' +import { HybridNitroSQLite } from '../nitro' +import { locks, type QueuedOperation } from '../concurrency' import type { QueryResult, Transaction, @@ -7,19 +8,6 @@ import type { } from '../types' import { execute, executeAsync } from './execute' -export interface PendingTransaction { - /* - * The start function should not throw or return a promise because the - * queue just calls it and does not monitor for failures or completions. - * - * It should catch any errors and call the resolve or reject of the wrapping - * promise when complete. - * - * It should also automatically commit or rollback the transaction if needed - */ - start: () => void -} - export const transaction = ( dbName: string, fn: (tx: Transaction) => Promise | void @@ -101,36 +89,36 @@ export const transaction = ( } finally { locks[dbName]!.inProgress = false isFinalized = false - startNextTransaction(dbName) + startNextOperation(dbName) } } return new Promise((resolve, reject) => { - const tx: PendingTransaction = { + const queuedTransaction: QueuedOperation = { start: () => { run().then(resolve).catch(reject) }, } - locks[dbName]?.queue.push(tx) - startNextTransaction(dbName) + locks[dbName]?.queue.push(queuedTransaction) + startNextOperation(dbName) }) } -function startNextTransaction(dbName: string) { +export function startNextOperation(dbName: string) { if (locks[dbName] == null) throw Error(`Lock not found for db: ${dbName}`) if (locks[dbName].inProgress) { - // Transaction is already in process bail out + // Operation is already in process bail out return } if (locks[dbName].queue.length > 0) { locks[dbName].inProgress = true - const tx = locks[dbName].queue.shift()! + const operation = locks[dbName].queue.shift()! setImmediate(() => { - tx.start() + operation.start() }) } } From ee332157da7b49b0e25650ac69bfde8710aad516 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 3 Nov 2025 17:04:21 +0000 Subject: [PATCH 03/28] feat: add better errors --- package/src/NitroSQLiteError.ts | 52 +++++++++++++++++++++++++++++++ package/src/operations/execute.ts | 36 ++++++++++++--------- 2 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 package/src/NitroSQLiteError.ts diff --git a/package/src/NitroSQLiteError.ts b/package/src/NitroSQLiteError.ts new file mode 100644 index 00000000..6745bcd6 --- /dev/null +++ b/package/src/NitroSQLiteError.ts @@ -0,0 +1,52 @@ +const NITRO_SQLITE_ERROR_NAME = 'NitroSQLiteError' + +/** + * Custom error class for NitroSQLite operations + * Extends the native Error class with proper prototype chain and error handling + */ +export default class NitroSQLiteError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options) + this.name = NITRO_SQLITE_ERROR_NAME + + // Maintains proper prototype chain for instanceof checks + Object.setPrototypeOf(this, NitroSQLiteError.prototype) + } + + /** + * Converts an unknown error to a NitroSQLiteError + * Preserves stack traces and error causes when available + */ + static fromError(error: unknown): NitroSQLiteError { + if (error instanceof NitroSQLiteError) { + return error + } + + if (error instanceof Error) { + const nitroSQLiteError = new NitroSQLiteError(error.message, { + cause: error.cause, + }) + // Preserve original stack trace if available + if (error.stack) { + nitroSQLiteError.stack = error.stack + } + return nitroSQLiteError + } + + if (typeof error === 'string') { + return new NitroSQLiteError(error) + } + + return new NitroSQLiteError('Unknown error occurred', { + cause: error, + }) + } + + /** + * Converts a native error (from C++ bridge) to a NitroSQLiteError + * Alias for fromError for semantic clarity + */ + static fromNativeError(error: unknown): NitroSQLiteError { + return NitroSQLiteError.fromError(error) + } +} diff --git a/package/src/operations/execute.ts b/package/src/operations/execute.ts index 0b708198..2c7da0d1 100644 --- a/package/src/operations/execute.ts +++ b/package/src/operations/execute.ts @@ -8,6 +8,7 @@ import type { SQLiteQueryParams, QueryResultRow, } from '../types' +import NitroSQLiteError from '../NitroSQLiteError' export function execute( dbName: string, @@ -18,13 +19,17 @@ export function execute( ? toNativeQueryParams(params) : (params as NativeSQLiteQueryParams) - const nativeResult = HybridNitroSQLite.execute( - dbName, - query, - transformedParams - ) - const result = buildJsQueryResult(nativeResult) - return result + try { + const nativeResult = HybridNitroSQLite.execute( + dbName, + query, + transformedParams + ) + + return buildJsQueryResult(nativeResult) + } catch (error) { + throw NitroSQLiteError.fromError(error) + } } export async function executeAsync( @@ -36,13 +41,16 @@ export async function executeAsync( ? toNativeQueryParams(params) : (params as NativeSQLiteQueryParams) - const nativeResult = await HybridNitroSQLite.executeAsync( - dbName, - query, - transformedParams - ) - const result = buildJsQueryResult(nativeResult) - return result + try { + const nativeResult = await HybridNitroSQLite.executeAsync( + dbName, + query, + transformedParams + ) + return buildJsQueryResult(nativeResult) + } catch (error) { + throw NitroSQLiteError.fromError(error) + } } function toNativeQueryParams( From 3c85b867c706d9c77f952ccad811b7eb299bfefb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 3 Nov 2025 17:04:28 +0000 Subject: [PATCH 04/28] feat: improve concurrency features --- package/src/DatabaseQueue.ts | 128 +++++++++++++++++++++++++ package/src/concurrency.ts | 8 -- package/src/operations/executeBatch.ts | 70 ++++++-------- package/src/operations/session.ts | 22 +++-- package/src/operations/transaction.ts | 101 +++++++------------ 5 files changed, 206 insertions(+), 123 deletions(-) create mode 100644 package/src/DatabaseQueue.ts delete mode 100644 package/src/concurrency.ts diff --git a/package/src/DatabaseQueue.ts b/package/src/DatabaseQueue.ts new file mode 100644 index 00000000..1295cf9c --- /dev/null +++ b/package/src/DatabaseQueue.ts @@ -0,0 +1,128 @@ +import NitroSQLiteError from './NitroSQLiteError' + +export interface QueuedOperation { + /** + * Starts the operation + */ + start: () => void +} + +export type DatabaseQueue = { + queue: QueuedOperation[] + inProgress: boolean +} + +const databaseQueues = new Map() + +export function openDatabaseQueue(dbName: string) { + if (isDatabaseOpen(dbName)) { + throw new NitroSQLiteError( + `Database ${dbName} is already open. There is already a connection to the database.` + ) + } + + databaseQueues.set(dbName, { queue: [], inProgress: false }) +} + +export function closeDatabaseQueue(dbName: string) { + const queue = getDatabaseQueue(dbName) + + if (queue.inProgress || queue.queue.length > 0) { + console.warn( + `Database queue for ${dbName} has operations in the queue. Closing anyway.` + ) + } + + databaseQueues.delete(dbName) +} + +export function isDatabaseOpen(dbName: string) { + return databaseQueues.has(dbName) +} + +export function throwIfDatabaseIsNotOpen(dbName: string) { + if (!isDatabaseOpen(dbName)) + throw new NitroSQLiteError( + `Database ${dbName} is not open. There is no connection to the database.` + ) +} + +function getDatabaseQueue(dbName: string) { + throwIfDatabaseIsNotOpen(dbName) + + const queue = databaseQueues.get(dbName)! + return queue +} + +export function openDatabase(dbName: string) { + databaseQueues.set(dbName, { queue: [], inProgress: false }) +} + +export function closeDatabase(dbName: string) { + databaseQueues.delete(dbName) +} + +export function queueOperationAsync< + OperationCallback extends () => Promise, + Result = void, +>(dbName: string, callback: OperationCallback) { + const queue = getDatabaseQueue(dbName) + + return new Promise((resolve, reject) => { + const operation: QueuedOperation = { + start: async () => { + try { + const result = await callback() + resolve(result) + } catch (error) { + reject(error) + } finally { + queue.inProgress = false + startOperationAsync(dbName) + } + }, + } + + queue.queue.push(operation) + startOperationAsync(dbName) + }) +} + +function startOperationAsync(dbName: string) { + const queue = getDatabaseQueue(dbName) + + if (queue.inProgress) { + // Operation is already in process bail out + return + } + + if (queue.queue.length > 0) { + queue.inProgress = true + + const operation = queue.queue.shift()! + setImmediate(() => { + operation.start() + }) + } +} + +export function startOperationSync< + OperationCallback extends () => Result, + Result = void, +>(dbName: string, callback: OperationCallback) { + const queue = getDatabaseQueue(dbName) + + // Database is busy - cannot execute synchronously + if (queue.inProgress || queue.queue.length > 0) { + throw new NitroSQLiteError( + `Cannot run synchronous operation on database. Database ${dbName} is busy with another operation.` + ) + } + + // Execute synchronously + queue.inProgress = true + const result = callback() + queue.inProgress = false + + return result +} diff --git a/package/src/concurrency.ts b/package/src/concurrency.ts deleted file mode 100644 index 77248e2d..00000000 --- a/package/src/concurrency.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface QueuedOperation { - start: () => void -} - -export const locks: Record< - string, - { queue: QueuedOperation[]; inProgress: boolean } -> = {} diff --git a/package/src/operations/executeBatch.ts b/package/src/operations/executeBatch.ts index cde12926..bc81b182 100644 --- a/package/src/operations/executeBatch.ts +++ b/package/src/operations/executeBatch.ts @@ -3,78 +3,62 @@ import { replaceWithNativeNullValue, } from '../nullHandling' import { HybridNitroSQLite } from '../nitro' -import { locks, type QueuedOperation } from '../concurrency' +import { + queueOperationAsync, + startOperationSync, + throwIfDatabaseIsNotOpen, +} from '../DatabaseQueue' import type { NativeSQLiteQueryParams, BatchQueryResult, BatchQueryCommand, NativeBatchQueryCommand, } from '../types' -import { startNextOperation } from './transaction' +import NitroSQLiteError from '../NitroSQLiteError' export function executeBatch( dbName: string, commands: BatchQueryCommand[] ): BatchQueryResult { - if (locks[dbName] == null) - throw Error(`Nitro SQLite Error: No lock found on db: ${dbName}`) + throwIfDatabaseIsNotOpen(dbName) const transformedCommands = isSimpleNullHandlingEnabled() ? toNativeBatchQueryCommands(commands) : (commands as NativeBatchQueryCommand[]) - // If lock is immediately available, execute synchronously - if (!locks[dbName].inProgress && locks[dbName].queue.length === 0) { - locks[dbName].inProgress = true - try { - const result = HybridNitroSQLite.executeBatch(dbName, transformedCommands) - return result - } finally { - locks[dbName].inProgress = false - startNextOperation(dbName) - } + try { + return startOperationSync(dbName, () => + HybridNitroSQLite.executeBatch(dbName, transformedCommands) + ) + } catch (error) { + throw NitroSQLiteError.fromError(error) } - - // Lock is busy - cannot execute synchronously - throw Error( - `Nitro SQLite Error: Database ${dbName} is busy with another operation. Use executeBatchAsync for queued execution.` - ) } export async function executeBatchAsync( dbName: string, commands: BatchQueryCommand[] ): Promise { - if (locks[dbName] == null) - throw Error(`Nitro SQLite Error: No lock found on db: ${dbName}`) + throwIfDatabaseIsNotOpen(dbName) const transformedCommands = isSimpleNullHandlingEnabled() ? toNativeBatchQueryCommands(commands) : (commands as NativeBatchQueryCommand[]) - async function run() { - try { - const result = await HybridNitroSQLite.executeBatchAsync( - dbName, - transformedCommands - ) - return result - } finally { - locks[dbName]!.inProgress = false - startNextOperation(dbName) - } + try { + return queueOperationAsync(dbName, async () => { + try { + return await HybridNitroSQLite.executeBatchAsync( + dbName, + transformedCommands + ) + } catch (error) { + throw NitroSQLiteError.fromError(error) + } + }) + } catch (error) { + throw NitroSQLiteError.fromError(error) } - - return new Promise((resolve, reject) => { - const operation: QueuedOperation = { - start: () => { - run().then(resolve).catch(reject) - }, - } - - locks[dbName]?.queue.push(operation) - startNextOperation(dbName) - }) } function toNativeBatchQueryCommands( diff --git a/package/src/operations/session.ts b/package/src/operations/session.ts index 7ac3d7c1..3c840a68 100644 --- a/package/src/operations/session.ts +++ b/package/src/operations/session.ts @@ -1,4 +1,4 @@ -import { locks, HybridNitroSQLite } from '../nitro' +import { HybridNitroSQLite } from '../nitro' import { transaction } from './transaction' import type { BatchQueryCommand, @@ -11,6 +11,8 @@ import type { } from '../types' import { execute, executeAsync } from './execute' import { executeBatch, executeBatchAsync } from './executeBatch' +import NitroSQLiteError from '../NitroSQLiteError' +import { closeDatabaseQueue, openDatabaseQueue } from '../DatabaseQueue' export function open( options: NitroSQLiteConnectionOptions @@ -45,15 +47,19 @@ export function open( } export function openDb(dbName: string, location?: string) { - HybridNitroSQLite.open(dbName, location) - - locks[dbName] = { - queue: [], - inProgress: false, + try { + HybridNitroSQLite.open(dbName, location) + openDatabaseQueue(dbName) + } catch (error) { + throw NitroSQLiteError.fromError(error) } } export function close(dbName: string) { - HybridNitroSQLite.close(dbName) - delete locks[dbName] + try { + HybridNitroSQLite.close(dbName) + closeDatabaseQueue(dbName) + } catch (error) { + throw NitroSQLiteError.fromError(error) + } } diff --git a/package/src/operations/transaction.ts b/package/src/operations/transaction.ts index 112d709e..84b6e8b7 100644 --- a/package/src/operations/transaction.ts +++ b/package/src/operations/transaction.ts @@ -1,5 +1,5 @@ import { HybridNitroSQLite } from '../nitro' -import { locks, type QueuedOperation } from '../concurrency' +import { queueOperationAsync, throwIfDatabaseIsNotOpen } from '../DatabaseQueue' import type { QueryResult, Transaction, @@ -7,13 +7,13 @@ import type { QueryResultRow, } from '../types' import { execute, executeAsync } from './execute' +import NitroSQLiteError from '../NitroSQLiteError' export const transaction = ( dbName: string, fn: (tx: Transaction) => Promise | void ): Promise => { - if (locks[dbName] == null) - throw Error(`Nitro SQLite Error: No lock found on db: ${dbName}`) + throwIfDatabaseIsNotOpen(dbName) let isFinalized = false @@ -23,8 +23,8 @@ export const transaction = ( params?: SQLiteQueryParams ): QueryResult => { if (isFinalized) { - throw Error( - `Nitro SQLite Error: Cannot execute query on finalized transaction: ${dbName}` + throw new NitroSQLiteError( + `Cannot execute query on finalized transaction: ${dbName}` ) } return execute(dbName, query, params) @@ -35,8 +35,8 @@ export const transaction = ( params?: SQLiteQueryParams ): Promise> => { if (isFinalized) { - throw Error( - `Nitro SQLite Error: Cannot execute query on finalized transaction: ${dbName}` + throw new NitroSQLiteError( + `Cannot execute query on finalized transaction: ${dbName}` ) } return executeAsync(dbName, query, params) @@ -44,8 +44,8 @@ export const transaction = ( const commit = () => { if (isFinalized) { - throw Error( - `Nitro SQLite Error: Cannot execute commit on finalized transaction: ${dbName}` + throw new NitroSQLiteError( + `Cannot execute commit on finalized transaction: ${dbName}` ) } const result = HybridNitroSQLite.execute(dbName, 'COMMIT') @@ -55,8 +55,8 @@ export const transaction = ( const rollback = () => { if (isFinalized) { - throw Error( - `Nitro SQLite Error: Cannot execute rollback on finalized transaction: ${dbName}` + throw new NitroSQLiteError( + `Cannot execute rollback on finalized transaction: ${dbName}` ) } const result = HybridNitroSQLite.execute(dbName, 'ROLLBACK') @@ -64,61 +64,34 @@ export const transaction = ( return result } - async function run() { - try { - await HybridNitroSQLite.executeAsync(dbName, 'BEGIN TRANSACTION') - - await fn({ - commit, - execute: executeOnTransaction, - executeAsync: executeAsyncOnTransaction, - rollback, - }) - - if (!isFinalized) commit() - } catch (executionError) { - if (!isFinalized) { - try { - rollback() - } catch (rollbackError) { - throw rollbackError + try { + return queueOperationAsync(dbName, async () => { + try { + await HybridNitroSQLite.executeAsync(dbName, 'BEGIN TRANSACTION') + + await fn({ + commit, + execute: executeOnTransaction, + executeAsync: executeAsyncOnTransaction, + rollback, + }) + + if (!isFinalized) commit() + } catch (executionError) { + if (!isFinalized) { + try { + rollback() + } catch (rollbackError) { + throw rollbackError + } } - } - - throw executionError - } finally { - locks[dbName]!.inProgress = false - isFinalized = false - startNextOperation(dbName) - } - } - - return new Promise((resolve, reject) => { - const queuedTransaction: QueuedOperation = { - start: () => { - run().then(resolve).catch(reject) - }, - } - locks[dbName]?.queue.push(queuedTransaction) - startNextOperation(dbName) - }) -} - -export function startNextOperation(dbName: string) { - if (locks[dbName] == null) throw Error(`Lock not found for db: ${dbName}`) - - if (locks[dbName].inProgress) { - // Operation is already in process bail out - return - } - - if (locks[dbName].queue.length > 0) { - locks[dbName].inProgress = true - - const operation = locks[dbName].queue.shift()! - setImmediate(() => { - operation.start() + throw executionError + } finally { + isFinalized = false + } }) + } catch (error) { + throw NitroSQLiteError.fromError(error) } } From d1d6f9996e8fb96da8604bdcd00f0230a83c1367 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 3 Nov 2025 17:23:49 +0000 Subject: [PATCH 05/28] refactor: rename databaseQueue variables --- package/src/DatabaseQueue.ts | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/package/src/DatabaseQueue.ts b/package/src/DatabaseQueue.ts index 1295cf9c..741a880c 100644 --- a/package/src/DatabaseQueue.ts +++ b/package/src/DatabaseQueue.ts @@ -25,9 +25,9 @@ export function openDatabaseQueue(dbName: string) { } export function closeDatabaseQueue(dbName: string) { - const queue = getDatabaseQueue(dbName) + const databaseQueue = getDatabaseQueue(dbName) - if (queue.inProgress || queue.queue.length > 0) { + if (databaseQueue.inProgress || databaseQueue.queue.length > 0) { console.warn( `Database queue for ${dbName} has operations in the queue. Closing anyway.` ) @@ -66,7 +66,7 @@ export function queueOperationAsync< OperationCallback extends () => Promise, Result = void, >(dbName: string, callback: OperationCallback) { - const queue = getDatabaseQueue(dbName) + const databaseQueue = getDatabaseQueue(dbName) return new Promise((resolve, reject) => { const operation: QueuedOperation = { @@ -77,13 +77,13 @@ export function queueOperationAsync< } catch (error) { reject(error) } finally { - queue.inProgress = false + databaseQueue.inProgress = false startOperationAsync(dbName) } }, } - queue.queue.push(operation) + databaseQueue.queue.push(operation) startOperationAsync(dbName) }) } @@ -91,38 +91,36 @@ export function queueOperationAsync< function startOperationAsync(dbName: string) { const queue = getDatabaseQueue(dbName) - if (queue.inProgress) { - // Operation is already in process bail out + // Queue is empty or in progress. Bail out. + if (queue.inProgress || queue.queue.length === 0) { return } - if (queue.queue.length > 0) { - queue.inProgress = true + queue.inProgress = true - const operation = queue.queue.shift()! - setImmediate(() => { - operation.start() - }) - } + const operation = queue.queue.shift()! + setImmediate(() => { + operation.start() + }) } export function startOperationSync< OperationCallback extends () => Result, Result = void, >(dbName: string, callback: OperationCallback) { - const queue = getDatabaseQueue(dbName) + const databaseQueue = getDatabaseQueue(dbName) // Database is busy - cannot execute synchronously - if (queue.inProgress || queue.queue.length > 0) { + if (databaseQueue.inProgress || databaseQueue.queue.length > 0) { throw new NitroSQLiteError( `Cannot run synchronous operation on database. Database ${dbName} is busy with another operation.` ) } // Execute synchronously - queue.inProgress = true + databaseQueue.inProgress = true const result = callback() - queue.inProgress = false + databaseQueue.inProgress = false return result } From 7f6e1ed6be6af8b40fbb76ea88e038ae26c5a788 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 3 Nov 2025 15:25:44 +0000 Subject: [PATCH 06/28] feat: add `isExcusive` parameter to `transaction` --- package/src/operations/transaction.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package/src/operations/transaction.ts b/package/src/operations/transaction.ts index 84b6e8b7..fbf2da1f 100644 --- a/package/src/operations/transaction.ts +++ b/package/src/operations/transaction.ts @@ -11,7 +11,8 @@ import NitroSQLiteError from '../NitroSQLiteError' export const transaction = ( dbName: string, - fn: (tx: Transaction) => Promise | void + fn: (tx: Transaction) => Promise | void, + isExclusive = false ): Promise => { throwIfDatabaseIsNotOpen(dbName) @@ -67,7 +68,10 @@ export const transaction = ( try { return queueOperationAsync(dbName, async () => { try { - await HybridNitroSQLite.executeAsync(dbName, 'BEGIN TRANSACTION') + await HybridNitroSQLite.executeAsync( + dbName, + isExclusive ? 'BEGIN EXCLUSIVE TRANSACTION' : 'BEGIN TRANSACTION' + ) await fn({ commit, From 9ec4284e8e3701c23336e9324d259059aad99dd4 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Nov 2025 12:46:57 +0000 Subject: [PATCH 07/28] fix: styling --- package/src/DatabaseQueue.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/package/src/DatabaseQueue.ts b/package/src/DatabaseQueue.ts index 741a880c..76a93978 100644 --- a/package/src/DatabaseQueue.ts +++ b/package/src/DatabaseQueue.ts @@ -17,7 +17,7 @@ const databaseQueues = new Map() export function openDatabaseQueue(dbName: string) { if (isDatabaseOpen(dbName)) { throw new NitroSQLiteError( - `Database ${dbName} is already open. There is already a connection to the database.` + `Database ${dbName} is already open. There is already a connection to the database.`, ) } @@ -29,7 +29,7 @@ export function closeDatabaseQueue(dbName: string) { if (databaseQueue.inProgress || databaseQueue.queue.length > 0) { console.warn( - `Database queue for ${dbName} has operations in the queue. Closing anyway.` + `Database queue for ${dbName} has operations in the queue. Closing anyway.`, ) } @@ -43,7 +43,7 @@ export function isDatabaseOpen(dbName: string) { export function throwIfDatabaseIsNotOpen(dbName: string) { if (!isDatabaseOpen(dbName)) throw new NitroSQLiteError( - `Database ${dbName} is not open. There is no connection to the database.` + `Database ${dbName} is not open. There is no connection to the database.`, ) } @@ -99,9 +99,7 @@ function startOperationAsync(dbName: string) { queue.inProgress = true const operation = queue.queue.shift()! - setImmediate(() => { - operation.start() - }) + setImmediate(operation.start) } export function startOperationSync< @@ -113,7 +111,7 @@ export function startOperationSync< // Database is busy - cannot execute synchronously if (databaseQueue.inProgress || databaseQueue.queue.length > 0) { throw new NitroSQLiteError( - `Cannot run synchronous operation on database. Database ${dbName} is busy with another operation.` + `Cannot run synchronous operation on database. Database ${dbName} is busy with another operation.`, ) } From 4fb573ba179158682144a4f27cfcfa58811c4dfb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Nov 2025 12:53:05 +0000 Subject: [PATCH 08/28] fix: handle errors in sync `executeBatch` --- package/src/DatabaseQueue.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package/src/DatabaseQueue.ts b/package/src/DatabaseQueue.ts index 76a93978..c93f07ba 100644 --- a/package/src/DatabaseQueue.ts +++ b/package/src/DatabaseQueue.ts @@ -117,8 +117,9 @@ export function startOperationSync< // Execute synchronously databaseQueue.inProgress = true - const result = callback() - databaseQueue.inProgress = false - - return result + try { + return callback() + } finally { + databaseQueue.inProgress = false + } } From dcc59f249de894963ba0f0119add7fd1394fd54d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 4 Nov 2025 15:24:14 +0000 Subject: [PATCH 09/28] fix: add NitroSQLite prefix to custom error --- package/src/NitroSQLiteError.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/package/src/NitroSQLiteError.ts b/package/src/NitroSQLiteError.ts index 6745bcd6..a193ecb3 100644 --- a/package/src/NitroSQLiteError.ts +++ b/package/src/NitroSQLiteError.ts @@ -1,4 +1,5 @@ -const NITRO_SQLITE_ERROR_NAME = 'NitroSQLiteError' +const NITRO_SQLITE_ERROR_NAME = 'NitroSQLiteError' as const +const NITRO_SQLITE_ERROR_PREFIX = '[NitroSQLite] ' as const /** * Custom error class for NitroSQLite operations @@ -8,6 +9,7 @@ export default class NitroSQLiteError extends Error { constructor(message: string, options?: ErrorOptions) { super(message, options) this.name = NITRO_SQLITE_ERROR_NAME + this.message = NITRO_SQLITE_ERROR_PREFIX + message // Maintains proper prototype chain for instanceof checks Object.setPrototypeOf(this, NitroSQLiteError.prototype) @@ -41,12 +43,4 @@ export default class NitroSQLiteError extends Error { cause: error, }) } - - /** - * Converts a native error (from C++ bridge) to a NitroSQLiteError - * Alias for fromError for semantic clarity - */ - static fromNativeError(error: unknown): NitroSQLiteError { - return NitroSQLiteError.fromError(error) - } } From a34dfe6b03177ebfefa65fb214108ba6c2667a03 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Nov 2025 13:33:34 +0000 Subject: [PATCH 10/28] refactor: remove type exports --- package/src/DatabaseQueue.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/src/DatabaseQueue.ts b/package/src/DatabaseQueue.ts index c93f07ba..5d8bc057 100644 --- a/package/src/DatabaseQueue.ts +++ b/package/src/DatabaseQueue.ts @@ -1,13 +1,13 @@ import NitroSQLiteError from './NitroSQLiteError' -export interface QueuedOperation { +interface QueuedOperation { /** * Starts the operation */ start: () => void } -export type DatabaseQueue = { +type DatabaseQueue = { queue: QueuedOperation[] inProgress: boolean } From db99dad5ac14ba60c53f6c3fd3cb4b53fcffd1a3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Nov 2025 19:09:41 +0000 Subject: [PATCH 11/28] fix: database queue promise chaining --- package/src/DatabaseQueue.ts | 36 +++++++++++++++------------ package/src/operations/transaction.ts | 32 +++++++++++------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/package/src/DatabaseQueue.ts b/package/src/DatabaseQueue.ts index 5d8bc057..184560a5 100644 --- a/package/src/DatabaseQueue.ts +++ b/package/src/DatabaseQueue.ts @@ -62,25 +62,27 @@ export function closeDatabase(dbName: string) { databaseQueues.delete(dbName) } -export function queueOperationAsync< - OperationCallback extends () => Promise, - Result = void, ->(dbName: string, callback: OperationCallback) { +export function queueOperationAsync( + dbName: string, + callback: () => Promise, +) { const databaseQueue = getDatabaseQueue(dbName) return new Promise((resolve, reject) => { + async function start() { + try { + const result = await callback() + resolve(result) + } catch (error) { + reject(error) + } finally { + databaseQueue.inProgress = false + startOperationAsync(dbName) + } + } + const operation: QueuedOperation = { - start: async () => { - try { - const result = await callback() - resolve(result) - } catch (error) { - reject(error) - } finally { - databaseQueue.inProgress = false - startOperationAsync(dbName) - } - }, + start, } databaseQueue.queue.push(operation) @@ -99,7 +101,9 @@ function startOperationAsync(dbName: string) { queue.inProgress = true const operation = queue.queue.shift()! - setImmediate(operation.start) + setImmediate(() => { + operation.start() + }) } export function startOperationSync< diff --git a/package/src/operations/transaction.ts b/package/src/operations/transaction.ts index fd35e580..02895a3c 100644 --- a/package/src/operations/transaction.ts +++ b/package/src/operations/transaction.ts @@ -1,4 +1,3 @@ -import { HybridNitroSQLite } from '../nitro' import { queueOperationAsync, throwIfDatabaseIsNotOpen } from '../DatabaseQueue' import type { QueryResult, @@ -9,20 +8,19 @@ import type { import { execute, executeAsync } from './execute' import NitroSQLiteError from '../NitroSQLiteError' -export const transaction = ( +export const transaction = async ( dbName: string, - fn: (tx: Transaction) => Promise | void, + transactionCallback: (tx: Transaction) => Promise, isExclusive = false, -): Promise => { +) => { throwIfDatabaseIsNotOpen(dbName) let isFinalized = false - // Local transaction context object implementation - const executeOnTransaction = ( + const executeOnTransaction = ( query: string, params?: SQLiteQueryParams, - ): QueryResult => { + ): QueryResult => { if (isFinalized) { throw new NitroSQLiteError( `Cannot execute query on finalized transaction: ${dbName}`, @@ -31,10 +29,10 @@ export const transaction = ( return execute(dbName, query, params) } - const executeAsyncOnTransaction = ( + const executeAsyncOnTransaction = ( query: string, params?: SQLiteQueryParams, - ): Promise> => { + ): Promise> => { if (isFinalized) { throw new NitroSQLiteError( `Cannot execute query on finalized transaction: ${dbName}`, @@ -49,9 +47,8 @@ export const transaction = ( `Cannot execute commit on finalized transaction: ${dbName}`, ) } - const result = HybridNitroSQLite.execute(dbName, 'COMMIT') isFinalized = true - return result + return execute(dbName, 'COMMIT') } const rollback = () => { @@ -60,20 +57,19 @@ export const transaction = ( `Cannot execute rollback on finalized transaction: ${dbName}`, ) } - const result = HybridNitroSQLite.execute(dbName, 'ROLLBACK') isFinalized = true - return result + return execute(dbName, 'ROLLBACK') } try { - return queueOperationAsync(dbName, async () => { + return await queueOperationAsync(dbName, async () => { try { - await HybridNitroSQLite.executeAsync( + await executeAsync( dbName, isExclusive ? 'BEGIN EXCLUSIVE TRANSACTION' : 'BEGIN TRANSACTION', ) - await fn({ + const result = await transactionCallback({ commit, execute: executeOnTransaction, executeAsync: executeAsyncOnTransaction, @@ -81,6 +77,8 @@ export const transaction = ( }) if (!isFinalized) commit() + + return result } catch (executionError) { if (!isFinalized) { try { @@ -91,8 +89,6 @@ export const transaction = ( } throw executionError - } finally { - isFinalized = false } }) } catch (error) { From 3730594560218e3f872d208fbb7e7219029b6c82 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Nov 2025 19:09:59 +0000 Subject: [PATCH 12/28] refactor: remove redundant session functions --- package/src/operations/session.ts | 36 +++++++++++++------------------ 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/package/src/operations/session.ts b/package/src/operations/session.ts index 69ce9dd5..da5c3536 100644 --- a/package/src/operations/session.ts +++ b/package/src/operations/session.ts @@ -17,15 +17,27 @@ import { closeDatabaseQueue, openDatabaseQueue } from '../DatabaseQueue' export function open( options: NitroSQLiteConnectionOptions, ): NitroSQLiteConnection { - openDb(options.name, options.location) + try { + HybridNitroSQLite.open(options.name, options.location) + openDatabaseQueue(options.name) + } catch (error) { + throw NitroSQLiteError.fromError(error) + } return { - close: () => close(options.name), + close: () => { + try { + HybridNitroSQLite.close(options.name) + closeDatabaseQueue(options.name) + } catch (error) { + throw NitroSQLiteError.fromError(error) + } + }, delete: () => HybridNitroSQLite.drop(options.name, options.location), attach: (dbNameToAttach: string, alias: string, location?: string) => HybridNitroSQLite.attach(options.name, dbNameToAttach, alias, location), detach: (alias: string) => HybridNitroSQLite.detach(options.name, alias), - transaction: (fn: (tx: Transaction) => Promise | void) => + transaction: (fn: (tx: Transaction) => Promise) => transaction(options.name, fn), execute: ( query: string, @@ -45,21 +57,3 @@ export function open( HybridNitroSQLite.loadFileAsync(options.name, location), } } - -export function openDb(dbName: string, location?: string) { - try { - HybridNitroSQLite.open(dbName, location) - openDatabaseQueue(dbName) - } catch (error) { - throw NitroSQLiteError.fromError(error) - } -} - -export function close(dbName: string) { - try { - HybridNitroSQLite.close(dbName) - closeDatabaseQueue(dbName) - } catch (error) { - throw NitroSQLiteError.fromError(error) - } -} From 943bd189529e3a9fafe385f3254bd0dc6e80eb3a Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Nov 2025 20:08:28 +0000 Subject: [PATCH 13/28] refactor: move db name to constants --- example/src/tests/db.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/example/src/tests/db.ts b/example/src/tests/db.ts index 6e4e354c..5fe59b4a 100644 --- a/example/src/tests/db.ts +++ b/example/src/tests/db.ts @@ -7,6 +7,8 @@ import { open } from 'react-native-nitro-sqlite' const chance = new Chance() +export const TEST_DB_NAME = 'test' + export let testDb: NitroSQLiteConnection | undefined export function resetTestDb() { try { @@ -15,13 +17,15 @@ export function resetTestDb() { testDb.delete() } testDb = open({ - name: 'test', + name: TEST_DB_NAME, }) } catch (e) { console.warn('Error resetting user database', e) } } +const LARGE_DB_NAME = 'large' + // Copyright 2024 Oscar Franco // Taken from "op-sqlite" example project. // Used to demonstrate the performance of NitroSQLite. @@ -34,7 +38,7 @@ export function resetLargeDb() { largeDb.delete() } largeDb = open({ - name: 'large', + name: LARGE_DB_NAME, }) largeDb.execute( From d3ab2b4efbcb814317fadffebb5bf3bfcfb3ec3c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Nov 2025 22:33:37 +0000 Subject: [PATCH 14/28] chore: add package sources to example ts files --- example/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/tsconfig.json b/example/tsconfig.json index 3b967a5e..b9d76a7d 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../config/tsconfig.json", - "include": ["src", "index.js"] + "include": ["src", "index.js", "../package"] } From 6d356f16f7fab0ef5b74663d1285d6ac1697dff9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Nov 2025 23:10:53 +0000 Subject: [PATCH 15/28] refactor: restructure test db setup --- example/src/tests/db.ts | 14 ++++++++++- example/src/tests/unit/common.ts | 25 +++---------------- example/src/tests/unit/specs/execute.spec.ts | 3 ++- .../src/tests/unit/specs/executeBatch.spec.ts | 3 ++- .../src/tests/unit/specs/transaction.spec.ts | 3 ++- package/src/DatabaseQueue.ts | 6 ++--- 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/example/src/tests/db.ts b/example/src/tests/db.ts index 5fe59b4a..c02a67c8 100644 --- a/example/src/tests/db.ts +++ b/example/src/tests/db.ts @@ -4,21 +4,33 @@ import type { BatchQueryCommand, } from 'react-native-nitro-sqlite' import { open } from 'react-native-nitro-sqlite' +import { + getDatabaseQueue, + type DatabaseQueue, +} from '../../../package/src/DatabaseQueue' const chance = new Chance() export const TEST_DB_NAME = 'test' -export let testDb: NitroSQLiteConnection | undefined +export let testDb: NitroSQLiteConnection +export let testDbQueue: DatabaseQueue export function resetTestDb() { try { if (testDb != null) { testDb.close() testDb.delete() } + testDb = open({ name: TEST_DB_NAME, }) + testDbQueue = getDatabaseQueue(TEST_DB_NAME) + + testDb.execute('DROP TABLE IF EXISTS User;') + testDb.execute( + 'CREATE TABLE User ( id REAL PRIMARY KEY, name TEXT NOT NULL, age REAL, networth REAL) STRICT;', + ) } catch (e) { console.warn('Error resetting user database', e) } diff --git a/example/src/tests/unit/common.ts b/example/src/tests/unit/common.ts index 4d78cba5..a8ae9737 100644 --- a/example/src/tests/unit/common.ts +++ b/example/src/tests/unit/common.ts @@ -1,9 +1,6 @@ import { Chance } from 'chance' -import { - NitroSQLiteConnection, - enableSimpleNullHandling, -} from 'react-native-nitro-sqlite' -import { testDb as testDbInternal, resetTestDb } from '../db' +import { enableSimpleNullHandling } from 'react-native-nitro-sqlite' +import { resetTestDb } from '../db' import chai from 'chai' export function isError(e: unknown): e is Error { @@ -13,23 +10,7 @@ export function isError(e: unknown): e is Error { export const expect = chai.expect export const chance = new Chance() -export let testDb: NitroSQLiteConnection - export function setupTestDb() { enableSimpleNullHandling(false) - - try { - resetTestDb() - - if (testDbInternal == null) throw new Error('Failed to reset test database') - - testDbInternal.execute('DROP TABLE IF EXISTS User;') - testDbInternal.execute( - 'CREATE TABLE User ( id REAL PRIMARY KEY, name TEXT NOT NULL, age REAL, networth REAL) STRICT;', - ) - - testDb = testDbInternal! - } catch (e) { - console.warn('Error resetting user database', e) - } + resetTestDb() } diff --git a/example/src/tests/unit/specs/execute.spec.ts b/example/src/tests/unit/specs/execute.spec.ts index 85e1269f..dc8c005d 100644 --- a/example/src/tests/unit/specs/execute.spec.ts +++ b/example/src/tests/unit/specs/execute.spec.ts @@ -1,9 +1,10 @@ -import { chance, expect, isError, testDb } from '../common' +import { chance, expect, isError } from '../common' import { enableSimpleNullHandling, NITRO_SQLITE_NULL, } from 'react-native-nitro-sqlite' import { describe, it } from '../../MochaRNAdapter' +import { testDb } from '../../db' export default function registerExecuteUnitTests() { describe('execute', () => { diff --git a/example/src/tests/unit/specs/executeBatch.spec.ts b/example/src/tests/unit/specs/executeBatch.spec.ts index f5d776d0..200acbd5 100644 --- a/example/src/tests/unit/specs/executeBatch.spec.ts +++ b/example/src/tests/unit/specs/executeBatch.spec.ts @@ -1,6 +1,7 @@ -import { chance, expect, testDb } from '../common' +import { chance, expect } from '../common' import type { BatchQueryCommand } from 'react-native-nitro-sqlite' import { describe, it } from '../../MochaRNAdapter' +import { testDb } from '../../db' export default function registerExecuteBatchUnitTests() { describe('executeBatch', () => { diff --git a/example/src/tests/unit/specs/transaction.spec.ts b/example/src/tests/unit/specs/transaction.spec.ts index 5c714cdc..12f3c5ab 100644 --- a/example/src/tests/unit/specs/transaction.spec.ts +++ b/example/src/tests/unit/specs/transaction.spec.ts @@ -1,6 +1,7 @@ -import { chance, expect, isError, testDb } from '../common' +import { chance, expect, isError } from '../common' import { describe, it } from '../../MochaRNAdapter' import type { User } from '../../../model/User' +import { testDb, testDbQueue } from '../../db' export default function registerTransactionUnitTests() { describe('transaction', () => { diff --git a/package/src/DatabaseQueue.ts b/package/src/DatabaseQueue.ts index 184560a5..bd485a61 100644 --- a/package/src/DatabaseQueue.ts +++ b/package/src/DatabaseQueue.ts @@ -1,13 +1,13 @@ import NitroSQLiteError from './NitroSQLiteError' -interface QueuedOperation { +export interface QueuedOperation { /** * Starts the operation */ start: () => void } -type DatabaseQueue = { +export type DatabaseQueue = { queue: QueuedOperation[] inProgress: boolean } @@ -47,7 +47,7 @@ export function throwIfDatabaseIsNotOpen(dbName: string) { ) } -function getDatabaseQueue(dbName: string) { +export function getDatabaseQueue(dbName: string) { throwIfDatabaseIsNotOpen(dbName) const queue = databaseQueues.get(dbName)! From 16a4a9c24e96da4825d2f78a1feade1fbf48981b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 5 Nov 2025 23:11:27 +0000 Subject: [PATCH 16/28] feat: add prefix to native C++ exception --- package/cpp/NitroSQLiteException.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package/cpp/NitroSQLiteException.hpp b/package/cpp/NitroSQLiteException.hpp index 636d9163..5fad007c 100644 --- a/package/cpp/NitroSQLiteException.hpp +++ b/package/cpp/NitroSQLiteException.hpp @@ -4,6 +4,8 @@ #include #include +const std::string NITRO_SQLITE_EXCEPTION_PREFIX = "[NitroSQLiteException]"; + enum NitroSQLiteExceptionType { UnknownError, DatabaseCannotBeOpened, @@ -33,7 +35,7 @@ class NitroSQLiteException : public std::exception { explicit NitroSQLiteException(const std::string& message) : NitroSQLiteException(NitroSQLiteExceptionType::UnknownError, message) {} NitroSQLiteException(const NitroSQLiteExceptionType& type, const char* message) : NitroSQLiteException(type, std::string(message)) {} NitroSQLiteException(const NitroSQLiteExceptionType& type, const std::string& message) - : _exceptionString("[" + typeToString(type) + "] " + message) {} + : _exceptionString(NITRO_SQLITE_EXCEPTION_PREFIX + " [" + typeToString(type) + "] " + message) {} private: const std::string _exceptionString; From f867ac4ab5fd0201c58ed4b19c34fce80177c577 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 00:02:57 +0000 Subject: [PATCH 17/28] fix: add back line --- package/src/NitroSQLiteError.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/package/src/NitroSQLiteError.ts b/package/src/NitroSQLiteError.ts index cdb78de8..8456eedd 100644 --- a/package/src/NitroSQLiteError.ts +++ b/package/src/NitroSQLiteError.ts @@ -26,6 +26,7 @@ export default class NitroSQLiteError extends Error { const nitroSQLiteError = new NitroSQLiteError(error.message, { cause: error.cause, }) + // Preserve original stack trace if available if (error.stack) { nitroSQLiteError.stack = error.stack From dc25ef314cd812518aceb411c8ce42b9de03c942 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 00:21:55 +0000 Subject: [PATCH 18/28] fix: transaction rollback result --- package/src/operations/transaction.ts | 3 ++- package/src/types.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/package/src/operations/transaction.ts b/package/src/operations/transaction.ts index 91864be5..18c76d94 100644 --- a/package/src/operations/transaction.ts +++ b/package/src/operations/transaction.ts @@ -84,7 +84,8 @@ export const transaction = async ( } try { - return rollback() + rollback() + return undefined as Result } catch (rollbackError) { throw NitroSQLiteError.fromError(rollbackError) } diff --git a/package/src/types.ts b/package/src/types.ts index 26e80f1b..4045349d 100644 --- a/package/src/types.ts +++ b/package/src/types.ts @@ -8,7 +8,9 @@ export interface NitroSQLiteConnection { delete(): void attach(dbNameToAttach: string, alias: string, location?: string): void detach(alias: string): void - transaction(fn: (tx: Transaction) => Promise | void): Promise + transaction( + fn: (tx: Transaction) => Promise, + ): Promise execute: ExecuteQuery executeAsync: ExecuteAsyncQuery executeBatch(commands: BatchQueryCommand[]): BatchQueryResult From 4606806e91c07b512190974723bec09f0deb5c60 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 00:30:18 +0000 Subject: [PATCH 19/28] test: add transaction queuing test --- .../src/tests/unit/specs/transaction.spec.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/example/src/tests/unit/specs/transaction.spec.ts b/example/src/tests/unit/specs/transaction.spec.ts index 12f3c5ab..21c91ae1 100644 --- a/example/src/tests/unit/specs/transaction.spec.ts +++ b/example/src/tests/unit/specs/transaction.spec.ts @@ -431,5 +431,50 @@ export default function registerTransactionUnitTests() { else expect.fail('Should have thrown a valid NitroSQLiteException') } }) + + it('transaction are queued', async () => { + const transaction1Promise = testDb.transaction(async (tx) => { + tx.execute('SELECT * FROM [User];') + + expect(testDbQueue.queue.length).to.equal(2) + expect(testDbQueue.inProgress).to.equal(true) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + tx.execute('SELECT * FROM [User];') + + expect(testDbQueue.queue.length).to.equal(2) + expect(testDbQueue.inProgress).to.equal(true) + }) + + expect(testDbQueue.inProgress).to.equal(true) + expect(testDbQueue.queue.length).to.equal(0) + + const transaction2Promise = testDb.transaction(async (tx) => { + tx.execute('SELECT * FROM [User];') + }) + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + const transaction3Promise = testDb.transaction(async (tx) => { + tx.execute('SELECT * FROM [User];') + }) + + await transaction1Promise + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + await transaction2Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(true) + + await transaction3Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(false) + }) }) } From e26d85e8f3a721619e1917caaa87eb110dcb2713 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 00:32:01 +0000 Subject: [PATCH 20/28] feat: adapt tests to new error types --- example/src/tests/unit/common.ts | 9 +++-- example/src/tests/unit/specs/execute.spec.ts | 4 +- .../src/tests/unit/specs/transaction.spec.ts | 39 +++++++++++-------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/example/src/tests/unit/common.ts b/example/src/tests/unit/common.ts index a8ae9737..d1da4862 100644 --- a/example/src/tests/unit/common.ts +++ b/example/src/tests/unit/common.ts @@ -1,10 +1,13 @@ import { Chance } from 'chance' -import { enableSimpleNullHandling } from 'react-native-nitro-sqlite' +import { + enableSimpleNullHandling, + NitroSQLiteError, +} from 'react-native-nitro-sqlite' import { resetTestDb } from '../db' import chai from 'chai' -export function isError(e: unknown): e is Error { - return e instanceof Error +export function isNitroSQLiteError(e: unknown): e is NitroSQLiteError { + return e instanceof NitroSQLiteError } export const expect = chai.expect diff --git a/example/src/tests/unit/specs/execute.spec.ts b/example/src/tests/unit/specs/execute.spec.ts index dc8c005d..721104b9 100644 --- a/example/src/tests/unit/specs/execute.spec.ts +++ b/example/src/tests/unit/specs/execute.spec.ts @@ -1,4 +1,4 @@ -import { chance, expect, isError } from '../common' +import { chance, expect, isNitroSQLiteError } from '../common' import { enableSimpleNullHandling, NITRO_SQLITE_NULL, @@ -94,7 +94,7 @@ export default function registerExecuteUnitTests() { [id, name, age, networth], ) } catch (e: unknown) { - if (isError(e)) { + if (isNitroSQLiteError(e)) { expect(e.message).to.include( 'cannot store TEXT value in REAL column User.age', ) diff --git a/example/src/tests/unit/specs/transaction.spec.ts b/example/src/tests/unit/specs/transaction.spec.ts index 21c91ae1..2453310d 100644 --- a/example/src/tests/unit/specs/transaction.spec.ts +++ b/example/src/tests/unit/specs/transaction.spec.ts @@ -1,8 +1,11 @@ -import { chance, expect, isError } from '../common' +import { chance, expect, isNitroSQLiteError } from '../common' import { describe, it } from '../../MochaRNAdapter' import type { User } from '../../../model/User' import { testDb, testDbQueue } from '../../db' +const DUMMY_ERROR_NAME = 'Transaction Rejection Error' +const DUMMY_ERROR_MESSAGE = 'Error from callback' + export default function registerTransactionUnitTests() { describe('transaction', () => { it('Transaction, auto commit', async () => { @@ -199,17 +202,18 @@ export default function registerTransactionUnitTests() { it('Transaction, rejects on callback error', async () => { const promised = testDb.transaction(() => { - throw new Error('Error from callback') + throw new Error(DUMMY_ERROR_MESSAGE) }) // ASSERT: should return a promise that eventually rejects expect(promised).to.have.property('then').that.is.a('function') try { await promised - expect.fail('Should not resolve') + expect.fail(DUMMY_ERROR_NAME) } catch (e) { - if (isError(e)) expect(e.message).to.equal('Error from callback') - else expect.fail('Should have thrown a valid NitroSQLiteException') + if (isNitroSQLiteError(e)) + expect(e.message).to.include(DUMMY_ERROR_MESSAGE) + else expect.fail('Should have thrown a valid NitroSQLiteError') } }) @@ -221,11 +225,11 @@ export default function registerTransactionUnitTests() { expect(promised).to.have.property('then').that.is.a('function') try { await promised - expect.fail('Should not resolve') + expect.fail(DUMMY_ERROR_NAME) } catch (e) { - if (isError(e)) + if (isNitroSQLiteError(e)) expect(e.message).to.include('no such table: tableThatDoesNotExist') - else expect.fail('Should have thrown a valid NitroSQLiteException') + else expect.fail('Should have thrown a valid NitroSQLiteError') } }) @@ -289,7 +293,7 @@ export default function registerTransactionUnitTests() { ) }) } catch (e) { - if (isError(e)) { + if (isNitroSQLiteError(e)) { expect(e.message) .to.include('SqlExecutionError') .and.to.include('cannot store TEXT value in REAL column User.id') @@ -297,7 +301,7 @@ export default function registerTransactionUnitTests() { const res = testDb.execute('SELECT * FROM User') expect(res.rows?._array).to.eql([]) } else { - expect.fail('Should have thrown a valid NitroSQLiteException') + expect.fail('Should have thrown a valid NitroSQLiteError') } } }) @@ -401,17 +405,18 @@ export default function registerTransactionUnitTests() { it('Async transaction, rejects on callback error', async () => { const promised = testDb.transaction(() => { - throw new Error('Error from callback') + throw new Error(DUMMY_ERROR_MESSAGE) }) // ASSERT: should return a promise that eventually rejects expect(promised).to.have.property('then').that.is.a('function') try { await promised - expect.fail('Should not resolve') + expect.fail(DUMMY_ERROR_NAME) } catch (e) { - if (isError(e)) expect(e.message).to.equal('Error from callback') - else expect.fail('Should have thrown a valid NitroSQLiteException') + if (isNitroSQLiteError(e)) + expect(e.message).to.include(DUMMY_ERROR_MESSAGE) + else expect.fail('Should have thrown a valid NitroSQLiteError') } }) @@ -424,11 +429,11 @@ export default function registerTransactionUnitTests() { expect(promised).to.have.property('then').that.is.a('function') try { await promised - expect.fail('Should not resolve') + expect.fail(DUMMY_ERROR_NAME) } catch (e) { - if (isError(e)) + if (isNitroSQLiteError(e)) expect(e.message).to.include('no such table: tableThatDoesNotExist') - else expect.fail('Should have thrown a valid NitroSQLiteException') + else expect.fail('Should have thrown a valid NitroSQLiteError') } }) From a43d4b707c430da0f59b3fd24c9dd40148d6cddc Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 00:32:16 +0000 Subject: [PATCH 21/28] fix: make all transaction callbacks async --- example/src/tests/unit/specs/transaction.spec.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/example/src/tests/unit/specs/transaction.spec.ts b/example/src/tests/unit/specs/transaction.spec.ts index 2453310d..d5e21f6f 100644 --- a/example/src/tests/unit/specs/transaction.spec.ts +++ b/example/src/tests/unit/specs/transaction.spec.ts @@ -14,7 +14,7 @@ export default function registerTransactionUnitTests() { const age = chance.integer() const networth = chance.floating() - await testDb.transaction((tx) => { + await testDb.transaction(async (tx) => { const res = tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], @@ -44,7 +44,7 @@ export default function registerTransactionUnitTests() { const age = chance.integer() const networth = chance.floating() - await testDb.transaction((tx) => { + await testDb.transaction(async (tx) => { const res = tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], @@ -83,7 +83,7 @@ export default function registerTransactionUnitTests() { // ACT: Start multiple transactions to upsert and select the same record const promises = [] for (let iteration = 1; iteration <= iterations; iteration++) { - const promised = testDb.transaction((tx) => { + const promised = testDb.transaction(async (tx) => { // ACT: Upsert statement to create record / increment the value tx.execute( ` @@ -130,7 +130,7 @@ export default function registerTransactionUnitTests() { const age = chance.integer() const networth = chance.floating() - await testDb.transaction((tx) => { + await testDb.transaction(async (tx) => { const res = tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], @@ -168,7 +168,7 @@ export default function registerTransactionUnitTests() { const age = chance.integer() const networth = chance.floating() - await testDb.transaction((tx) => { + await testDb.transaction(async (tx) => { try { tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', @@ -189,7 +189,7 @@ export default function registerTransactionUnitTests() { const age = chance.integer() const networth = chance.floating() - await testDb.transaction((tx) => { + await testDb.transaction(async (tx) => { tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], @@ -211,6 +211,7 @@ export default function registerTransactionUnitTests() { await promised expect.fail(DUMMY_ERROR_NAME) } catch (e) { + console.log(e) if (isNitroSQLiteError(e)) expect(e.message).to.include(DUMMY_ERROR_MESSAGE) else expect.fail('Should have thrown a valid NitroSQLiteError') @@ -218,7 +219,7 @@ export default function registerTransactionUnitTests() { }) it('Transaction, rejects on invalid query', async () => { - const promised = testDb.transaction((tx) => { + const promised = testDb.transaction(async (tx) => { tx.execute('SELECT * FROM [tableThatDoesNotExist];') }) // ASSERT: should return a promise that eventually rejects From 3f361f7427804f518ab2a8fb0c0c4f7f378e6b89 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 00:32:41 +0000 Subject: [PATCH 22/28] refactor: export NitroSQLiteError class from package --- package/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package/src/index.ts b/package/src/index.ts index e3f31db9..9f350d5a 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -4,8 +4,6 @@ import { open } from './operations/session' import { execute, executeAsync } from './operations/execute' import { init } from './OnLoad' import { executeBatch, executeBatchAsync } from './operations/executeBatch' -export type * from './types' -export { typeORMDriver } from './typeORM' init() @@ -30,3 +28,6 @@ export { isSimpleNullHandlingEnabled, enableSimpleNullHandling, } from './nullHandling' +export { default as NitroSQLiteError } from './NitroSQLiteError' +export type * from './types' +export { typeORMDriver } from './typeORM' From f6d80e7116d02d4b7dbce4de479e1dab40b43482 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 00:33:02 +0000 Subject: [PATCH 23/28] fix: transaction type --- package/src/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package/src/types.ts b/package/src/types.ts index 4045349d..dd11c308 100644 --- a/package/src/types.ts +++ b/package/src/types.ts @@ -8,9 +8,9 @@ export interface NitroSQLiteConnection { delete(): void attach(dbNameToAttach: string, alias: string, location?: string): void detach(alias: string): void - transaction( - fn: (tx: Transaction) => Promise, - ): Promise + transaction: ( + transactionCallback: (tx: Transaction) => Promise, + ) => Promise execute: ExecuteQuery executeAsync: ExecuteAsyncQuery executeBatch(commands: BatchQueryCommand[]): BatchQueryResult From e0a84b50fa59bb276eec0ca75442d12ad7c598b2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 13:47:32 +0000 Subject: [PATCH 24/28] fix: make another transaction callback async --- example/src/tests/unit/specs/transaction.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/tests/unit/specs/transaction.spec.ts b/example/src/tests/unit/specs/transaction.spec.ts index d5e21f6f..30cc6ead 100644 --- a/example/src/tests/unit/specs/transaction.spec.ts +++ b/example/src/tests/unit/specs/transaction.spec.ts @@ -201,7 +201,7 @@ export default function registerTransactionUnitTests() { }) it('Transaction, rejects on callback error', async () => { - const promised = testDb.transaction(() => { + const promised = testDb.transaction(async () => { throw new Error(DUMMY_ERROR_MESSAGE) }) From 72cfc74b1015658092fcae74fd588c73d0751db2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 14:23:09 +0000 Subject: [PATCH 25/28] fix: always throw error on rollback --- package/src/operations/transaction.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/package/src/operations/transaction.ts b/package/src/operations/transaction.ts index 18c76d94..3ce6440c 100644 --- a/package/src/operations/transaction.ts +++ b/package/src/operations/transaction.ts @@ -79,16 +79,15 @@ export const transaction = async ( return result } catch (executionError) { - if (isFinalized) { - throw NitroSQLiteError.fromError(executionError) + if (!isFinalized) { + try { + rollback() + } catch (rollbackError) { + throw NitroSQLiteError.fromError(rollbackError) + } } - try { - rollback() - return undefined as Result - } catch (rollbackError) { - throw NitroSQLiteError.fromError(rollbackError) - } + throw NitroSQLiteError.fromError(executionError) } }) } From 91abc207a4ac12515bd2098b7f03883348e1e971 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 14:23:43 +0000 Subject: [PATCH 26/28] refactor: rename variable --- package/src/operations/transaction.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package/src/operations/transaction.ts b/package/src/operations/transaction.ts index 3ce6440c..ec6a6d0b 100644 --- a/package/src/operations/transaction.ts +++ b/package/src/operations/transaction.ts @@ -15,13 +15,13 @@ export const transaction = async ( ) => { throwIfDatabaseIsNotOpen(dbName) - let isFinalized = false + let isFinished = false const executeOnTransaction = ( query: string, params?: SQLiteQueryParams, ): QueryResult => { - if (isFinalized) { + if (isFinished) { throw new NitroSQLiteError( `Cannot execute query on finalized transaction: ${dbName}`, ) @@ -33,7 +33,7 @@ export const transaction = async ( query: string, params?: SQLiteQueryParams, ): Promise> => { - if (isFinalized) { + if (isFinished) { throw new NitroSQLiteError( `Cannot execute query on finalized transaction: ${dbName}`, ) @@ -42,22 +42,22 @@ export const transaction = async ( } const commit = () => { - if (isFinalized) { + if (isFinished) { throw new NitroSQLiteError( `Cannot execute commit on finalized transaction: ${dbName}`, ) } - isFinalized = true + isFinished = true return execute(dbName, 'COMMIT') } const rollback = () => { - if (isFinalized) { + if (isFinished) { throw new NitroSQLiteError( `Cannot execute rollback on finalized transaction: ${dbName}`, ) } - isFinalized = true + isFinished = true return execute(dbName, 'ROLLBACK') } @@ -75,11 +75,11 @@ export const transaction = async ( rollback, }) - if (!isFinalized) commit() + if (!isFinished) commit() return result } catch (executionError) { - if (!isFinalized) { + if (!isFinished) { try { rollback() } catch (rollbackError) { From ad583bdd2c00da6b0717edef42727868db81a5e6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 14:38:43 +0000 Subject: [PATCH 27/28] test: add more DatabaseQueue tests --- .../tests/unit/specs/DatabaseQueue.spec.ts | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 example/src/tests/unit/specs/DatabaseQueue.spec.ts diff --git a/example/src/tests/unit/specs/DatabaseQueue.spec.ts b/example/src/tests/unit/specs/DatabaseQueue.spec.ts new file mode 100644 index 00000000..1d36a8f2 --- /dev/null +++ b/example/src/tests/unit/specs/DatabaseQueue.spec.ts @@ -0,0 +1,182 @@ +import { + expect, + isNitroSQLiteError, + TEST_ERROR, + TEST_ERROR_CODES, + TEST_ERROR_MESSAGE, +} from '../common' +import { describe, it } from '../../MochaRNAdapter' +import { testDb, testDbQueue } from '../../db' +import type { BatchQueryCommand } from 'react-native-nitro-sqlite' + +const TEST_QUERY = 'SELECT * FROM [User];' + +const TEST_BATCH_COMMANDS: BatchQueryCommand[] = [{ query: TEST_QUERY }] + +export default function registerDatabaseQueueUnitTests() { + describe('Database Queue', () => { + it('multiple transactions are queued', async () => { + const transaction1Promise = testDb.transaction(async (tx) => { + tx.execute(TEST_QUERY) + + expect(testDbQueue.queue.length).to.equal(2) + expect(testDbQueue.inProgress).to.equal(true) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + tx.execute(TEST_QUERY) + + expect(testDbQueue.queue.length).to.equal(2) + expect(testDbQueue.inProgress).to.equal(true) + }) + + expect(testDbQueue.inProgress).to.equal(true) + expect(testDbQueue.queue.length).to.equal(0) + + const transaction2Promise = testDb.transaction(async (tx) => { + tx.execute(TEST_QUERY) + }) + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + const transaction3Promise = testDb.transaction(async (tx) => { + tx.execute(TEST_QUERY) + }) + + await transaction1Promise + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + await transaction2Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(true) + + await transaction3Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(false) + }) + + it('multiple executeBatchAsync operations are queued', async () => { + const executeBatch1Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS) + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(true) + + const executeBatch2Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS) + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + const executeBatch3Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS) + + expect(testDbQueue.queue.length).to.equal(2) + expect(testDbQueue.inProgress).to.equal(true) + + await executeBatch1Promise + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + await executeBatch2Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(true) + + await executeBatch3Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(false) + }) + + it('mixed transactions and executeBatchAsync operations are queued', async () => { + const transaction1Promise = testDb.transaction(async (tx) => { + tx.execute('SELECT * FROM [User];') + }) + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(true) + + const executeBatch1Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS) + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + const transaction2Promise = testDb.transaction(async (tx) => { + tx.execute(TEST_QUERY) + }) + + expect(testDbQueue.queue.length).to.equal(2) + expect(testDbQueue.inProgress).to.equal(true) + + const executeBatch2Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS) + + expect(testDbQueue.queue.length).to.equal(3) + expect(testDbQueue.inProgress).to.equal(true) + + await transaction1Promise + + expect(testDbQueue.queue.length).to.equal(2) + expect(testDbQueue.inProgress).to.equal(true) + + await executeBatch1Promise + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + await transaction2Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(true) + + await executeBatch2Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(false) + }) + + it('errors are thrown through DatabaseQueue', async () => { + const transaction1Promise = testDb.transaction(async (tx) => { + tx.execute('SELECT * FROM [User];') + throw TEST_ERROR + }) + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(true) + + const executeBatch1Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS) + + expect(testDbQueue.queue.length).to.equal(1) + expect(testDbQueue.inProgress).to.equal(true) + + try { + await transaction1Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(true) + } catch (e) { + if (isNitroSQLiteError(e)) { + expect(e.message).to.include(TEST_ERROR_MESSAGE) + } else { + expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR) + } + } + + try { + await executeBatch1Promise + + expect(testDbQueue.queue.length).to.equal(0) + expect(testDbQueue.inProgress).to.equal(false) + } catch (e) { + if (isNitroSQLiteError(e)) { + expect(e.message).to.include(TEST_ERROR_MESSAGE) + } else { + expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR) + } + } + }) + }) +} From 9bcae9c165a0011e4ad2d217328a668f24da366d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 6 Nov 2025 14:39:13 +0000 Subject: [PATCH 28/28] refactor: re-arrange tests and extract error messages and strings --- example/src/tests/unit/common.ts | 8 ++ example/src/tests/unit/index.ts | 9 +- .../specs/{ => operations}/execute.spec.ts | 6 +- .../{ => operations}/executeBatch.spec.ts | 6 +- .../{ => operations}/transaction.spec.ts | 90 +++++-------------- 5 files changed, 44 insertions(+), 75 deletions(-) rename example/src/tests/unit/specs/{ => operations}/execute.spec.ts (96%) rename example/src/tests/unit/specs/{ => operations}/executeBatch.spec.ts (94%) rename example/src/tests/unit/specs/{ => operations}/transaction.spec.ts (83%) diff --git a/example/src/tests/unit/common.ts b/example/src/tests/unit/common.ts index d1da4862..c37ccbb8 100644 --- a/example/src/tests/unit/common.ts +++ b/example/src/tests/unit/common.ts @@ -6,6 +6,14 @@ import { import { resetTestDb } from '../db' import chai from 'chai' +export const TEST_ERROR_CODES = { + EXPECT_NITRO_SQLITE_ERROR: 'Should have thrown a valid NitroSQLiteError', + EXPECT_PROMISE_REJECTION: 'Should have thrown a promise rejection', +} as const + +export const TEST_ERROR_MESSAGE = 'Error from callback' +export const TEST_ERROR = new Error(TEST_ERROR_MESSAGE) + export function isNitroSQLiteError(e: unknown): e is NitroSQLiteError { return e instanceof NitroSQLiteError } diff --git a/example/src/tests/unit/index.ts b/example/src/tests/unit/index.ts index 7b8762c6..bb4c1070 100644 --- a/example/src/tests/unit/index.ts +++ b/example/src/tests/unit/index.ts @@ -1,9 +1,10 @@ import { beforeEach, describe } from '../MochaRNAdapter' import { setupTestDb } from './common' -import registerExecuteUnitTests from './specs/execute.spec' -import registerTransactionUnitTests from './specs/transaction.spec' -import registerExecuteBatchUnitTests from './specs/executeBatch.spec' +import registerExecuteUnitTests from './specs/operations/execute.spec' +import registerTransactionUnitTests from './specs/operations/transaction.spec' +import registerExecuteBatchUnitTests from './specs/operations/executeBatch.spec' import registerTypeORMUnitTestsSpecs from './specs/typeorm.spec' +import registerDatabaseQueueUnitTests from './specs/DatabaseQueue.spec' export function registerUnitTests() { beforeEach(setupTestDb) @@ -13,6 +14,8 @@ export function registerUnitTests() { registerTransactionUnitTests() registerExecuteBatchUnitTests() }) + + registerDatabaseQueueUnitTests() } export function registerTypeORMUnitTests() { diff --git a/example/src/tests/unit/specs/execute.spec.ts b/example/src/tests/unit/specs/operations/execute.spec.ts similarity index 96% rename from example/src/tests/unit/specs/execute.spec.ts rename to example/src/tests/unit/specs/operations/execute.spec.ts index 721104b9..4c50196f 100644 --- a/example/src/tests/unit/specs/execute.spec.ts +++ b/example/src/tests/unit/specs/operations/execute.spec.ts @@ -1,10 +1,10 @@ -import { chance, expect, isNitroSQLiteError } from '../common' +import { chance, expect, isNitroSQLiteError } from '../../common' import { enableSimpleNullHandling, NITRO_SQLITE_NULL, } from 'react-native-nitro-sqlite' -import { describe, it } from '../../MochaRNAdapter' -import { testDb } from '../../db' +import { describe, it } from '../../../MochaRNAdapter' +import { testDb } from '../../../db' export default function registerExecuteUnitTests() { describe('execute', () => { diff --git a/example/src/tests/unit/specs/executeBatch.spec.ts b/example/src/tests/unit/specs/operations/executeBatch.spec.ts similarity index 94% rename from example/src/tests/unit/specs/executeBatch.spec.ts rename to example/src/tests/unit/specs/operations/executeBatch.spec.ts index 200acbd5..901f019f 100644 --- a/example/src/tests/unit/specs/executeBatch.spec.ts +++ b/example/src/tests/unit/specs/operations/executeBatch.spec.ts @@ -1,7 +1,7 @@ -import { chance, expect } from '../common' +import { chance, expect } from '../../common' import type { BatchQueryCommand } from 'react-native-nitro-sqlite' -import { describe, it } from '../../MochaRNAdapter' -import { testDb } from '../../db' +import { describe, it } from '../../../MochaRNAdapter' +import { testDb } from '../../../db' export default function registerExecuteBatchUnitTests() { describe('executeBatch', () => { diff --git a/example/src/tests/unit/specs/transaction.spec.ts b/example/src/tests/unit/specs/operations/transaction.spec.ts similarity index 83% rename from example/src/tests/unit/specs/transaction.spec.ts rename to example/src/tests/unit/specs/operations/transaction.spec.ts index 30cc6ead..fd194aa8 100644 --- a/example/src/tests/unit/specs/transaction.spec.ts +++ b/example/src/tests/unit/specs/operations/transaction.spec.ts @@ -1,10 +1,14 @@ -import { chance, expect, isNitroSQLiteError } from '../common' -import { describe, it } from '../../MochaRNAdapter' -import type { User } from '../../../model/User' -import { testDb, testDbQueue } from '../../db' - -const DUMMY_ERROR_NAME = 'Transaction Rejection Error' -const DUMMY_ERROR_MESSAGE = 'Error from callback' +import { + chance, + expect, + isNitroSQLiteError, + TEST_ERROR, + TEST_ERROR_MESSAGE, + TEST_ERROR_CODES, +} from '../../common' +import { describe, it } from '../../../MochaRNAdapter' +import type { User } from '../../../../model/User' +import { testDb } from '../../../db' export default function registerTransactionUnitTests() { describe('transaction', () => { @@ -202,19 +206,18 @@ export default function registerTransactionUnitTests() { it('Transaction, rejects on callback error', async () => { const promised = testDb.transaction(async () => { - throw new Error(DUMMY_ERROR_MESSAGE) + throw TEST_ERROR }) // ASSERT: should return a promise that eventually rejects expect(promised).to.have.property('then').that.is.a('function') try { await promised - expect.fail(DUMMY_ERROR_NAME) + expect.fail(TEST_ERROR_CODES.EXPECT_PROMISE_REJECTION) } catch (e) { - console.log(e) if (isNitroSQLiteError(e)) - expect(e.message).to.include(DUMMY_ERROR_MESSAGE) - else expect.fail('Should have thrown a valid NitroSQLiteError') + expect(e.message).to.include(TEST_ERROR_MESSAGE) + else expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR) } }) @@ -226,11 +229,11 @@ export default function registerTransactionUnitTests() { expect(promised).to.have.property('then').that.is.a('function') try { await promised - expect.fail(DUMMY_ERROR_NAME) + expect.fail(TEST_ERROR_CODES.EXPECT_PROMISE_REJECTION) } catch (e) { if (isNitroSQLiteError(e)) expect(e.message).to.include('no such table: tableThatDoesNotExist') - else expect.fail('Should have thrown a valid NitroSQLiteError') + else expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR) } }) @@ -302,7 +305,7 @@ export default function registerTransactionUnitTests() { const res = testDb.execute('SELECT * FROM User') expect(res.rows?._array).to.eql([]) } else { - expect.fail('Should have thrown a valid NitroSQLiteError') + expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR) } } }) @@ -406,18 +409,18 @@ export default function registerTransactionUnitTests() { it('Async transaction, rejects on callback error', async () => { const promised = testDb.transaction(() => { - throw new Error(DUMMY_ERROR_MESSAGE) + throw new Error(TEST_ERROR_MESSAGE) }) // ASSERT: should return a promise that eventually rejects expect(promised).to.have.property('then').that.is.a('function') try { await promised - expect.fail(DUMMY_ERROR_NAME) + expect.fail(TEST_ERROR_CODES.EXPECT_PROMISE_REJECTION) } catch (e) { if (isNitroSQLiteError(e)) - expect(e.message).to.include(DUMMY_ERROR_MESSAGE) - else expect.fail('Should have thrown a valid NitroSQLiteError') + expect(e.message).to.include(TEST_ERROR_MESSAGE) + else expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR) } }) @@ -430,57 +433,12 @@ export default function registerTransactionUnitTests() { expect(promised).to.have.property('then').that.is.a('function') try { await promised - expect.fail(DUMMY_ERROR_NAME) + expect.fail(TEST_ERROR_CODES.EXPECT_PROMISE_REJECTION) } catch (e) { if (isNitroSQLiteError(e)) expect(e.message).to.include('no such table: tableThatDoesNotExist') - else expect.fail('Should have thrown a valid NitroSQLiteError') + else expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR) } }) - - it('transaction are queued', async () => { - const transaction1Promise = testDb.transaction(async (tx) => { - tx.execute('SELECT * FROM [User];') - - expect(testDbQueue.queue.length).to.equal(2) - expect(testDbQueue.inProgress).to.equal(true) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - tx.execute('SELECT * FROM [User];') - - expect(testDbQueue.queue.length).to.equal(2) - expect(testDbQueue.inProgress).to.equal(true) - }) - - expect(testDbQueue.inProgress).to.equal(true) - expect(testDbQueue.queue.length).to.equal(0) - - const transaction2Promise = testDb.transaction(async (tx) => { - tx.execute('SELECT * FROM [User];') - }) - - expect(testDbQueue.queue.length).to.equal(1) - expect(testDbQueue.inProgress).to.equal(true) - - const transaction3Promise = testDb.transaction(async (tx) => { - tx.execute('SELECT * FROM [User];') - }) - - await transaction1Promise - - expect(testDbQueue.queue.length).to.equal(1) - expect(testDbQueue.inProgress).to.equal(true) - - await transaction2Promise - - expect(testDbQueue.queue.length).to.equal(0) - expect(testDbQueue.inProgress).to.equal(true) - - await transaction3Promise - - expect(testDbQueue.queue.length).to.equal(0) - expect(testDbQueue.inProgress).to.equal(false) - }) }) }