Skip to content

Commit 5a7be1c

Browse files
authored
feat: Migrate JS-sided locks to DatabaseQueue for transactions and executeBatch commands (#227)
* Update bun.lockb * feat: implement locks for `executeBatch` * feat: add better errors * feat: improve concurrency features * refactor: rename databaseQueue variables * feat: add `isExcusive` parameter to `transaction` * fix: styling * fix: handle errors in sync `executeBatch` * fix: add NitroSQLite prefix to custom error * refactor: remove type exports * fix: database queue promise chaining * refactor: remove redundant session functions * refactor: move db name to constants * chore: add package sources to example ts files * refactor: restructure test db setup * feat: add prefix to native C++ exception * fix: add back line * fix: transaction rollback result * test: add transaction queuing test * feat: adapt tests to new error types * fix: make all transaction callbacks async * refactor: export NitroSQLiteError class from package * fix: transaction type * fix: make another transaction callback async * fix: always throw error on rollback * refactor: rename variable * test: add more DatabaseQueue tests * refactor: re-arrange tests and extract error messages and strings
1 parent af4e605 commit 5a7be1c

File tree

16 files changed

+460
-167
lines changed

16 files changed

+460
-167
lines changed

example/src/tests/db.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,40 @@ import type {
44
BatchQueryCommand,
55
} from 'react-native-nitro-sqlite'
66
import { open } from 'react-native-nitro-sqlite'
7+
import {
8+
getDatabaseQueue,
9+
type DatabaseQueue,
10+
} from '../../../package/src/DatabaseQueue'
711

812
const chance = new Chance()
913

10-
export let testDb: NitroSQLiteConnection | undefined
14+
export const TEST_DB_NAME = 'test'
15+
16+
export let testDb: NitroSQLiteConnection
17+
export let testDbQueue: DatabaseQueue
1118
export function resetTestDb() {
1219
try {
1320
if (testDb != null) {
1421
testDb.close()
1522
testDb.delete()
1623
}
24+
1725
testDb = open({
18-
name: 'test',
26+
name: TEST_DB_NAME,
1927
})
28+
testDbQueue = getDatabaseQueue(TEST_DB_NAME)
29+
30+
testDb.execute('DROP TABLE IF EXISTS User;')
31+
testDb.execute(
32+
'CREATE TABLE User ( id REAL PRIMARY KEY, name TEXT NOT NULL, age REAL, networth REAL) STRICT;',
33+
)
2034
} catch (e) {
2135
console.warn('Error resetting user database', e)
2236
}
2337
}
2438

39+
const LARGE_DB_NAME = 'large'
40+
2541
// Copyright 2024 Oscar Franco
2642
// Taken from "op-sqlite" example project.
2743
// Used to demonstrate the performance of NitroSQLite.
@@ -34,7 +50,7 @@ export function resetLargeDb() {
3450
largeDb.delete()
3551
}
3652
largeDb = open({
37-
name: 'large',
53+
name: LARGE_DB_NAME,
3854
})
3955

4056
largeDb.execute(

example/src/tests/unit/common.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,27 @@
11
import { Chance } from 'chance'
22
import {
3-
NitroSQLiteConnection,
43
enableSimpleNullHandling,
4+
NitroSQLiteError,
55
} from 'react-native-nitro-sqlite'
6-
import { testDb as testDbInternal, resetTestDb } from '../db'
6+
import { resetTestDb } from '../db'
77
import chai from 'chai'
88

9-
export function isError(e: unknown): e is Error {
10-
return e instanceof Error
9+
export const TEST_ERROR_CODES = {
10+
EXPECT_NITRO_SQLITE_ERROR: 'Should have thrown a valid NitroSQLiteError',
11+
EXPECT_PROMISE_REJECTION: 'Should have thrown a promise rejection',
12+
} as const
13+
14+
export const TEST_ERROR_MESSAGE = 'Error from callback'
15+
export const TEST_ERROR = new Error(TEST_ERROR_MESSAGE)
16+
17+
export function isNitroSQLiteError(e: unknown): e is NitroSQLiteError {
18+
return e instanceof NitroSQLiteError
1119
}
1220

1321
export const expect = chai.expect
1422
export const chance = new Chance()
1523

16-
export let testDb: NitroSQLiteConnection
17-
1824
export function setupTestDb() {
1925
enableSimpleNullHandling(false)
20-
21-
try {
22-
resetTestDb()
23-
24-
if (testDbInternal == null) throw new Error('Failed to reset test database')
25-
26-
testDbInternal.execute('DROP TABLE IF EXISTS User;')
27-
testDbInternal.execute(
28-
'CREATE TABLE User ( id REAL PRIMARY KEY, name TEXT NOT NULL, age REAL, networth REAL) STRICT;',
29-
)
30-
31-
testDb = testDbInternal!
32-
} catch (e) {
33-
console.warn('Error resetting user database', e)
34-
}
26+
resetTestDb()
3527
}

example/src/tests/unit/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { beforeEach, describe } from '../MochaRNAdapter'
22
import { setupTestDb } from './common'
3-
import registerExecuteUnitTests from './specs/execute.spec'
4-
import registerTransactionUnitTests from './specs/transaction.spec'
5-
import registerExecuteBatchUnitTests from './specs/executeBatch.spec'
3+
import registerExecuteUnitTests from './specs/operations/execute.spec'
4+
import registerTransactionUnitTests from './specs/operations/transaction.spec'
5+
import registerExecuteBatchUnitTests from './specs/operations/executeBatch.spec'
66
import registerTypeORMUnitTestsSpecs from './specs/typeorm.spec'
7+
import registerDatabaseQueueUnitTests from './specs/DatabaseQueue.spec'
78

89
export function registerUnitTests() {
910
beforeEach(setupTestDb)
@@ -13,6 +14,8 @@ export function registerUnitTests() {
1314
registerTransactionUnitTests()
1415
registerExecuteBatchUnitTests()
1516
})
17+
18+
registerDatabaseQueueUnitTests()
1619
}
1720

1821
export function registerTypeORMUnitTests() {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import {
2+
expect,
3+
isNitroSQLiteError,
4+
TEST_ERROR,
5+
TEST_ERROR_CODES,
6+
TEST_ERROR_MESSAGE,
7+
} from '../common'
8+
import { describe, it } from '../../MochaRNAdapter'
9+
import { testDb, testDbQueue } from '../../db'
10+
import type { BatchQueryCommand } from 'react-native-nitro-sqlite'
11+
12+
const TEST_QUERY = 'SELECT * FROM [User];'
13+
14+
const TEST_BATCH_COMMANDS: BatchQueryCommand[] = [{ query: TEST_QUERY }]
15+
16+
export default function registerDatabaseQueueUnitTests() {
17+
describe('Database Queue', () => {
18+
it('multiple transactions are queued', async () => {
19+
const transaction1Promise = testDb.transaction(async (tx) => {
20+
tx.execute(TEST_QUERY)
21+
22+
expect(testDbQueue.queue.length).to.equal(2)
23+
expect(testDbQueue.inProgress).to.equal(true)
24+
25+
await new Promise<void>((resolve) => setTimeout(resolve, 100))
26+
27+
tx.execute(TEST_QUERY)
28+
29+
expect(testDbQueue.queue.length).to.equal(2)
30+
expect(testDbQueue.inProgress).to.equal(true)
31+
})
32+
33+
expect(testDbQueue.inProgress).to.equal(true)
34+
expect(testDbQueue.queue.length).to.equal(0)
35+
36+
const transaction2Promise = testDb.transaction(async (tx) => {
37+
tx.execute(TEST_QUERY)
38+
})
39+
40+
expect(testDbQueue.queue.length).to.equal(1)
41+
expect(testDbQueue.inProgress).to.equal(true)
42+
43+
const transaction3Promise = testDb.transaction(async (tx) => {
44+
tx.execute(TEST_QUERY)
45+
})
46+
47+
await transaction1Promise
48+
49+
expect(testDbQueue.queue.length).to.equal(1)
50+
expect(testDbQueue.inProgress).to.equal(true)
51+
52+
await transaction2Promise
53+
54+
expect(testDbQueue.queue.length).to.equal(0)
55+
expect(testDbQueue.inProgress).to.equal(true)
56+
57+
await transaction3Promise
58+
59+
expect(testDbQueue.queue.length).to.equal(0)
60+
expect(testDbQueue.inProgress).to.equal(false)
61+
})
62+
63+
it('multiple executeBatchAsync operations are queued', async () => {
64+
const executeBatch1Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS)
65+
66+
expect(testDbQueue.queue.length).to.equal(0)
67+
expect(testDbQueue.inProgress).to.equal(true)
68+
69+
const executeBatch2Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS)
70+
71+
expect(testDbQueue.queue.length).to.equal(1)
72+
expect(testDbQueue.inProgress).to.equal(true)
73+
74+
const executeBatch3Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS)
75+
76+
expect(testDbQueue.queue.length).to.equal(2)
77+
expect(testDbQueue.inProgress).to.equal(true)
78+
79+
await executeBatch1Promise
80+
81+
expect(testDbQueue.queue.length).to.equal(1)
82+
expect(testDbQueue.inProgress).to.equal(true)
83+
84+
await executeBatch2Promise
85+
86+
expect(testDbQueue.queue.length).to.equal(0)
87+
expect(testDbQueue.inProgress).to.equal(true)
88+
89+
await executeBatch3Promise
90+
91+
expect(testDbQueue.queue.length).to.equal(0)
92+
expect(testDbQueue.inProgress).to.equal(false)
93+
})
94+
95+
it('mixed transactions and executeBatchAsync operations are queued', async () => {
96+
const transaction1Promise = testDb.transaction(async (tx) => {
97+
tx.execute('SELECT * FROM [User];')
98+
})
99+
100+
expect(testDbQueue.queue.length).to.equal(0)
101+
expect(testDbQueue.inProgress).to.equal(true)
102+
103+
const executeBatch1Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS)
104+
105+
expect(testDbQueue.queue.length).to.equal(1)
106+
expect(testDbQueue.inProgress).to.equal(true)
107+
108+
const transaction2Promise = testDb.transaction(async (tx) => {
109+
tx.execute(TEST_QUERY)
110+
})
111+
112+
expect(testDbQueue.queue.length).to.equal(2)
113+
expect(testDbQueue.inProgress).to.equal(true)
114+
115+
const executeBatch2Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS)
116+
117+
expect(testDbQueue.queue.length).to.equal(3)
118+
expect(testDbQueue.inProgress).to.equal(true)
119+
120+
await transaction1Promise
121+
122+
expect(testDbQueue.queue.length).to.equal(2)
123+
expect(testDbQueue.inProgress).to.equal(true)
124+
125+
await executeBatch1Promise
126+
127+
expect(testDbQueue.queue.length).to.equal(1)
128+
expect(testDbQueue.inProgress).to.equal(true)
129+
130+
await transaction2Promise
131+
132+
expect(testDbQueue.queue.length).to.equal(0)
133+
expect(testDbQueue.inProgress).to.equal(true)
134+
135+
await executeBatch2Promise
136+
137+
expect(testDbQueue.queue.length).to.equal(0)
138+
expect(testDbQueue.inProgress).to.equal(false)
139+
})
140+
141+
it('errors are thrown through DatabaseQueue', async () => {
142+
const transaction1Promise = testDb.transaction(async (tx) => {
143+
tx.execute('SELECT * FROM [User];')
144+
throw TEST_ERROR
145+
})
146+
147+
expect(testDbQueue.queue.length).to.equal(0)
148+
expect(testDbQueue.inProgress).to.equal(true)
149+
150+
const executeBatch1Promise = testDb.executeBatchAsync(TEST_BATCH_COMMANDS)
151+
152+
expect(testDbQueue.queue.length).to.equal(1)
153+
expect(testDbQueue.inProgress).to.equal(true)
154+
155+
try {
156+
await transaction1Promise
157+
158+
expect(testDbQueue.queue.length).to.equal(0)
159+
expect(testDbQueue.inProgress).to.equal(true)
160+
} catch (e) {
161+
if (isNitroSQLiteError(e)) {
162+
expect(e.message).to.include(TEST_ERROR_MESSAGE)
163+
} else {
164+
expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR)
165+
}
166+
}
167+
168+
try {
169+
await executeBatch1Promise
170+
171+
expect(testDbQueue.queue.length).to.equal(0)
172+
expect(testDbQueue.inProgress).to.equal(false)
173+
} catch (e) {
174+
if (isNitroSQLiteError(e)) {
175+
expect(e.message).to.include(TEST_ERROR_MESSAGE)
176+
} else {
177+
expect.fail(TEST_ERROR_CODES.EXPECT_NITRO_SQLITE_ERROR)
178+
}
179+
}
180+
})
181+
})
182+
}

example/src/tests/unit/specs/execute.spec.ts renamed to example/src/tests/unit/specs/operations/execute.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { chance, expect, isError, testDb } from '../common'
1+
import { chance, expect, isNitroSQLiteError } from '../../common'
22
import {
33
enableSimpleNullHandling,
44
NITRO_SQLITE_NULL,
55
} from 'react-native-nitro-sqlite'
6-
import { describe, it } from '../../MochaRNAdapter'
6+
import { describe, it } from '../../../MochaRNAdapter'
7+
import { testDb } from '../../../db'
78

89
export default function registerExecuteUnitTests() {
910
describe('execute', () => {
@@ -93,7 +94,7 @@ export default function registerExecuteUnitTests() {
9394
[id, name, age, networth],
9495
)
9596
} catch (e: unknown) {
96-
if (isError(e)) {
97+
if (isNitroSQLiteError(e)) {
9798
expect(e.message).to.include(
9899
'cannot store TEXT value in REAL column User.age',
99100
)

example/src/tests/unit/specs/executeBatch.spec.ts renamed to example/src/tests/unit/specs/operations/executeBatch.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { chance, expect, testDb } from '../common'
1+
import { chance, expect } from '../../common'
22
import type { BatchQueryCommand } from 'react-native-nitro-sqlite'
3-
import { describe, it } from '../../MochaRNAdapter'
3+
import { describe, it } from '../../../MochaRNAdapter'
4+
import { testDb } from '../../../db'
45

56
export default function registerExecuteBatchUnitTests() {
67
describe('executeBatch', () => {

0 commit comments

Comments
 (0)