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
6 changes: 6 additions & 0 deletions .github/workflows/build-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"

Expand All @@ -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
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,29 @@ 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. Configure it in your app's `package.json`:

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

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:
Expand Down
3 changes: 3 additions & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 25 additions & 11 deletions packages/react-native-nitro-sqlite/RNNitroSQLite.podspec
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))
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 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|
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"]
Expand All @@ -35,13 +54,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",
Expand Down
239 changes: 239 additions & 0 deletions scripts/test-podspec-threadsafe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
#!/usr/bin/env ruby

require "shellwords"
require "tmpdir"
require "json"

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

class Config
class << self
attr_accessor :installation_root

def instance
self
end
end
end
end

def min_ios_version_supported
"13.4"
end

def install_modules_dependencies(_spec) end

def main
default_flags = with_app_package({}) { flags_for(nil) }
assert_threadsafe(default_flags, "1")
assert_optimization_flags(default_flags)

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 = with_app_package("nitroSQLite" => {"threadSafe" => false}) do
flags_for("true")
end
assert_threadsafe(safe_flags, "1")
assert_optimization_flags(safe_flags)

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,
"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_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 true, false, 1, or 0"
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 <stdio.h>\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
Loading