From 2a03cbb0f60c0bee18a50e6dc89a7588df139278 Mon Sep 17 00:00:00 2001 From: chrispader Date: Thu, 10 Sep 2026 14:30:07 +0200 Subject: [PATCH 1/2] fix(ios): configure SQLite thread safety Default bundled SQLite builds to serialized mode while allowing explicit 0 or 1 selection through NITRO_SQLITE_THREADSAFE. Preserve the existing performance flags and document the system SQLite exception. Co-authored-by: samuelscheit --- .github/workflows/build-ios.yml | 6 + README.md | 15 ++ .../RNNitroSQLite.podspec | 18 +- scripts/test-podspec-threadsafe.rb | 186 ++++++++++++++++++ 4 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 scripts/test-podspec-threadsafe.rb diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 3675ca6f..c7eaf634 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -13,6 +13,7 @@ on: - "packages/react-native-nitro-sqlite/ios/**" - "**/Podfile.lock" - "**/*.podspec" + - "scripts/test-podspec-threadsafe.rb" - "**/react-native.config.js" - "**/nitro.json" pull_request: @@ -25,6 +26,7 @@ on: - "packages/react-native-nitro-sqlite/ios/**" - "**/Podfile.lock" - "**/*.podspec" + - "scripts/test-podspec-threadsafe.rb" - "**/react-native.config.js" - "**/nitro.json" @@ -44,6 +46,10 @@ jobs: use_frameworks: "" # intentionally unset steps: - uses: actions/checkout@v7 + + - name: Test SQLite thread-safety pod configuration + run: ruby scripts/test-podspec-threadsafe.rb + - uses: oven-sh/setup-bun@v2 - name: Set USE_FRAMEWORKS diff --git a/README.md b/README.md index 80544f3e..b2121025 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,21 @@ You can use this package as a TypeORM driver. Because of Metro and Node resoluti # Configuration +## Configure bundled SQLite thread safety on iOS + +The bundled SQLite library compiles with `SQLITE_THREADSAFE=1` by default. This includes SQLite's mutex code and selects serialized mode, which lets SQLite serialize concurrent access to database connections and prepared statements. + +`NITRO_SQLITE_THREADSAFE` accepts `0` or `1`. Set it to `0` before installing Pods to compile the bundled library without mutex support: + +```bash +cd ios +NITRO_SQLITE_THREADSAFE=0 pod install +``` + +With `SQLITE_THREADSAFE=0`, SQLite removes its mutex code and cannot be made thread-safe at runtime. Only use this setting if the application serializes every SQLite call across the entire process. Per-database JavaScript queues are not sufficient because separate connections and SQLite's global state can still be accessed concurrently by native threads. + +When `NITRO_SQLITE_USE_PHONE_VERSION=1`, the pod links the system SQLite library instead of compiling the bundled source. `NITRO_SQLITE_THREADSAFE` does not change how that system library was compiled. + ## Use system SQLite on iOS To use the system SQLite instead of the bundled one: diff --git a/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec b/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec index 61906122..1a05f7bc 100644 --- a/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec +++ b/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec @@ -1,15 +1,16 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +sqlite_threadsafe = ENV.fetch("NITRO_SQLITE_THREADSAFE", "1") + +unless %w[0 1].include?(sqlite_threadsafe) + raise "NITRO_SQLITE_THREADSAFE must be either 0 or 1" +end folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1' log_message = lambda do |message| puts "\e[34m#{message}\e[0m" end -# TODO: Should be customizable in package.json. -# Used to create comparable benchmark results -performance_mode = 1 - Pod::Spec.new do |s| s.name = "RNNitroSQLite" s.version = package["version"] @@ -35,13 +36,8 @@ Pod::Spec.new do |s| optimizedCflags = '$(inherited) -DSQLITE_DQS=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS=1 -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_OMIT_DEPRECATED=1 -DSQLITE_OMIT_PROGRESS_CALLBACK=1 -DSQLITE_OMIT_SHARED_CACHE=1 -DSQLITE_USE_ALLOCA=1' - if performance_mode == 1 - log_message.call("Thread unsafe (1) performance mode enabled. Use only transactions! 🚀🚀") - other_cflags = optimizedCflags + ' -DSQLITE_THREADSAFE=0 ' - elsif performance_mode == 2 - log_message.call("Thread safe (2) performance mode enabled 🚀") - other_cflags = optimizedCflags + ' -DSQLITE_THREADSAFE=1 ' - end + log_message.call("SQLite thread safety: SQLITE_THREADSAFE=#{sqlite_threadsafe}") + other_cflags = optimizedCflags + " -DSQLITE_THREADSAFE=#{sqlite_threadsafe} " s.pod_target_xcconfig = { :GCC_PREPROCESSOR_DEFINITIONS => "HAVE_FULLFSYNC=1", diff --git a/scripts/test-podspec-threadsafe.rb b/scripts/test-podspec-threadsafe.rb new file mode 100644 index 00000000..896db06c --- /dev/null +++ b/scripts/test-podspec-threadsafe.rb @@ -0,0 +1,186 @@ +#!/usr/bin/env ruby + +require "shellwords" +require "tmpdir" + +ROOT = File.expand_path("..", __dir__) +PACKAGE_DIRECTORY = File.join(ROOT, "packages", "react-native-nitro-sqlite") +PODSPEC = File.join(PACKAGE_DIRECTORY, "RNNitroSQLite.podspec") +SQLITE_SOURCE = File.join(PACKAGE_DIRECTORY, "cpp", "sqlite", "sqlite3.c") + +module Pod + module UI + def self.puts(*) end + end + + class Spec + class << self + attr_accessor :last + + def new + spec = allocate + spec.send(:initialize) + yield spec + self.last = spec + end + end + + attr_reader :attributes_hash + + def initialize + @attributes_hash = {} + end + + def dependency(*) end + + def method_missing(name, *arguments) + attribute = name.to_s + return @attributes_hash[attribute] if arguments.empty? + + if attribute.end_with?("=") && arguments.length == 1 + @attributes_hash[attribute.delete_suffix("=")] = arguments.first + return arguments.first + end + + super + end + + def respond_to_missing?(*_arguments) + true + end + end +end + +def min_ios_version_supported + "13.4" +end + +def install_modules_dependencies(_spec) end + +def main + default_flags = flags_for(nil) + assert_threadsafe(default_flags, "1") + assert_optimization_flags(default_flags) + + unsafe_flags = flags_for("0") + assert_threadsafe(unsafe_flags, "0") + assert_optimization_flags(unsafe_flags) + + safe_flags = flags_for("1") + assert_threadsafe(safe_flags, "1") + assert_optimization_flags(safe_flags) + + assert_invalid_value_rejected + assert_system_sqlite_configuration + compile_and_probe(unsafe_flags, "0") + compile_and_probe(safe_flags, "1") + + puts "SQLite pod configuration tests passed" +end + +def flags_for(threadsafe) + spec = evaluate_podspec( + "NITRO_SQLITE_THREADSAFE" => threadsafe, + "NITRO_SQLITE_USE_PHONE_VERSION" => nil, + ) + spec.attributes_hash.fetch("pod_target_xcconfig").fetch("OTHER_CFLAGS") +end + +def evaluate_podspec(environment) + previous_environment = environment.to_h do |name, _value| + [name, ENV.key?(name) ? ENV.fetch(name) : nil] + end + + environment.each do |name, value| + value.nil? ? ENV.delete(name) : ENV[name] = value + end + + Dir.chdir(PACKAGE_DIRECTORY) { load PODSPEC } + Pod::Spec.last +ensure + previous_environment&.each do |name, value| + value.nil? ? ENV.delete(name) : ENV[name] = value + end +end + +def assert_threadsafe(flags, expected) + assert_includes(flags, "-DSQLITE_THREADSAFE=#{expected}") + other = expected == "1" ? "0" : "1" + refute_includes(flags, "-DSQLITE_THREADSAFE=#{other}") +end + +def assert_optimization_flags(flags) + %w[ + -DSQLITE_DQS=0 + -DSQLITE_DEFAULT_MEMSTATUS=0 + -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 + -DSQLITE_LIKE_DOESNT_MATCH_BLOBS=1 + -DSQLITE_MAX_EXPR_DEPTH=0 + -DSQLITE_OMIT_DEPRECATED=1 + -DSQLITE_OMIT_PROGRESS_CALLBACK=1 + -DSQLITE_OMIT_SHARED_CACHE=1 + -DSQLITE_USE_ALLOCA=1 + ].each { |flag| assert_includes(flags, flag) } +end + +def assert_invalid_value_rejected + evaluate_podspec("NITRO_SQLITE_THREADSAFE" => "2") + fail "Expected an invalid NITRO_SQLITE_THREADSAFE value to fail" +rescue RuntimeError => error + expected = "NITRO_SQLITE_THREADSAFE must be either 0 or 1" + fail "Unexpected validation error: #{error.message}" unless error.message == expected +end + +def assert_system_sqlite_configuration + spec = evaluate_podspec( + "NITRO_SQLITE_THREADSAFE" => "1", + "NITRO_SQLITE_USE_PHONE_VERSION" => "1", + ) + attributes = spec.attributes_hash + + assert_equal(attributes.fetch("library"), "sqlite3") + assert_equal( + attributes.fetch("exclude_files"), + ["cpp/sqlite/sqlite3.c", "cpp/sqlite/sqlite3.h"], + ) +end + +def compile_and_probe(flags, expected) + 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", + ) + + compile_flags = Shellwords.split(flags).reject { |flag| flag == "$(inherited)" } + command = [ + ENV.fetch("CC", "cc"), + *compile_flags, + "-I#{File.dirname(SQLITE_SOURCE)}", + SQLITE_SOURCE, + probe, + "-o", + binary, + ] + fail "Failed to compile bundled SQLite with SQLITE_THREADSAFE=#{expected}" unless system(*command) + + actual = IO.popen([binary], &:read) + assert_equal(actual, expected) + end +end + +def assert_includes(value, expected) + fail "Expected #{value.inspect} to include #{expected.inspect}" unless value.include?(expected) +end + +def refute_includes(value, unexpected) + fail "Expected #{value.inspect} not to include #{unexpected.inspect}" if value.include?(unexpected) +end + +def assert_equal(actual, expected) + fail "Expected #{expected.inspect}, got #{actual.inspect}" unless actual == expected +end + +main From 6a755122fab7c6aab1d82a5644afac2a906608fc Mon Sep 17 00:00:00 2001 From: chrispader Date: Thu, 10 Sep 2026 15:51:35 +0200 Subject: [PATCH 2/2] feat(ios): configure thread safety from package.json --- README.md | 14 +++- example/package.json | 3 + .../RNNitroSQLite.podspec | 24 ++++++- scripts/test-podspec-threadsafe.rb | 69 ++++++++++++++++--- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b2121025..574995d0 100644 --- a/README.md +++ b/README.md @@ -298,13 +298,21 @@ You can use this package as a TypeORM driver. Because of Metro and Node resoluti ## Configure bundled SQLite thread safety on iOS -The bundled SQLite library compiles with `SQLITE_THREADSAFE=1` by default. This includes SQLite's mutex code and selects serialized mode, which lets SQLite serialize concurrent access to database connections and prepared statements. +The bundled SQLite library compiles with `SQLITE_THREADSAFE=1` by default. This includes SQLite's mutex code and selects serialized mode, which lets SQLite serialize concurrent access to database connections and prepared statements. Configure it in your app's `package.json`: -`NITRO_SQLITE_THREADSAFE` accepts `0` or `1`. Set it to `0` before installing Pods to compile the bundled library without mutex support: +```json +{ + "nitroSQLite": { + "threadSafe": true + } +} +``` + +`threadSafe` accepts `true` or `false`. You can override it for one Pod installation with the `NITRO_SQLITE_THREADSAFE` environment variable. Environment variables accept `true`, `false`, `1`, or `0`: ```bash cd ios -NITRO_SQLITE_THREADSAFE=0 pod install +NITRO_SQLITE_THREADSAFE=false pod install ``` With `SQLITE_THREADSAFE=0`, SQLite removes its mutex code and cannot be made thread-safe at runtime. Only use this setting if the application serializes every SQLite call across the entire process. Per-database JavaScript queues are not sufficient because separate connections and SQLite's global state can still be accessed concurrently by native threads. diff --git a/example/package.json b/example/package.json index 0c709806..1a7d8bd2 100644 --- a/example/package.json +++ b/example/package.json @@ -2,6 +2,9 @@ "name": "react-native-nitro-sqlite-example", "version": "9.6.0", "private": true, + "nitroSQLite": { + "threadSafe": true + }, "scripts": { "start": "react-native start", "android": "react-native run-android", diff --git a/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec b/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec index 1a05f7bc..f6f01c51 100644 --- a/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec +++ b/packages/react-native-nitro-sqlite/RNNitroSQLite.podspec @@ -1,10 +1,28 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) -sqlite_threadsafe = ENV.fetch("NITRO_SQLITE_THREADSAFE", "1") +app_package_json_path = File.expand_path("../package.json", Pod::Config.instance.installation_root) +app_package = File.exist?(app_package_json_path) ? JSON.parse(File.read(app_package_json_path)) : {} +app_config = app_package.fetch("nitroSQLite", {}) -unless %w[0 1].include?(sqlite_threadsafe) - raise "NITRO_SQLITE_THREADSAFE must be either 0 or 1" +unless app_config.is_a?(Hash) + raise "nitroSQLite in package.json must be an object" +end + +if ENV.key?("NITRO_SQLITE_THREADSAFE") + thread_safe_value = ENV["NITRO_SQLITE_THREADSAFE"] + unless %w[true false 1 0].include?(thread_safe_value) + raise "NITRO_SQLITE_THREADSAFE must be true, false, 1, or 0" + end + + sqlite_threadsafe = %w[true 1].include?(thread_safe_value) ? "1" : "0" +else + thread_safe_value = app_config.fetch("threadSafe", true) + unless [true, false].include?(thread_safe_value) + raise "nitroSQLite.threadSafe in package.json must be true or false" + end + + sqlite_threadsafe = thread_safe_value ? "1" : "0" end folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1' log_message = lambda do |message| diff --git a/scripts/test-podspec-threadsafe.rb b/scripts/test-podspec-threadsafe.rb index 896db06c..c8fedd45 100644 --- a/scripts/test-podspec-threadsafe.rb +++ b/scripts/test-podspec-threadsafe.rb @@ -2,6 +2,7 @@ require "shellwords" require "tmpdir" +require "json" ROOT = File.expand_path("..", __dir__) PACKAGE_DIRECTORY = File.join(ROOT, "packages", "react-native-nitro-sqlite") @@ -49,6 +50,16 @@ def respond_to_missing?(*_arguments) true end end + + class Config + class << self + attr_accessor :installation_root + + def instance + self + end + end + end end def min_ios_version_supported @@ -58,26 +69,48 @@ def min_ios_version_supported def install_modules_dependencies(_spec) end def main - default_flags = flags_for(nil) + default_flags = with_app_package({}) { flags_for(nil) } assert_threadsafe(default_flags, "1") assert_optimization_flags(default_flags) - unsafe_flags = flags_for("0") + unsafe_flags = with_app_package("nitroSQLite" => {"threadSafe" => false}) do + flags_for(nil) + end assert_threadsafe(unsafe_flags, "0") assert_optimization_flags(unsafe_flags) - safe_flags = flags_for("1") + safe_flags = with_app_package("nitroSQLite" => {"threadSafe" => false}) do + flags_for("true") + end assert_threadsafe(safe_flags, "1") assert_optimization_flags(safe_flags) - assert_invalid_value_rejected - assert_system_sqlite_configuration + environment_unsafe_flags = with_app_package("nitroSQLite" => {"threadSafe" => true}) do + flags_for("false") + end + assert_threadsafe(environment_unsafe_flags, "0") + assert_optimization_flags(environment_unsafe_flags) + + assert_invalid_package_value_rejected + assert_invalid_package_config_rejected + assert_invalid_environment_value_rejected + with_app_package({}) { assert_system_sqlite_configuration } compile_and_probe(unsafe_flags, "0") compile_and_probe(safe_flags, "1") puts "SQLite pod configuration tests passed" end +def with_app_package(contents) + Dir.mktmpdir("nitro-sqlite-app") do |directory| + ios_directory = File.join(directory, "ios") + Dir.mkdir(ios_directory) + File.write(File.join(directory, "package.json"), JSON.generate(contents)) + Pod::Config.installation_root = ios_directory + yield + end +end + def flags_for(threadsafe) spec = evaluate_podspec( "NITRO_SQLITE_THREADSAFE" => threadsafe, @@ -123,11 +156,31 @@ def assert_optimization_flags(flags) ].each { |flag| assert_includes(flags, flag) } end -def assert_invalid_value_rejected - evaluate_podspec("NITRO_SQLITE_THREADSAFE" => "2") +def assert_invalid_package_value_rejected + with_app_package("nitroSQLite" => {"threadSafe" => 1}) do + evaluate_podspec("NITRO_SQLITE_THREADSAFE" => nil) + end + fail "Expected an invalid NITRO_SQLITE_THREADSAFE value to fail" +rescue RuntimeError => error + expected = "nitroSQLite.threadSafe in package.json must be true or false" + fail "Unexpected validation error: #{error.message}" unless error.message == expected +end + +def assert_invalid_package_config_rejected + with_app_package("nitroSQLite" => true) do + evaluate_podspec("NITRO_SQLITE_THREADSAFE" => nil) + end + fail "Expected an invalid nitroSQLite configuration to fail" +rescue RuntimeError => error + expected = "nitroSQLite in package.json must be an object" + fail "Unexpected validation error: #{error.message}" unless error.message == expected +end + +def assert_invalid_environment_value_rejected + with_app_package({}) { evaluate_podspec("NITRO_SQLITE_THREADSAFE" => "2") } fail "Expected an invalid NITRO_SQLITE_THREADSAFE value to fail" rescue RuntimeError => error - expected = "NITRO_SQLITE_THREADSAFE must be either 0 or 1" + expected = "NITRO_SQLITE_THREADSAFE must be true, false, 1, or 0" fail "Unexpected validation error: #{error.message}" unless error.message == expected end