Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

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.

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.

**[Read the documentation](https://sqlite.margelo.com/docs)** for setup, guides, integrations, and the API reference.

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.
Expand Down
41 changes: 41 additions & 0 deletions docs/content/docs/concepts/queries-and-indexes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,44 @@ db.close()
```

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.

## Query spatial ranges

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).

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.

```ts
import { open } from 'react-native-nitro-sqlite'

const db = open({ name: 'places.sqlite' })

await db.executeAsync(`
CREATE VIRTUAL TABLE IF NOT EXISTS place_bounds USING rtree(
id,
minLongitude, maxLongitude,
minLatitude, maxLatitude
)
`)
await db.executeAsync(
'INSERT OR REPLACE INTO place_bounds VALUES (?, ?, ?, ?, ?)',
[1, 16.36, 16.38, 48.2, 48.22],
)

const { rows } = await db.executeAsync<{ id: number }>(
`SELECT id FROM place_bounds
WHERE minLongitude <= ? AND maxLongitude >= ?
AND minLatitude <= ? AND maxLatitude >= ?`,
[16.4, 16.35, 48.23, 48.19],
)
console.log(rows._array) // [{ id: 1 }]
db.close()
```

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.

`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.

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.

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.
20 changes: 18 additions & 2 deletions docs/content/docs/configuration/android.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,31 @@ Set `nitroSqliteFlags` in the app's `android/gradle.properties` to pass definiti
nitroSqliteFlags="-DSQLITE_ENABLE_FTS5=1"
```

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.
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.

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.

## RTree support

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.

To omit R\*Tree from the bundled build, set this boolean in your app's `package.json` and rebuild the native app:

```json
{
"nitroSQLite": {
"enableRTree": false
}
}
```

`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`.

## Threading

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.

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.
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`.

## Vector search

Expand Down
16 changes: 16 additions & 0 deletions docs/content/docs/configuration/ios.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ NITRO_SQLITE_USE_PHONE_VERSION=1 npx pod-install

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.

## RTree support

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.

To omit R\*Tree from the bundled build, set this boolean in your app's `package.json`, install Pods again, and rebuild:

```json
{
"nitroSQLite": {
"enableRTree": false
}
}
```

`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`.

## Thread safety and performance mode

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`:
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/configuration/macos.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ The repository's macOS example targets macOS 14 or later with React Native macOS

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.

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.
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.
71 changes: 71 additions & 0 deletions example/tests/unit/specs/operations/execute.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,77 @@ export default function registerExecuteUnitTests() {
})
})

describe('SQLite extensions', () => {
it('creates and queries an RTree virtual table', () => {
testDb.execute('DROP TABLE IF EXISTS SpatialIndex')

try {
testDb.execute(`
CREATE VIRTUAL TABLE SpatialIndex USING rtree(
id,
minX, maxX,
minY, maxY
)
`)
testDb.execute(
'INSERT INTO SpatialIndex (id, minX, maxX, minY, maxY) VALUES (?, ?, ?, ?, ?)',
[1, 10, 20, 30, 40],
)

const result = testDb.execute(
'SELECT id, minX, maxX, minY, maxY FROM SpatialIndex WHERE minX <= ? AND maxX >= ?',
[15, 15],
)

expect(result.results).toEqual([
{ id: 1, minX: 10, maxX: 20, minY: 30, maxY: 40 },
])
} finally {
testDb.execute('DROP TABLE IF EXISTS SpatialIndex')
}
})

for (const module of ['rtree', 'rtree_i32']) {
it(`reopens and migrates a database containing ${module}`, () => {
const name = `rtree-migration-${module}`
let db = open({ name })
let isOpen = true

try {
db.execute('CREATE TABLE Item (id INTEGER PRIMARY KEY)')
db.execute(
`CREATE VIRTUAL TABLE SpatialIndex USING ${module}(id, minX, maxX, minY, maxY)`,
)
db.execute('INSERT INTO Item VALUES (1)')
db.execute('INSERT INTO SpatialIndex VALUES (1, 10, 20, 30, 40)')
db.execute(
'CREATE VIEW SpatialItems AS SELECT Item.id FROM Item JOIN SpatialIndex USING (id)',
)
db.close()
isOpen = false

db = open({ name })
isOpen = true
db.execute('ALTER TABLE Item RENAME TO RenamedItem')
db.execute('ALTER TABLE SpatialIndex RENAME TO RenamedSpatialIndex')

expect(db.execute('SELECT id FROM SpatialItems').results).toEqual([
{ id: 1 },
])
expect(
db.execute(
'SELECT id FROM RenamedSpatialIndex WHERE minX <= ? AND maxX >= ? AND minY <= ? AND maxY >= ?',
[15, 15, 35, 35],
).results,
).toEqual([{ id: 1 }])
} finally {
if (isOpen) db.close()
db.delete()
}
})
}
})

describe('Bind errors', () => {
it('throws when execute receives an extra parameter without exposing it', () => {
const extraParameter = 'do-not-expose-sync-parameter'
Expand Down
9 changes: 8 additions & 1 deletion packages/react-native-nitro-sqlite/RNNitroSQLite.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ unless app_config.is_a?(Hash)
raise "nitroSQLite in package.json must be an object"
end

enable_rtree = app_config.fetch("enableRTree", true)
unless [true, false].include?(enable_rtree)
raise "nitroSQLite.enableRTree in package.json must be true or false"
end

if ENV.key?("NITRO_SQLITE_THREADSAFE")
thread_safe_value = ENV["NITRO_SQLITE_THREADSAFE"]
unless %w[true false 1 0].include?(thread_safe_value)
Expand Down Expand Up @@ -77,7 +82,9 @@ Pod::Spec.new do |s|
log_message.call("SQLite thread safety: SQLITE_THREADSAFE=#{sqlite_threadsafe}")
log_message.call("SQLite performance mode: #{performance_mode ? "enabled" : "disabled"}")
performance_cflags = performance_mode ? " #{optimized_cflags}" : ""
other_cflags = "#{inherited_cflags}#{performance_cflags} -DSQLITE_THREADSAFE=#{sqlite_threadsafe} "
# SQLite checks whether this macro is defined, so disabling RTree must omit it.
rtree_cflags = enable_rtree ? " -DSQLITE_ENABLE_RTREE=1" : ""
other_cflags = "#{inherited_cflags}#{performance_cflags}#{rtree_cflags} -DSQLITE_THREADSAFE=#{sqlite_threadsafe} "

s.pod_target_xcconfig = {
:GCC_PREPROCESSOR_DEFINITIONS => "HAVE_FULLFSYNC=1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ project.ext.resolveNitroSqliteDefaultFlags = { File appPackageFile, String custo

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

def defaultFlags = [threadSafe ? '-DSQLITE_THREADSAFE=1' : '-DSQLITE_THREADSAFE=0']
// Omit the macro when disabled; SQLITE_ENABLE_RTREE=0 still enables SQLite's module.
if (enableRTree) {
defaultFlags += '-DSQLITE_ENABLE_RTREE=1'
}
if (performanceMode) {
defaultFlags += [
"-DSQLITE_DQS=0",
Expand Down
11 changes: 8 additions & 3 deletions scripts/android-sqlite-flags/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@ tasks.register('testSqliteFlags') {
}

def defaultFlags = flagsFor([:], '')
assert defaultFlags.size() == 10
assert defaultFlags.size() == 11
assert defaultFlags.contains('-DSQLITE_THREADSAFE=1')
assert defaultFlags.contains('-DSQLITE_DQS=0')
assert defaultFlags.contains('-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1')
assert defaultFlags.contains('-DSQLITE_ENABLE_RTREE=1')
assert resolveNitroSqliteDefaultFlags(new File(temporaryDir, 'missing.json'), '') == defaultFlags

assert flagsFor([nitroSQLite: [performanceMode: false]], '') == ['-DSQLITE_THREADSAFE=1']
assert flagsFor([nitroSQLite: [performanceMode: false]], '') == ['-DSQLITE_THREADSAFE=1', '-DSQLITE_ENABLE_RTREE=1']
assert flagsFor([nitroSQLite: [threadSafe: false]], '').contains('-DSQLITE_THREADSAFE=0')
assert flagsFor([nitroSQLite: [threadSafe: false, performanceMode: false]], '') == ['-DSQLITE_THREADSAFE=0']
assert flagsFor([nitroSQLite: [threadSafe: false, performanceMode: false]], '') == ['-DSQLITE_THREADSAFE=0', '-DSQLITE_ENABLE_RTREE=1']
assert !flagsFor([nitroSQLite: [enableRTree: false]], '').any { it.startsWith('-DSQLITE_ENABLE_RTREE') }
assert flagsFor([nitroSQLite: [threadSafe: false, performanceMode: false, enableRTree: false]], '') == ['-DSQLITE_THREADSAFE=0']

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

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