Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
92744b3
fix: wait for asynchronous row batch delivery
chrispader Oct 23, 2026
5a8e3f1
chore(example): replace react-native-quick-crypto with buffer
chrispader Oct 22, 2026
38dbe78
feat!: migrate to Nitro V1
chrispader Oct 22, 2026
3bee6c9
ci: pause the podspec thread-safety test
chrispader Oct 22, 2026
57bff2d
build: publish built JavaScript so apps don't compile the specs
chrispader Oct 22, 2026
f48b970
build(ios)!: build with Swift Package Manager
chrispader Oct 22, 2026
fb786ed
docs: document the Nitro V1 setup
chrispader Oct 22, 2026
b4b1317
refactor: rename the prepared statement hybrid object
chrispader Oct 22, 2026
c33bc75
style: format the renamed prepared statement files
chrispader Oct 22, 2026
537737b
refactor: replace NitroSQLiteOnLoad with a platform context
chrispader Oct 22, 2026
73023ec
refactor: shorten SQLite type and helper names
chrispader Oct 22, 2026
22b9d30
test(docs): validate all canonical alias exports
chrispader Oct 22, 2026
14812ba
refactor: encode query results with AnyJSON
chrispader Oct 26, 2026
6fb0ccb
perf: write whole-number REAL values as JSON integers
chrispader Oct 28, 2026
690ab14
perf(ios): format fractional numbers with std::to_chars on iOS 16.3+
chrispader Oct 28, 2026
3749d0a
perf: convert small query results without parsing JSON
chrispader Oct 28, 2026
2b5aaee
perf: move copied parameters into background operations
chrispader Oct 28, 2026
f49b40a
chore(example): benchmark async reads and fractional REAL values
chrispader Oct 28, 2026
c6208b1
docs: list the Nitro V1 optimizations and the PRs they belong to
chrispader Oct 28, 2026
045b7f4
docs: point the benchmark change at its main PR
chrispader Oct 28, 2026
00a12a2
chore(example): benchmark app workloads
chrispader Oct 29, 2026
519e5ad
docs: list the app workload benchmarks and their main PR
chrispader Oct 29, 2026
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
21 changes: 13 additions & 8 deletions .github/ci-paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@
".github/actions/**",
".github/ci-paths.json",
"scripts/ci*",
"packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseMigration.*",
"packages/react-native-nitro-sqlite/cpp/NitroSQLiteDatabaseConnections.*",
"packages/react-native-nitro-sqlite/cpp/NitroSQLiteExecuteBatch.*",
"packages/react-native-nitro-sqlite/cpp/NitroSQLiteOperations.*",
"packages/react-native-nitro-sqlite/cpp/NitroSQLiteStatementGroup.hpp",
"packages/react-native-nitro-sqlite/cpp/NitroSQLiteStatementTail.hpp",
"packages/react-native-nitro-sqlite/cpp/SQLiteDatabaseMigration.*",
"packages/react-native-nitro-sqlite/cpp/SQLiteDatabaseConnections.*",
"packages/react-native-nitro-sqlite/cpp/SQLiteExecuteBatch.*",
"packages/react-native-nitro-sqlite/cpp/SQLiteJSON.*",
"packages/react-native-nitro-sqlite/cpp/SQLiteOperations.*",
"packages/react-native-nitro-sqlite/cpp/SQLiteQueryResults.*",
"packages/react-native-nitro-sqlite/cpp/SQLiteStatementGroup.hpp",
"packages/react-native-nitro-sqlite/cpp/SQLiteStatementTail.hpp",
"packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.*",
"packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3-symbol-prefix.h",
"packages/react-native-nitro-sqlite-vec/cpp/**",
Expand Down Expand Up @@ -132,20 +134,23 @@
"packages/react-native-nitro-sqlite-vec/cpp/**",
"packages/react-native-nitro-sqlite/RNNitroSQLite.podspec",
"packages/react-native-nitro-sqlite-vec/RNNitroSqliteVec.podspec",
"example/Gemfile*",
"packages/react-native-nitro-sqlite/Package.swift",
"packages/react-native-nitro-sqlite-vec/Package.swift",
"bun.lock",
"package.json",
"example/package.json",
"packages/react-native-nitro-sqlite/package.json",
"packages/react-native-nitro-sqlite-vec/package.json",
"patches/react-native-nitro-modules*",
"patches/@react-native-harness*",
"patches/react-native-screens*",
"patches/@react-native-clipboard*",
"example/react-native.config.js",
"packages/react-native-nitro-sqlite/react-native.config.js",
"packages/react-native-nitro-sqlite-vec/react-native.config.js",
"packages/react-native-nitro-sqlite/nitro.json",
"!packages/react-native-nitro-sqlite/src/__tests__/**",
"scripts/test-podspec-threadsafe.rb",
"scripts/sqlite-package-config.test.ts",
"example/index.js",
"example/babel.config*.js",
"example/app.json",
Expand Down
20 changes: 11 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ jobs:
android_configuration: ${{ steps.select.outputs.android_configuration }}
ios: ${{ steps.select.outputs.ios }}
macos: ${{ steps.select.outputs.macos }}
ios_pods: ${{ steps.pods.outputs.ios_pods }}
macos_pods: ${{ steps.pods.outputs.macos_pods }}
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -89,7 +88,6 @@ jobs:
name: ci-lockfiles
path: |
bun.lock
example/ios/Podfile.lock
example/macos/Podfile.lock
retention-days: 1
- name: Require committed dependency resolutions
Expand All @@ -99,7 +97,7 @@ jobs:
git diff --exit-code -- bun.lock
- name: Select native lockfile checks
id: pods
if: steps.select.outputs.dependencies == 'true' && (steps.select.outputs.ios == 'true' || steps.select.outputs.macos == 'true')
if: steps.select.outputs.dependencies == 'true' && steps.select.outputs.macos == 'true'
run: node scripts/ci-dependencies.ts
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || github.sha }}
Expand Down Expand Up @@ -129,6 +127,9 @@ jobs:
(cd packages/react-native-nitro-sqlite-vec && ../../node_modules/.bin/jest --coverage --runInBand)
(cd example && ../node_modules/.bin/jest --config jest.mocha.config.js --runInBand --watchman=false)
node --test example/macos/scripts/test-macos.test.mjs
- name: Build the library package
if: steps.select.outputs.typescript == 'true'
run: bun --cwd packages/react-native-nitro-sqlite run build
- name: Format C++
if: steps.select.outputs.cpp_format == 'true'
run: |
Expand All @@ -137,9 +138,9 @@ jobs:
- name: Test C++ database lifecycle and SQLite symbols
if: steps.select.outputs.cpp_tests == 'true'
run: bash scripts/test-cpp.sh
- name: Test SQLite thread-safety pod configuration
- name: Test SQLite Swift package configuration
if: steps.select.outputs.ios == 'true'
run: ruby scripts/test-podspec-threadsafe.rb
run: node --test scripts/sqlite-package-config.test.ts
- name: Check documentation
if: steps.select.outputs.docs == 'true'
run: |
Expand All @@ -149,13 +150,12 @@ jobs:
lockfiles:
name: Native lockfiles
needs: checks
# Paused until Nitro V1 supports React Native macOS. Remove `false &&` to resume it.
if: >-
false &&
github.event_name != 'push' && !github.event.pull_request.draft &&
(needs.checks.outputs.ios_pods == 'true' || needs.checks.outputs.macos_pods == 'true')
needs.checks.outputs.macos_pods == 'true'
uses: ./.github/workflows/update-lockfiles.yml
with:
ios: ${{ needs.checks.outputs.ios_pods == 'true' }}
macos: ${{ needs.checks.outputs.macos_pods == 'true' }}

android:
name: Android
Expand Down Expand Up @@ -185,7 +185,9 @@ jobs:
macos:
name: macOS
needs: [checks, lockfiles]
# Paused until Nitro V1 supports React Native macOS. Remove `false &&` to resume it.
if: >-
false &&
!cancelled() && needs.checks.result == 'success' &&
(needs.lockfiles.result == 'success' || needs.lockfiles.result == 'skipped') &&
github.event_name != 'push' && !github.event.pull_request.draft &&
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,19 @@ jobs:
bun run sync:lockfile
bun run check:lockfile
bun install --frozen-lockfile
# The macOS steps are paused until Nitro V1 supports React Native macOS.
- uses: ruby/setup-ruby@v1
if: false
with:
ruby-version: 3.3.0
bundler-cache: true
working-directory: example
- name: Select Xcode
if: false
run: sudo xcode-select -s "/Applications/Xcode_26.5.app/Contents/Developer"
- name: Update iOS and macOS lockfiles
run: |
bun example pods
bun example pods:macos
- name: Update macOS lockfile
if: false
run: bun example pods:macos
- name: Commit lockfiles and optional announcement
run: node scripts/commit-release-lockfiles.ts
env:
Expand Down
32 changes: 14 additions & 18 deletions .github/workflows/test-harness-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,16 @@ permissions:

jobs:
test:
name: iOS integration tests, ${{ matrix.config.name }}, Xcode ${{ matrix.config.xcode }}
runs-on: macos-26
name: iOS integration tests, Xcode ${{ matrix.config.xcode }}
# Nitro V1 requires Swift 6.4, which GitHub provides in its Xcode 27 preview image.
runs-on: xcode-27
timeout-minutes: 40
strategy:
matrix:
config: ${{ fromJSON(inputs.full && '[{"name":"static_frameworks","frameworks":"static","xcode":"26.5"},{"name":"no_frameworks","frameworks":"","xcode":"26.5"},{"name":"static_frameworks","frameworks":"static","xcode":"26.2"}]' || '[{"name":"static_frameworks","frameworks":"static","xcode":"26.5"},{"name":"no_frameworks","frameworks":"","xcode":"26.5"}]') }}
config: ${{ fromJSON(inputs.full && '[{"xcode":"27.0"},{"xcode":"27.1"}]' || '[{"xcode":"27.0"}]') }}
env:
USE_CCACHE: 1
USE_FRAMEWORKS: ${{ matrix.config.frameworks }}
IOS_SIMULATOR_NAME: iPhone 17 Pro
IOS_SIMULATOR_OS: ${{ matrix.config.xcode }}
IOS_SIMULATOR_NAME: iPhone 18 Pro
IOS_SIMULATOR_OS: '27.0'
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
Expand All @@ -41,7 +40,7 @@ jobs:
uses: hendrikmuhs/[email protected]
with:
max-size: 500M
key: ${{ runner.os }}-${{ matrix.config.name }}-ccache-example-ios-xcode${{ matrix.config.xcode }}
key: ${{ runner.os }}-ccache-example-ios-xcode${{ matrix.config.xcode }}
create-symlink: true
- name: Setup ccache behavior
run: |
Expand All @@ -50,13 +49,6 @@ jobs:
echo "CCACHE_DEPEND=true" >> $GITHUB_ENV
echo "CCACHE_INODECACHE=true" >> $GITHUB_ENV

- name: Setup Ruby (bundle)
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
working-directory: example

- name: Select Xcode ${{ matrix.config.xcode }}
run: sudo xcode-select -s "/Applications/Xcode_${{ matrix.config.xcode }}.app/Contents/Developer"

Expand All @@ -79,9 +71,9 @@ jobs:
xcrun simctl boot "$simulator_udid"
fi

- name: Install Pods
- name: Generate Swift packages
working-directory: example
run: bun pods
run: bun spm

- name: Build App
working-directory: example/ios
Expand All @@ -90,7 +82,7 @@ jobs:
xcodebuild \
CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ \
-derivedDataPath build/DerivedData -UseModernBuildSystem=YES \
-workspace NitroSQLiteExample.xcworkspace \
-project NitroSQLiteExample.xcodeproj \
-scheme NitroSQLiteExample \
-sdk iphonesimulator \
-configuration Debug \
Expand All @@ -104,6 +96,10 @@ jobs:
timeout-minutes: 10
run: xcrun simctl bootstatus "$IOS_SIMULATOR_UDID" -b

# react-native-harness 1.5.0 fails on Xcode 27, where `simctl appinfo` errors for apps that are not installed.
- name: Install app on the iOS simulator
run: xcrun simctl install "$IOS_SIMULATOR_UDID" example/ios/build/DerivedData/Build/Products/Debug-iphonesimulator/NitroSQLiteExample.app

- name: Run iOS integration tests
uses: callstackincubator/[email protected]
env:
Expand Down
21 changes: 1 addition & 20 deletions .github/workflows/update-lockfiles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@ name: Check native lockfiles

on:
workflow_call:
inputs:
ios:
type: boolean
default: false
macos:
type: boolean
default: false

permissions:
contents: read
Expand Down Expand Up @@ -38,12 +31,7 @@ jobs:
working-directory: example
- name: Select Xcode 26.5
run: sudo xcode-select -s '/Applications/Xcode_26.5.app/Contents/Developer'
- name: Refresh iOS lockfile
if: inputs.ios
working-directory: example
run: bun pods
- name: Refresh macOS lockfile
if: inputs.macos
working-directory: example
run: bun pods:macos
- name: Attach refreshed native lockfiles
Expand All @@ -52,17 +40,10 @@ jobs:
with:
name: native-lockfiles
path: |
example/ios/Podfile.lock
example/macos/Podfile.lock
example/Gemfile.lock
retention-days: 1
- name: Require committed native dependency resolutions
env:
CHECK_IOS: ${{ inputs.ios }}
CHECK_MACOS: ${{ inputs.macos }}
run: |
lockfiles=()
if [[ "$CHECK_IOS" == true ]]; then lockfiles+=(example/ios/Podfile.lock); fi
if [[ "$CHECK_MACOS" == true ]]; then lockfiles+=(example/macos/Podfile.lock); fi
ruby scripts/ci-pod-lockfiles.rb "${lockfiles[@]}"
ruby scripts/ci-pod-lockfiles.rb example/macos/Podfile.lock
git diff --exit-code -- example/Gemfile.lock
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ To run the example app on Android:
bun example android
```

To run the example app on iOS:
To run the example app on iOS, generate its Swift packages first. Run this again after changing native dependencies:

```sh
bun example spm
bun example ios
```

Expand All @@ -49,7 +50,7 @@ Remember to add tests for your change if possible. Run the unit tests by:
bun test
```

To edit the Objective-C files, open `example/ios/SequelExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-nitro-sqlite`.
To edit the Objective-C++ and C++ files, open `example/ios/NitroSQLiteExample.xcodeproj` in Xcode and find the source files in the `RNNitroSQLite` package.

To edit the Kotlin files, open `example/android` in Android studio and find the source files at `rnnitrosqlite` under `Android`.

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ If you use a coding agent, give it the [NitroSQLite skill](https://github.com/ma

## Installation

Requires React Native 0.75 or newer and `react-native-nitro-modules` 0.37.1 or newer.
Requires React Native 0.88 or newer and Nitro V1. iOS builds use Swift Package Manager and require Xcode with Swift 6.4 or newer.

```sh
npm install react-native-nitro-sqlite react-native-nitro-modules
npm install react-native-nitro-sqlite [email protected]
npx react-native spm
```

Rebuild the native app after installing. Expo projects need a development build; Expo Go cannot load this native module. See [Getting Started](https://sqlite.margelo.com/docs) for details.
Expand Down
61 changes: 61 additions & 0 deletions V1_OPTIMIZATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Nitro V1 optimizations

This branch combines the whole Nitro V1 stack with the optimizations below, so they can be benchmarked together against `main`. Each optimization is a separate commit. After benchmarking, cherry-pick the commits worth keeping into the PR listed for each one, then rebase the layers above it with `gh stack rebase --upstack`.

The stack in this branch, bottom to top: #465, #456, #453, #457, #458, #459, #460, #462, #463, #467.

## Changes

### 1. Write whole-number REAL values as JSON integers

- Commit: `perf: write whole-number REAL values as JSON integers`
- Target: #467 (`nitro-v1/any-json`)
- Benchmark: `300k wide rows · full read + access`, which was 20% slower than `main` on the iPhone 16

REAL values that hold whole numbers below 2^53 now go through the 64-bit integer formatter. On Apple platforms this replaces SQLite's formatter, which took about 40 ns per value against 7 ns on a Mac, and it drops the `.0` that SQLite appended. The wide benchmark table stores whole numbers in all four REAL columns, so each read formats 1.2 million of them. Android also gains a little, because integer formatting is about twice as fast as `std::to_chars` for doubles.

### 2. Format fractional numbers with `std::to_chars` on iOS 16.3 and later

- Commit: `perf(ios): format fractional numbers with std::to_chars on iOS 16.3+`
- Target: #467
- Benchmark: `100k fractional REAL rows · full read + access` (new) on iOS

Apple's libc++ only allows floating-point `std::to_chars` from iOS 16.3, and the package targets iOS 16.0. The encoder now looks the function up at runtime with `dlsym` and keeps SQLite's formatter for iOS 16.0 to 16.2. That roughly halves the formatting time for fractional values. The alternative is to raise the iOS minimum to 16.3 and call `std::to_chars` directly, which drops iOS 16.0 to 16.2. That decision is still open, and this commit becomes unnecessary if the minimum goes up.

### 3. Convert small results directly instead of parsing JSON

- Commit: `perf: convert small query results without parsing JSON`
- Target: #467
- Benchmarks: `1k indexed lookups · prepared`, which was 4% and 11% slower than `main` on the Galaxy S22 and Pixel 7a, and `WAL · 100 writes + 100 reads on separate connections`, which was 4% slower on the iPhone 16

Every result went through `JSON.parse`, including the empty result of each write and single-row lookups. A result batch now keeps native values until it holds more than 32 cells, then encodes the rows read so far as JSON and continues in JSON. Empty results skip conversion. The limit is `kNativeCellLimit` in `cpp/SQLiteQueryResults.cpp`. The device benchmarks put the break-even at about 20 to 30 cells, so retune it if the new numbers point elsewhere. Streamed batches also reserve the previous batch's JSON size up front. The result tests now run each edge case with 1 row and with 40 rows, so both conversion paths stay covered.

### 4. Move copied parameters into background operations

- Commit: `perf: move copied parameters into background operations`
- Target: #453 (`@chrispader/feat/nitro-v1`)
- Benchmarks: async inserts and async batches

`executeAsync` and prepared `executeAsync` copied their parameters twice before the worker thread used them, and `executeBatchAsync` copied every command three times. Each parameter is now copied once. In #453 the result type is still `NitroSQLiteQueryResult` and the prepared statement file is still `HybridNitroSQLitePreparedStatement.cpp`, so the cherry-pick needs those names.

### 5. Benchmark async reads and fractional REAL values

- Commit: `chore(example): benchmark async reads and fractional REAL values`
- Target: #469 (`chore/benchmark-read-cases`), merged into `main`. The stack picks it up when it is rebased onto `main`.

Adds `10k narrow rows · async read + access`, `300k wide rows · async read + access`, and `100k fractional REAL rows · full read + access`. Synchronous reads encode and parse results on the JavaScript thread, while asynchronous reads encode them on the connection's worker thread, which is where AnyJSON should help most. The existing cases keep their order and labels.

### 6. Benchmark app workloads

- Commit: `chore(example): benchmark app workloads`
- Target: #470 (`chore/benchmark-real-world`) against `main`. Drop this commit when the stack is rebased onto `main`, because #470 brings the same cases.

Adds WAL-mode cases modeled on apps: a chat app that stores 500 synced messages in a transaction, opens its inbox and 10 conversations, and launches 20 times, plus key-value storage, a response cache with 200 JSON documents of 9 KB, and a thumbnail cache with 120 BLOBs of 32 KB. It also adds async point lookups and synchronous inserts inside one transaction. On `main`, #470 also runs the chat cases through Drizzle and reads rows as arrays with `executeRaw`. This branch has neither API yet, so those cases are missing here. The response cache and thumbnail cases test two weak spots of the JSON encoding in #467: JSON documents stored as text are full of quotes the encoder must escape, and every BLOB is patched in after parsing.

## Not changed

- Asynchronous transactions on Android were 6% slower on the Galaxy S22 and 27% slower on the Pixel 7a. Both Nitro versions resolve promises through the same React Native call invoker with the same thread hops, and no code change accounts for the extra 60 µs per statement on the Pixel. Changes 3 and 4 remove some work per statement, but finding the cause needs a Perfetto trace of `main` and V1 on that phone.
- Synchronous batches still copy their parameters once. Removing that copy needs a non-owning batch type across `SQLiteExecuteBatch` and the statement group helper, for about 1% of the batch time.
- Every result builds a metadata object per column. Caching or skipping it would change the public result shape.
- Coalescing promise resolutions or briefly spinning the connection worker would cut thread wake-up latency for asynchronous statements, but needs a bigger design change or costs CPU time.
- The newest Nitro V1 nightly (102) has no changes to the C++ promise, callback, or AnyJSON code, and it renames `nitro.json` keys.
Loading