Skip to content

Commit d51cd54

Browse files
fix: enable SQLite RTree by default (#425)
* fix: enable SQLite RTree extension (cherry picked from commit 3655b99) * fix: cover RTree migrations and allow build opt-out --------- Co-authored-by: Trần Đình Huy <kingonwork@gmail.com>
1 parent c69e045 commit d51cd54

11 files changed

Lines changed: 238 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
Nitro SQLite is a SQLite library for React Native on iOS, macOS, visionOS, and Android, built with [Nitro Modules](https://nitro.margelo.com/). It provides synchronous and asynchronous queries, transactions, and batch operations.
1212

13+
The bundled SQLite build enables [R\*Tree indexes](https://sqlite.margelo.com/docs/concepts/queries-and-indexes#query-spatial-ranges) by default on Apple platforms and Android. Set `nitroSQLite.enableRTree` to `false` in your app's `package.json` to omit it from the native build.
14+
1315
**[Read the documentation](https://sqlite.margelo.com/docs)** for setup, guides, integrations, and the API reference.
1416

1517
If you use a coding agent, give it the [NitroSQLite skill](https://github.com/margelo/react-native-skills/blob/nitro-sqlite/skills/react-native-nitro-sqlite/SKILL.md). It links to focused guidance for connections, queries, transactions, concurrency, and migration. See the [AI agent guide](https://sqlite.margelo.com/docs/guides/ai-agents) for what to check in generated code.

‎docs/content/docs/concepts/queries-and-indexes.mdx‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,44 @@ db.close()
3333
```
3434

3535
Bind application values with `?`; placeholders cannot stand in for table or column names. Add an index for a query you actually run, then inspect its plan with representative data. [Parameters and results](/docs/guides/parameters-and-results) describes returned rows, and the [performance guide](/docs/guides/performance) covers keeping large reads bounded.
36+
37+
## Query spatial ranges
38+
39+
An R\*Tree index stores bounding boxes and finds those that overlap a query box. Use it for queries such as finding places inside a map viewport or events that overlap a time interval. A normal B-tree index orders values by its columns; an R\*Tree can narrow a search across several independent coordinate ranges. Bounding boxes can include candidates outside an object's exact geometry, so apply an exact check when your query requires one. See [SQLite's R\*Tree documentation](https://www.sqlite.org/rtree.html).
40+
41+
Nitro SQLite's bundled build includes `rtree` and `rtree_i32` by default. You still create the virtual table and populate it explicitly. Enabling the module does not create indexes or change queries against ordinary tables.
42+
43+
```ts
44+
import { open } from 'react-native-nitro-sqlite'
45+
46+
const db = open({ name: 'places.sqlite' })
47+
48+
await db.executeAsync(`
49+
CREATE VIRTUAL TABLE IF NOT EXISTS place_bounds USING rtree(
50+
id,
51+
minLongitude, maxLongitude,
52+
minLatitude, maxLatitude
53+
)
54+
`)
55+
await db.executeAsync(
56+
'INSERT OR REPLACE INTO place_bounds VALUES (?, ?, ?, ?, ?)',
57+
[1, 16.36, 16.38, 48.2, 48.22],
58+
)
59+
60+
const { rows } = await db.executeAsync<{ id: number }>(
61+
`SELECT id FROM place_bounds
62+
WHERE minLongitude <= ? AND maxLongitude >= ?
63+
AND minLatitude <= ? AND maxLatitude >= ?`,
64+
[16.4, 16.35, 48.23, 48.19],
65+
)
66+
console.log(rows._array) // [{ id: 1 }]
67+
db.close()
68+
```
69+
70+
If you store the same objects in an ordinary table, keep their bounding boxes in sync through triggers or updates in the same transaction. R\*Tree data takes storage and adds work to writes. Its storage and index maintenance apply to the tables you create, rather than every database opened by an app that includes the module.
71+
72+
`rtree` stores coordinates as 32-bit floats and rounds lower bounds down and upper bounds up. Overlap queries can return extra candidates. Containment queries can miss an entry at an edge unless you expand the query box as described in [SQLite's roundoff guidance](https://www.sqlite.org/rtree.html#roundoff_error). Use `rtree_i32` for signed 32-bit integer coordinates. Both support one to five dimensions.
73+
74+
A write can restructure an active R\*Tree scan and fail with `SQLITE_LOCKED`. Nitro SQLite's execution methods, including prepared statement execution, read the complete result before returning, so a completed read does not leave that scan open.
75+
76+
For apps that do not need R\*Tree, set `nitroSQLite.enableRTree` to `false` in the app's `package.json` and rebuild the native app. See [Apple configuration](/docs/configuration/ios#rtree-support) or [Android configuration](/docs/configuration/android#rtree-support). Disabling it makes R\*Tree tables unavailable. Queries and schema migrations involving those tables can fail with `no such module: rtree`, including a rename of an ordinary table referenced by a view that also uses R\*Tree. System SQLite support depends on the operating system's build.

‎docs/content/docs/configuration/android.mdx‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,31 @@ Set `nitroSqliteFlags` in the app's `android/gradle.properties` to pass definiti
1919
nitroSqliteFlags="-DSQLITE_ENABLE_FTS5=1"
2020
```
2121

22-
These are SQLite compile definitions, so they affect the bundled source when the native library is rebuilt. Android does not use the iOS `nitroSQLite` package settings or the `NITRO_SQLITE_USE_PHONE_VERSION` pod switch.
22+
These are SQLite compile definitions, so they affect the bundled source when the native library is rebuilt. The Apple `NITRO_SQLITE_USE_PHONE_VERSION` pod switch does not apply to Android.
2323

2424
To use the licensed SQLite Encryption Extension, follow [encrypt a database](/docs/guides/encryption) for the matching source files, symbol prefix, and `SQLITE_ENABLE_SEE` flag.
2525

26+
## RTree support
27+
28+
R\*Tree indexes find bounding boxes that overlap a spatial or time range. The bundled SQLite build includes `rtree` and `rtree_i32` by default. See [query spatial ranges](/docs/concepts/queries-and-indexes#query-spatial-ranges) for SQL examples and limitations.
29+
30+
To omit R\*Tree from the bundled build, set this boolean in your app's `package.json` and rebuild the native app:
31+
32+
```json
33+
{
34+
"nitroSQLite": {
35+
"enableRTree": false
36+
}
37+
}
38+
```
39+
40+
`enableRTree` defaults to `true` independently of `performanceMode`. A non-boolean value fails Gradle configuration. Disabling it omits the default `SQLITE_ENABLE_RTREE` definition. Do not add `-DSQLITE_ENABLE_RTREE=0` to `nitroSqliteFlags` to disable it: SQLite checks whether the macro is defined, so that value still enables the module. Custom compile definitions can enable the module even when the package setting is `false`.
41+
2642
## Threading
2743

2844
The native library opens each database with `SQLITE_OPEN_FULLMUTEX` and serializes calls on each handle. The JavaScript connection helper coordinates work per managed connection and rejects conflicting synchronous work. `nitroSqliteFlags` can change compile-time SQLite behavior, including `SQLITE_THREADSAFE`. Nitro SQLite rejects [independent connections](/docs/guides/multiple-connections) when SQLite was built with `SQLITE_THREADSAFE=0`; other database handles can still run concurrently, so an app disabling mutexes must serialize SQLite calls across the process.
2945

30-
Android has no separate `performanceMode` Gradle property. Its native build always applies its CMake compiler options, including `-O2`. The iOS `performanceMode` package key does not configure Android.
46+
The app's `nitroSQLite.threadSafe` and `nitroSQLite.performanceMode` settings also configure Android's SQLite defaults. Custom `nitroSqliteFlags` override individual defaults. The native build additionally applies its CMake compiler options, including `-O2`.
3147

3248
## Vector search
3349

‎docs/content/docs/configuration/ios.mdx‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ NITRO_SQLITE_USE_PHONE_VERSION=1 npx pod-install
3939

4040
The bundled source is where the pod's compile settings apply. Changing the pod's `SQLITE_THREADSAFE` or performance flags cannot change how the system library was built.
4141

42+
## RTree support
43+
44+
R\*Tree indexes find bounding boxes that overlap a spatial or time range. The bundled SQLite build includes `rtree` and `rtree_i32` by default on iOS, macOS, and visionOS. See [query spatial ranges](/docs/concepts/queries-and-indexes#query-spatial-ranges) for SQL examples and limitations.
45+
46+
To omit R\*Tree from the bundled build, set this boolean in your app's `package.json`, install Pods again, and rebuild:
47+
48+
```json
49+
{
50+
"nitroSQLite": {
51+
"enableRTree": false
52+
}
53+
}
54+
```
55+
56+
`enableRTree` defaults to `true` independently of `performanceMode`. A non-boolean value fails Pod installation. Disabling it omits `SQLITE_ENABLE_RTREE`; defining that macro as `0` still enables SQLite's module. This setting cannot change system SQLite's capabilities when you use `NITRO_SQLITE_USE_PHONE_VERSION=1`.
57+
4258
## Thread safety and performance mode
4359

4460
The bundled SQLite build defaults to `SQLITE_THREADSAFE=1` and enables the project's performance compile flags. Set either option in the app's `package.json`:

‎docs/content/docs/configuration/macos.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ The repository's macOS example targets macOS 14 or later with React Native macOS
1717

1818
To keep Metro in a separate terminal, run the `start` script in `example/macos` and launch the `macos` script with `--no-packager`. Pass `--mode Release --no-packager` to build and launch the production bundle.
1919

20-
The [iOS configuration guide](/docs/configuration/ios) describes the shared Apple build flags for bundled SQLite, thread safety, performance mode, and vector search. Run CocoaPods from `macos` when applying those settings to a macOS app.
20+
The [iOS configuration guide](/docs/configuration/ios) describes the shared Apple build flags for bundled SQLite, R\*Tree, thread safety, performance mode, and vector search. Run CocoaPods from `macos` when applying those settings to a macOS app.

‎example/tests/unit/specs/operations/execute.spec.ts‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,77 @@ export default function registerExecuteUnitTests() {
382382
})
383383
})
384384

385+
describe('SQLite extensions', () => {
386+
it('creates and queries an RTree virtual table', () => {
387+
testDb.execute('DROP TABLE IF EXISTS SpatialIndex')
388+
389+
try {
390+
testDb.execute(`
391+
CREATE VIRTUAL TABLE SpatialIndex USING rtree(
392+
id,
393+
minX, maxX,
394+
minY, maxY
395+
)
396+
`)
397+
testDb.execute(
398+
'INSERT INTO SpatialIndex (id, minX, maxX, minY, maxY) VALUES (?, ?, ?, ?, ?)',
399+
[1, 10, 20, 30, 40],
400+
)
401+
402+
const result = testDb.execute(
403+
'SELECT id, minX, maxX, minY, maxY FROM SpatialIndex WHERE minX <= ? AND maxX >= ?',
404+
[15, 15],
405+
)
406+
407+
expect(result.results).toEqual([
408+
{ id: 1, minX: 10, maxX: 20, minY: 30, maxY: 40 },
409+
])
410+
} finally {
411+
testDb.execute('DROP TABLE IF EXISTS SpatialIndex')
412+
}
413+
})
414+
415+
for (const module of ['rtree', 'rtree_i32']) {
416+
it(`reopens and migrates a database containing ${module}`, () => {
417+
const name = `rtree-migration-${module}`
418+
let db = open({ name })
419+
let isOpen = true
420+
421+
try {
422+
db.execute('CREATE TABLE Item (id INTEGER PRIMARY KEY)')
423+
db.execute(
424+
`CREATE VIRTUAL TABLE SpatialIndex USING ${module}(id, minX, maxX, minY, maxY)`,
425+
)
426+
db.execute('INSERT INTO Item VALUES (1)')
427+
db.execute('INSERT INTO SpatialIndex VALUES (1, 10, 20, 30, 40)')
428+
db.execute(
429+
'CREATE VIEW SpatialItems AS SELECT Item.id FROM Item JOIN SpatialIndex USING (id)',
430+
)
431+
db.close()
432+
isOpen = false
433+
434+
db = open({ name })
435+
isOpen = true
436+
db.execute('ALTER TABLE Item RENAME TO RenamedItem')
437+
db.execute('ALTER TABLE SpatialIndex RENAME TO RenamedSpatialIndex')
438+
439+
expect(db.execute('SELECT id FROM SpatialItems').results).toEqual([
440+
{ id: 1 },
441+
])
442+
expect(
443+
db.execute(
444+
'SELECT id FROM RenamedSpatialIndex WHERE minX <= ? AND maxX >= ? AND minY <= ? AND maxY >= ?',
445+
[15, 15, 35, 35],
446+
).results,
447+
).toEqual([{ id: 1 }])
448+
} finally {
449+
if (isOpen) db.close()
450+
db.delete()
451+
}
452+
})
453+
}
454+
})
455+
385456
describe('Bind errors', () => {
386457
it('throws when execute receives an extra parameter without exposing it', () => {
387458
const extraParameter = 'do-not-expose-sync-parameter'

‎packages/react-native-nitro-sqlite/RNNitroSQLite.podspec‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ unless app_config.is_a?(Hash)
99
raise "nitroSQLite in package.json must be an object"
1010
end
1111

12+
enable_rtree = app_config.fetch("enableRTree", true)
13+
unless [true, false].include?(enable_rtree)
14+
raise "nitroSQLite.enableRTree in package.json must be true or false"
15+
end
16+
1217
if ENV.key?("NITRO_SQLITE_THREADSAFE")
1318
thread_safe_value = ENV["NITRO_SQLITE_THREADSAFE"]
1419
unless %w[true false 1 0].include?(thread_safe_value)
@@ -77,7 +82,9 @@ Pod::Spec.new do |s|
7782
log_message.call("SQLite thread safety: SQLITE_THREADSAFE=#{sqlite_threadsafe}")
7883
log_message.call("SQLite performance mode: #{performance_mode ? "enabled" : "disabled"}")
7984
performance_cflags = performance_mode ? " #{optimized_cflags}" : ""
80-
other_cflags = "#{inherited_cflags}#{performance_cflags} -DSQLITE_THREADSAFE=#{sqlite_threadsafe} "
85+
# SQLite checks whether this macro is defined, so disabling RTree must omit it.
86+
rtree_cflags = enable_rtree ? " -DSQLITE_ENABLE_RTREE=1" : ""
87+
other_cflags = "#{inherited_cflags}#{performance_cflags}#{rtree_cflags} -DSQLITE_THREADSAFE=#{sqlite_threadsafe} "
8188

8289
s.pod_target_xcconfig = {
8390
:GCC_PREPROCESSOR_DEFINITIONS => "HAVE_FULLFSYNC=1",

‎packages/react-native-nitro-sqlite/android/sqlite-flags.gradle‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ project.ext.resolveNitroSqliteDefaultFlags = { File appPackageFile, String custo
1818

1919
def threadSafe = readBooleanFlag("threadSafe")
2020
def performanceMode = readBooleanFlag("performanceMode")
21+
def enableRTree = readBooleanFlag("enableRTree")
2122
def customFlagNames = (customFlags =~ /-D([A-Za-z_][A-Za-z0-9_]*)/).collect { it[1] }.toSet()
2223

2324
def defaultFlags = [threadSafe ? '-DSQLITE_THREADSAFE=1' : '-DSQLITE_THREADSAFE=0']
25+
// Omit the macro when disabled; SQLITE_ENABLE_RTREE=0 still enables SQLite's module.
26+
if (enableRTree) {
27+
defaultFlags += '-DSQLITE_ENABLE_RTREE=1'
28+
}
2429
if (performanceMode) {
2530
defaultFlags += [
2631
"-DSQLITE_DQS=0",

‎scripts/android-sqlite-flags/build.gradle‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,23 @@ tasks.register('testSqliteFlags') {
1010
}
1111

1212
def defaultFlags = flagsFor([:], '')
13-
assert defaultFlags.size() == 10
13+
assert defaultFlags.size() == 11
1414
assert defaultFlags.contains('-DSQLITE_THREADSAFE=1')
1515
assert defaultFlags.contains('-DSQLITE_DQS=0')
1616
assert defaultFlags.contains('-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1')
17+
assert defaultFlags.contains('-DSQLITE_ENABLE_RTREE=1')
1718
assert resolveNitroSqliteDefaultFlags(new File(temporaryDir, 'missing.json'), '') == defaultFlags
1819

19-
assert flagsFor([nitroSQLite: [performanceMode: false]], '') == ['-DSQLITE_THREADSAFE=1']
20+
assert flagsFor([nitroSQLite: [performanceMode: false]], '') == ['-DSQLITE_THREADSAFE=1', '-DSQLITE_ENABLE_RTREE=1']
2021
assert flagsFor([nitroSQLite: [threadSafe: false]], '').contains('-DSQLITE_THREADSAFE=0')
21-
assert flagsFor([nitroSQLite: [threadSafe: false, performanceMode: false]], '') == ['-DSQLITE_THREADSAFE=0']
22+
assert flagsFor([nitroSQLite: [threadSafe: false, performanceMode: false]], '') == ['-DSQLITE_THREADSAFE=0', '-DSQLITE_ENABLE_RTREE=1']
23+
assert !flagsFor([nitroSQLite: [enableRTree: false]], '').any { it.startsWith('-DSQLITE_ENABLE_RTREE') }
24+
assert flagsFor([nitroSQLite: [threadSafe: false, performanceMode: false, enableRTree: false]], '') == ['-DSQLITE_THREADSAFE=0']
2225

2326
def customFlags = flagsFor([:], '-DSQLITE_THREADSAFE=0 -DSQLITE_DQS=3 -DSQLITE_ENABLE_FTS5=1')
2427
assert !customFlags.any { it.startsWith('-DSQLITE_THREADSAFE=') || it.startsWith('-DSQLITE_DQS=') }
2528
assert !flagsFor([:], '-DTHREADSAFE=0').any { it.startsWith('-DSQLITE_THREADSAFE=') }
29+
assert !flagsFor([:], '-DSQLITE_ENABLE_RTREE=1').any { it.startsWith('-DSQLITE_ENABLE_RTREE') }
2630

2731
def expectInvalid = { Map appPackage, String message ->
2832
try {
@@ -36,5 +40,6 @@ tasks.register('testSqliteFlags') {
3640
expectInvalid([nitroSQLite: true], 'nitroSQLite in package.json must be an object')
3741
expectInvalid([nitroSQLite: [threadSafe: 1]], 'nitroSQLite.threadSafe in package.json must be true or false')
3842
expectInvalid([nitroSQLite: [performanceMode: 'false']], 'nitroSQLite.performanceMode in package.json must be true or false')
43+
expectInvalid([nitroSQLite: [enableRTree: 'false']], 'nitroSQLite.enableRTree in package.json must be true or false')
3944
}
4045
}

0 commit comments

Comments
 (0)