Skip to content

Commit 25f4c3e

Browse files
chrispadercodex
andauthored
fix: route global raw queries through the managed queue
Co-authored-by: Codex <noreply@openai.com>
1 parent 12b0de9 commit 25f4c3e

6 files changed

Lines changed: 172 additions & 2 deletions

File tree

‎docs/content/docs/guides/sync-and-async.mdx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ See [prepared statements](/docs/guides/prepared-statements#connection-ordering-a
4545

4646
## Global helpers and native access
4747

48-
The [`NitroSQLite` export](/api/react-native-nitro-sqlite/variables/NitroSQLite) also has `execute`, `executeAsync`, `prepare`, `executeBatch`, `executeBatchAsync`, and `transaction` helpers that take a database name. `NitroSQLite.open(options)` is the same helper as the named [`open()` export](/api/react-native-nitro-sqlite/functions/open). Global queries join the default connection's JavaScript queue when the name was opened through `open()`. They do not address independent connections; use the object returned by `open()` for those. Without a default managed connection, global `execute` and `executeAsync` call the native methods directly and still need an open native database handle. Global `prepare`, batches, and transactions need a default connection opened through `open()`.
48+
The [`NitroSQLite` export](/api/react-native-nitro-sqlite/variables/NitroSQLite) also has `execute`, `executeAsync`, `executeRaw`, `executeRawAsync`, `prepare`, `executeBatch`, `executeBatchAsync`, and `transaction` helpers that take a database name. `NitroSQLite.open(options)` is the same helper as the named [`open()` export](/api/react-native-nitro-sqlite/functions/open).
49+
50+
Global queries join the default connection's JavaScript queue when the name was opened through `open()`. This includes `NitroSQLite.executeRaw(dbName, query, params)` and `NitroSQLite.executeRawAsync(dbName, query, params)`. A synchronous query throws while that connection is busy; an asynchronous query waits for an active transaction to finish. Global helpers do not address independent connections; use the object returned by `open()` for those.
51+
52+
Without a default managed connection, global `execute`, `executeAsync`, `executeRaw`, and `executeRawAsync` call the native methods directly and still need an open native database handle. Global `prepare`, batches, and transactions need a default connection opened through `open()`.
4953

5054
`NitroSQLite.native` exposes the underlying [`NitroSQLiteNative` hybrid object](/api/react-native-nitro-sqlite/hybrid-objects/NitroSQLiteNative). Its methods take a database name and return raw results without the connection helper's `rows` container. They also throw native errors without converting them to `NitroSQLiteError`. Its `executeAsync()` accepts an optional `onRows` callback that receives leading rows in batches, in order; the resolved `results` then holds only the rows after the last batch.
5155

‎docs/scripts/check-api.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ const requiredMembers = {
6767
'transaction',
6868
'execute',
6969
'executeAsync',
70+
'executeRaw',
71+
'executeRawAsync',
7072
'executeBatch',
7173
'executeBatchAsync',
7274
'close',

‎packages/react-native-nitro-sqlite/src/__tests__/entrypoints.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
NitroSQLiteExceptionType as ExceptionTypeFromErrors,
1919
} from '../errors'
2020
import { HybridNitroSQLite } from '../nitro'
21+
import { executeRaw, executeRawAsync } from '../operations/execute'
2122
import { init as androidInit } from '../OnLoad.android'
2223
import { init as iosInit } from '../OnLoad'
2324

@@ -32,6 +33,8 @@ describe('entrypoints', () => {
3233
expect(NitroSQLite.native).toBe(HybridNitroSQLite)
3334
expect(NitroSQLite.open).toBe(open)
3435
expect(NitroSQLite.open).not.toBe(HybridNitroSQLite.open)
36+
expect(NitroSQLite.executeRaw).toBe(executeRaw)
37+
expect(NitroSQLite.executeRawAsync).toBe(executeRawAsync)
3538
expect(iosInit()).toBeUndefined()
3639
})
3740

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
jest.mock('../nitro')
2+
3+
import { NitroSQLite } from '../index'
4+
import { HybridNitroSQLite } from '../nitro'
5+
import {
6+
closeDatabaseQueue,
7+
isDatabaseOpen,
8+
openDatabaseQueue,
9+
} from '../DatabaseQueue'
10+
import NitroSQLiteError from '../NitroSQLiteError'
11+
import { deferred, nativeRawResult, nativeResult } from './testUtils'
12+
13+
const dbName = 'execute-raw-test'
14+
const query = 'SELECT ? AS value'
15+
const raw = nativeRawResult(['value'], [[7]])
16+
17+
beforeEach(() => {
18+
jest.clearAllMocks()
19+
jest.mocked(HybridNitroSQLite.executeRaw).mockReturnValue(raw)
20+
jest.mocked(HybridNitroSQLite.executeRawAsync).mockResolvedValue(raw)
21+
jest.mocked(HybridNitroSQLite.execute).mockReturnValue(nativeResult())
22+
jest.mocked(HybridNitroSQLite.executeAsync).mockResolvedValue(nativeResult())
23+
})
24+
25+
afterEach(() => {
26+
if (isDatabaseOpen(dbName)) closeDatabaseQueue(dbName)
27+
})
28+
29+
it('forwards unmanaged raw queries and parameters without adapting their rows', async () => {
30+
expect(NitroSQLite.executeRaw(dbName, query, [7])).toBe(raw)
31+
await expect(NitroSQLite.executeRawAsync(dbName, query, [7])).resolves.toBe(
32+
raw,
33+
)
34+
expect(HybridNitroSQLite.executeRaw).toHaveBeenCalledWith(dbName, query, [7])
35+
expect(HybridNitroSQLite.executeRawAsync).toHaveBeenCalledWith(
36+
dbName,
37+
query,
38+
[7],
39+
)
40+
})
41+
42+
it('rejects global synchronous raw queries while managed work is pending', async () => {
43+
openDatabaseQueue(dbName)
44+
expect(NitroSQLite.executeRaw(dbName, query)).toBe(raw)
45+
46+
const gate = deferred<typeof raw>()
47+
jest
48+
.mocked(HybridNitroSQLite.executeRawAsync)
49+
.mockReturnValueOnce(gate.promise)
50+
const pending = NitroSQLite.executeRawAsync(dbName, query)
51+
try {
52+
expect(() => NitroSQLite.executeRaw(dbName, 'SELECT blocked')).toThrow(
53+
'busy',
54+
)
55+
expect(HybridNitroSQLite.executeRaw).toHaveBeenCalledTimes(1)
56+
} finally {
57+
gate.resolve(raw)
58+
await pending
59+
}
60+
})
61+
62+
it('keeps global raw queries outside an active managed transaction', async () => {
63+
openDatabaseQueue(dbName)
64+
const entered = deferred<void>()
65+
const finish = deferred<void>()
66+
const transaction = NitroSQLite.transaction(dbName, async () => {
67+
entered.resolve()
68+
await finish.promise
69+
})
70+
await entered.promise
71+
72+
const pending = NitroSQLite.executeRawAsync(dbName, query)
73+
try {
74+
expect(() => NitroSQLite.executeRaw(dbName, query)).toThrow('busy')
75+
expect(HybridNitroSQLite.executeRaw).not.toHaveBeenCalled()
76+
expect(HybridNitroSQLite.executeRawAsync).not.toHaveBeenCalled()
77+
} finally {
78+
finish.resolve()
79+
await transaction
80+
await pending
81+
}
82+
expect(HybridNitroSQLite.executeRawAsync).toHaveBeenCalledWith(
83+
dbName,
84+
query,
85+
undefined,
86+
)
87+
expect(
88+
jest.mocked(HybridNitroSQLite.execute).mock.invocationCallOrder[0],
89+
).toBeLessThan(
90+
jest.mocked(HybridNitroSQLite.executeRawAsync).mock.invocationCallOrder[0]!,
91+
)
92+
})
93+
94+
it('normalizes native errors and releases the managed queue after failure', async () => {
95+
openDatabaseQueue(dbName)
96+
jest.mocked(HybridNitroSQLite.executeRaw).mockImplementationOnce(() => {
97+
throw new Error('raw failed')
98+
})
99+
expect(() => NitroSQLite.executeRaw(dbName, query)).toThrow(NitroSQLiteError)
100+
expect(NitroSQLite.executeRaw(dbName, query)).toBe(raw)
101+
102+
jest
103+
.mocked(HybridNitroSQLite.executeRawAsync)
104+
.mockRejectedValueOnce('raw async failed')
105+
await expect(
106+
NitroSQLite.executeRawAsync(dbName, query),
107+
).rejects.toMatchObject({
108+
name: 'NitroSQLiteError',
109+
message: 'raw async failed',
110+
})
111+
await expect(NitroSQLite.executeRawAsync(dbName, query)).resolves.toBe(raw)
112+
})

‎packages/react-native-nitro-sqlite/src/index.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { transaction } from './operations/transaction'
22
import { HybridNitroSQLite } from './nitro'
33
import { open } from './operations/session'
4-
import { execute, executeAsync } from './operations/execute'
4+
import {
5+
execute,
6+
executeAsync,
7+
executeRaw,
8+
executeRawAsync,
9+
} from './operations/execute'
510
import { prepare } from './operations/prepare'
611
import { init } from './OnLoad'
712
import { executeBatch, executeBatchAsync } from './operations/executeBatch'
@@ -20,6 +25,8 @@ export const NitroSQLite = {
2025
transaction,
2126
execute,
2227
executeAsync,
28+
executeRaw,
29+
executeRawAsync,
2330
prepare,
2431
executeBatch,
2532
executeBatchAsync,

‎packages/react-native-nitro-sqlite/src/operations/execute.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,27 @@ export async function executeAsyncNative<Row extends QueryResultRow = never>(
111111
}
112112
}
113113

114+
/** Execute one SQL statement synchronously by database name, returning rows as arrays.
115+
* Uses the default managed connection's queue when the database was opened through `open()`.
116+
* Otherwise, calls the open native database directly.
117+
* @param dbName Name of an open database.
118+
* @param query SQL statement with optional positional placeholders.
119+
* @param params Values bound to the placeholders.
120+
* @returns Rows and column names in SQL column order, with affected row count and insert ID.
121+
* @throws NitroSQLiteError if the managed connection is busy or the query fails.
122+
*/
123+
export function executeRaw(
124+
dbName: string,
125+
query: string,
126+
params?: SQLiteQueryParams,
127+
): NitroSQLiteRawQueryResult {
128+
if (!isDatabaseOpen(dbName)) {
129+
return executeRawNative(dbName, query, params)
130+
}
131+
132+
return executeRawManaged(dbName, query, params)
133+
}
134+
114135
export function executeRawManaged(
115136
dbName: string,
116137
query: string,
@@ -134,6 +155,27 @@ export function executeRawNative(
134155
}
135156
}
136157

158+
/** Execute one SQL statement asynchronously by database name, returning rows as arrays.
159+
* Uses the default managed connection's queue when the database was opened through `open()`.
160+
* Otherwise, calls the open native database directly.
161+
* @param dbName Name of an open database.
162+
* @param query SQL statement with optional positional placeholders.
163+
* @param params Values bound to the placeholders.
164+
* @returns A promise of rows and column names in SQL column order, with affected row count and insert ID.
165+
* @throws NitroSQLiteError as a rejected promise if the query fails.
166+
*/
167+
export async function executeRawAsync(
168+
dbName: string,
169+
query: string,
170+
params?: SQLiteQueryParams,
171+
): Promise<NitroSQLiteRawQueryResult> {
172+
if (!isDatabaseOpen(dbName)) {
173+
return executeRawAsyncNative(dbName, query, params)
174+
}
175+
176+
return executeRawAsyncManaged(dbName, query, params)
177+
}
178+
137179
export async function executeRawAsyncManaged(
138180
dbName: string,
139181
query: string,

0 commit comments

Comments
 (0)