From 1e6504ab3ff180e263d7206b01c74e2796fab92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=C3=ACnh=20Huy?= Date: Sun, 23 Aug 2026 00:46:08 +0700 Subject: [PATCH 1/2] fix: enable SQLite RTree extension (cherry picked from commit 3655b9902e50d6d7c68ef5b648a79bfdad5b26e2) --- README.md | 2 ++ .../unit/specs/operations/execute.spec.ts | 31 +++++++++++++++++++ .../RNNitroSQLite.podspec | 2 +- .../android/CMakeLists.txt | 1 + 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2506dcd6..85effa2e 100644 --- a/README.md +++ b/README.md @@ -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 the RTree extension by default on iOS and Android. + **[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. diff --git a/example/tests/unit/specs/operations/execute.spec.ts b/example/tests/unit/specs/operations/execute.spec.ts index 2438e515..ba767e60 100644 --- a/example/tests/unit/specs/operations/execute.spec.ts +++ b/example/tests/unit/specs/operations/execute.spec.ts @@ -382,6 +382,37 @@ 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') + } + }) + }) + describe('ArrayBuffer support', () => { describe('execute', () => { it('stores and reads ArrayBuffer values from BLOB columns', () => { diff --git a/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec b/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec index 76126c63..22f25ba6 100644 --- a/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec +++ b/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec @@ -86,7 +86,7 @@ Pod::Spec.new do |s| 'CLANG_CXX_LIBRARY' => 'libc++', 'DEFINES_MODULE' => 'YES', "HEADER_SEARCH_PATHS" => "\"${PODS_ROOT}/RCT-Folly\"" + (nitro_sqlite_vec ? " \"#{nitro_sqlite_vec_cpp}\"" : ""), - "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES" + (nitro_sqlite_vec ? " NITRO_SQLITE_VEC=1" : "") + (ENV['NITRO_SQLITE_USE_PHONE_VERSION'] == '1' ? " NITRO_SQLITE_USE_PHONE_VERSION=1" : ""), + "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES SQLITE_ENABLE_RTREE=1" + (nitro_sqlite_vec ? " NITRO_SQLITE_VEC=1" : "") + (ENV['NITRO_SQLITE_USE_PHONE_VERSION'] == '1' ? " NITRO_SQLITE_USE_PHONE_VERSION=1" : ""), "OTHER_CPLUSPLUSFLAGS" => folly_compiler_flags, "OTHER_CFLAGS" => other_cflags, } diff --git a/packages/react-native-nitro-sqlite/android/CMakeLists.txt b/packages/react-native-nitro-sqlite/android/CMakeLists.txt index 78537f53..c85cd9bd 100644 --- a/packages/react-native-nitro-sqlite/android/CMakeLists.txt +++ b/packages/react-native-nitro-sqlite/android/CMakeLists.txt @@ -5,6 +5,7 @@ set (CMAKE_VERBOSE_MAKEFILE ON) set (CMAKE_CXX_STANDARD 20) add_definitions( + -DSQLITE_ENABLE_RTREE=1 ${SQLITE_DEFAULT_FLAGS} ${SQLITE_FLAGS} ) From fbaaa318db84a1b429ffa3aa20f4bdf8cc5ca165 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 1 Oct 2026 11:52:20 +0200 Subject: [PATCH 2/2] fix: cover RTree migrations and allow build opt-out --- README.md | 2 +- .../docs/concepts/queries-and-indexes.mdx | 41 +++++++++++ docs/content/docs/configuration/android.mdx | 20 +++++- docs/content/docs/configuration/ios.mdx | 16 +++++ docs/content/docs/configuration/macos.mdx | 2 +- .../unit/specs/operations/execute.spec.ts | 40 +++++++++++ .../RNNitroSQLite.podspec | 11 ++- .../android/CMakeLists.txt | 1 - .../android/sqlite-flags.gradle | 5 ++ scripts/android-sqlite-flags/build.gradle | 11 ++- scripts/test-podspec-threadsafe.rb | 69 ++++++++++++++++++- scripts/test-private-sqlite-symbols.sh | 3 +- 12 files changed, 207 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 85effa2e..c3cf2332 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 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 the RTree extension by default on iOS and Android. +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. diff --git a/docs/content/docs/concepts/queries-and-indexes.mdx b/docs/content/docs/concepts/queries-and-indexes.mdx index 626a7783..cd18515a 100644 --- a/docs/content/docs/concepts/queries-and-indexes.mdx +++ b/docs/content/docs/concepts/queries-and-indexes.mdx @@ -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. diff --git a/docs/content/docs/configuration/android.mdx b/docs/content/docs/configuration/android.mdx index 5f0a52be..9f2b1055 100644 --- a/docs/content/docs/configuration/android.mdx +++ b/docs/content/docs/configuration/android.mdx @@ -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 diff --git a/docs/content/docs/configuration/ios.mdx b/docs/content/docs/configuration/ios.mdx index 598233fe..52bfe5cb 100644 --- a/docs/content/docs/configuration/ios.mdx +++ b/docs/content/docs/configuration/ios.mdx @@ -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`: diff --git a/docs/content/docs/configuration/macos.mdx b/docs/content/docs/configuration/macos.mdx index 133d1222..caf63010 100644 --- a/docs/content/docs/configuration/macos.mdx +++ b/docs/content/docs/configuration/macos.mdx @@ -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. diff --git a/example/tests/unit/specs/operations/execute.spec.ts b/example/tests/unit/specs/operations/execute.spec.ts index ba767e60..656dc189 100644 --- a/example/tests/unit/specs/operations/execute.spec.ts +++ b/example/tests/unit/specs/operations/execute.spec.ts @@ -411,6 +411,46 @@ export default function registerExecuteUnitTests() { 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('ArrayBuffer support', () => { diff --git a/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec b/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec index 22f25ba6..fb21f117 100644 --- a/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec +++ b/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec @@ -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) @@ -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", @@ -86,7 +93,7 @@ Pod::Spec.new do |s| 'CLANG_CXX_LIBRARY' => 'libc++', 'DEFINES_MODULE' => 'YES', "HEADER_SEARCH_PATHS" => "\"${PODS_ROOT}/RCT-Folly\"" + (nitro_sqlite_vec ? " \"#{nitro_sqlite_vec_cpp}\"" : ""), - "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES SQLITE_ENABLE_RTREE=1" + (nitro_sqlite_vec ? " NITRO_SQLITE_VEC=1" : "") + (ENV['NITRO_SQLITE_USE_PHONE_VERSION'] == '1' ? " NITRO_SQLITE_USE_PHONE_VERSION=1" : ""), + "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES" + (nitro_sqlite_vec ? " NITRO_SQLITE_VEC=1" : "") + (ENV['NITRO_SQLITE_USE_PHONE_VERSION'] == '1' ? " NITRO_SQLITE_USE_PHONE_VERSION=1" : ""), "OTHER_CPLUSPLUSFLAGS" => folly_compiler_flags, "OTHER_CFLAGS" => other_cflags, } diff --git a/packages/react-native-nitro-sqlite/android/CMakeLists.txt b/packages/react-native-nitro-sqlite/android/CMakeLists.txt index c85cd9bd..78537f53 100644 --- a/packages/react-native-nitro-sqlite/android/CMakeLists.txt +++ b/packages/react-native-nitro-sqlite/android/CMakeLists.txt @@ -5,7 +5,6 @@ set (CMAKE_VERBOSE_MAKEFILE ON) set (CMAKE_CXX_STANDARD 20) add_definitions( - -DSQLITE_ENABLE_RTREE=1 ${SQLITE_DEFAULT_FLAGS} ${SQLITE_FLAGS} ) diff --git a/packages/react-native-nitro-sqlite/android/sqlite-flags.gradle b/packages/react-native-nitro-sqlite/android/sqlite-flags.gradle index 6b13d3a2..40e34142 100644 --- a/packages/react-native-nitro-sqlite/android/sqlite-flags.gradle +++ b/packages/react-native-nitro-sqlite/android/sqlite-flags.gradle @@ -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", diff --git a/scripts/android-sqlite-flags/build.gradle b/scripts/android-sqlite-flags/build.gradle index 87975513..21013d04 100644 --- a/scripts/android-sqlite-flags/build.gradle +++ b/scripts/android-sqlite-flags/build.gradle @@ -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 { @@ -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') } } diff --git a/scripts/test-podspec-threadsafe.rb b/scripts/test-podspec-threadsafe.rb index 8a59f669..804995e1 100644 --- a/scripts/test-podspec-threadsafe.rb +++ b/scripts/test-podspec-threadsafe.rb @@ -72,6 +72,12 @@ def main default_flags = with_app_package({}) { flags_for(nil) } assert_threadsafe(default_flags, "1") assert_optimization_flags(default_flags) + assert_includes(default_flags, "-DSQLITE_ENABLE_RTREE=1") + + without_rtree_flags = with_app_package("nitroSQLite" => {"enableRTree" => false}) do + flags_for(nil) + end + refute_includes(without_rtree_flags, "SQLITE_ENABLE_RTREE") unsafe_flags = with_app_package("nitroSQLite" => {"threadSafe" => false}) do flags_for(nil) @@ -96,6 +102,7 @@ def main end assert_threadsafe(unoptimized_flags, "1") refute_optimization_flags(unoptimized_flags) + assert_includes(unoptimized_flags, "-DSQLITE_ENABLE_RTREE=1") environment_enabled_flags = with_app_package("nitroSQLite" => {"performanceMode" => false}) do flags_for(nil, "true") @@ -117,10 +124,12 @@ def main assert_invalid_environment_value_rejected assert_invalid_package_performance_mode_rejected assert_invalid_environment_performance_mode_rejected + assert_invalid_package_rtree_rejected with_app_package({}) { assert_system_sqlite_configuration } compile_and_probe(unsafe_flags, "0") compile_and_probe(safe_flags, "1") compile_and_probe(unoptimized_flags, "1") + compile_and_probe(without_rtree_flags, "1", false) puts "SQLite pod configuration tests passed" end @@ -257,13 +266,66 @@ def assert_system_sqlite_configuration ) end -def compile_and_probe(flags, expected) +def assert_invalid_package_rtree_rejected + with_app_package("nitroSQLite" => {"enableRTree" => "false"}) do + evaluate_podspec("NITRO_SQLITE_THREADSAFE" => nil) + end + fail "Expected an invalid nitroSQLite.enableRTree value to fail" +rescue RuntimeError => error + expected = "nitroSQLite.enableRTree in package.json must be true or false" + fail "Unexpected validation error: #{error.message}" unless error.message == expected +end + +def compile_and_probe(flags, expected, rtree = true) Dir.mktmpdir("nitro-sqlite-threadsafe") do |directory| probe = File.join(directory, "probe.c") binary = File.join(directory, "probe") File.write( probe, - "#include \n#include \"sqlite3.h\"\nint main(void) { printf(\"%d\", sqlite3_threadsafe()); return 0; }\n", + <<~C, + #include + #include + #include + #include "sqlite3.h" + + int main(int argc, char **argv) { + assert(argc == 2); + sqlite3 *db = NULL; + assert(sqlite3_open(argv[1], &db) == SQLITE_OK); + int rc = sqlite3_exec(db, + "CREATE VIRTUAL TABLE SpatialIndex USING rtree(id,minX,maxX,minY,maxY)", + NULL, NULL, NULL); + if (#{rtree ? 1 : 0}) { + assert(rc == SQLITE_OK); + assert(sqlite3_exec(db, + "INSERT INTO SpatialIndex VALUES(1,10,20,30,40);" + "CREATE TABLE Item(id INTEGER PRIMARY KEY);" + "INSERT INTO Item VALUES(1);" + "CREATE VIEW SpatialItems AS SELECT Item.id FROM Item JOIN SpatialIndex USING(id);" + "CREATE VIRTUAL TABLE IntIndex USING rtree_i32(id,minX,maxX)", + NULL, NULL, NULL) == SQLITE_OK); + assert(sqlite3_close(db) == SQLITE_OK); + assert(sqlite3_open(argv[1], &db) == SQLITE_OK); + assert(sqlite3_exec(db, + "ALTER TABLE Item RENAME TO RenamedItem;" + "ALTER TABLE SpatialIndex RENAME TO RenamedSpatialIndex;", + NULL, NULL, NULL) == SQLITE_OK); + sqlite3_stmt *statement = NULL; + assert(sqlite3_prepare_v2(db, "SELECT id FROM SpatialItems", -1, + &statement, NULL) == SQLITE_OK); + assert(sqlite3_step(statement) == SQLITE_ROW); + assert(sqlite3_column_int(statement, 0) == 1); + assert(sqlite3_step(statement) == SQLITE_DONE); + assert(sqlite3_finalize(statement) == SQLITE_OK); + } else { + assert(rc == SQLITE_ERROR); + assert(strcmp(sqlite3_errmsg(db), "no such module: rtree") == 0); + } + assert(sqlite3_close(db) == SQLITE_OK); + printf("%d", sqlite3_threadsafe()); + return 0; + } + C ) compile_flags = Shellwords.split(flags).reject { |flag| flag == "$(inherited)" } @@ -278,7 +340,8 @@ def compile_and_probe(flags, expected) ] fail "Failed to compile bundled SQLite with SQLITE_THREADSAFE=#{expected}" unless system(*command) - actual = IO.popen([binary], &:read) + actual = IO.popen([binary, File.join(directory, "rtree.sqlite")], &:read) + fail "SQLite configuration probe failed" unless $?.success? assert_equal(actual, expected) end end diff --git a/scripts/test-private-sqlite-symbols.sh b/scripts/test-private-sqlite-symbols.sh index 48bbee45..c7d6630e 100644 --- a/scripts/test-private-sqlite-symbols.sh +++ b/scripts/test-private-sqlite-symbols.sh @@ -7,7 +7,7 @@ vec="$root/packages/react-native-nitro-sqlite-vec/cpp" tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT -cc -O0 -c "$sqlite/sqlite3.c" -o "$tmp/private.o" +cc -O0 -DSQLITE_ENABLE_RTREE=1 -c "$sqlite/sqlite3.c" -o "$tmp/private.o" cc -O0 -DSQLITE_CORE=1 -DSQLITE_VEC_STATIC=1 -I "$sqlite" -c "$vec/sqlite-vec/sqlite-vec.c" -o "$tmp/vec.o" c++ -std=c++20 -DSQLITE_CORE=1 -I "$sqlite" -I "$vec" -c "$vec/NitroSQLiteVecRegisterVectorExtensions.cpp" -o "$tmp/vec-register.o" @@ -86,6 +86,7 @@ int main(void) { assert(strcmp(system_version(), system_version_data()) == 0); assert(private_version_data() != system_version_data()); assert(private_compileoption("THREADSAFE=1") == 1); + assert(private_compileoption("ENABLE_RTREE") == 1); assert(private_vec_available()); assert(!system_vec_available()); printf("private: %s %s\n", private_version(), private_sourceid());