Repository navigation
Make JSI SQLite setup and reset failures reject instead of crashing - #12
Conversation
- Schema setup, migration and unsafeResetDatabase() failures on the JSI path reject the promise (reaching onSetUpError) instead of aborting the app - An unopenable database file is reported through the rejected setup promise instead of terminating the process - Calling a native method with the wrong argument count throws a JS error on Android too - The prepared statement cache is bounded to 256 statements with LRU eviction; statements in use are never evicted Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Database recovery reads an uninitialized flag and leaves successful resets uninitialized, while failed SQLite opens can leak handles.
2 open issues
What changed in this PR
This PR brings JSI SQLite error handling closer to the legacy bridges by reporting native setup and reset failures to JavaScript rather than terminating the app.
Changes:
- Routes open, setup, reset, and argument-count errors through JavaScript error handling.
- Adds least-recently-used statement caching with pinning to protect statements in use.
- Adds mocked dispatcher regression tests and documents the behavior changes.
| File | Description |
|---|---|
src/adapters/sqlite/makeDispatcher/test.js |
Tests error propagation and reset retries using mocked native adapters. |
src/adapters/sqlite/makeDispatcher/index.native.js |
Captures adapter-open errors and rejects subsequent calls. |
native/shared/Sqlite.h |
Defines movable statement guards with cache pinning. |
native/shared/Sqlite.cpp |
Throws open errors by value and implements statement pin tracking. |
native/shared/JSIHelpers.h |
Routes argument-count errors through the platform error wrapper. |
native/shared/DatabaseBridge.cpp |
Replaces setup and reset aborts with JavaScript errors. |
native/shared/Database.h |
Defines the statement cache structure and eviction interface. |
native/shared/Database.cpp |
Updates cache cleanup and clears reset mode after vacuum failure. |
native/shared/Database-turboSync.cpp |
Uses pinned statement guards during sync loading. |
native/shared/Database-sqlite.cpp |
Implements cache eviction and guarded statement preparation. |
native/shared/Database-batch.cpp |
Uses pinned statement guards for batch operations. |
CHANGELOG-Unreleased.md |
Documents JSI error handling and statement caching changes. |
Review effort: Balanced
…sqlite handle - unsafeResetDatabase() and unsafeClose() no longer assert that setup succeeded, so the app can recover after a failed schema setup or migration; a successful reset marks the database initialized - Database and SqliteDb initialize their state flags explicitly - A sqlite handle returned by a failed sqlite3_open() is closed before the error is thrown Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
A failed reset leaves the public Database API using ErrorAdapter and blocks retries, while the added tests do not exercise real native recovery or the public reset wrapper.
2 open issues
2 resolved since last review
Review effort: Balanced
…covery - database.unsafeResetDatabase() restores the adapter and clears record caches when the reset fails, so it can be retried - JSI calls on a database whose setup or reset failed (or that was closed) reject instead of failing an assertion - Shared adapter tests now check recovery after a failed migration and a failed reset on every SQLite dispatcher, including JSI - Fix a Flow refinement error in the JSI dispatcher Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- All adapters (JSI, Android, iOS, sqlite-node, LokiJS) clear their cached record IDs at the start of unsafeResetDatabase() - A reset that fails early no longer leaves later fetches failing with "Record ID ... was sent over the bridge, but it's not cached" - Regression test through the public Database API with a failing LokiJS reset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Empty or comment-only SQL is rejected instead of failing an assertion on a missing statement - Record queries whose first column is not the record id are rejected instead of failing an assertion (or reading the wrong column as id) - Shared adapter test covers both on the JSI dispatcher Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the review summary on e8762d0 ("potential native process aborts on empty or comment-only SQL"): fixed in d1b4c62, together with the other asserts that SQL from |
There was a problem hiding this comment.
🔵 Needs a closer look
Human assessment is needed because a schema-encoding failure before reset dispatch can leave JavaScript and driver caches inconsistent, causing subsequent record fetches to fail.
No open issues 🎉
3 resolved since last review
Review effort: Balanced
|
Re the review summary on d1b4c62 ("a schema-encoding failure before reset dispatch can leave JavaScript and driver caches inconsistent"): no code change. |


Fixes the JSI (C++) path of
SQLiteAdapter(jsi: true), which the Kenhub companion now uses on Android, so setup failures reject the way the legacy bridges do after #5 instead of killing the process. Related upstream issue: Nozbe#1945.unsafeResetDatabase()failures now reject the promise and reachonSetUpError, instead ofstd::abort()database.unsafeResetDatabase(). Other calls on it reject until a reset succeeds, instead of failing an assertiondatabase.unsafeResetDatabase()restores the adapter and clears record caches, so the reset can be retried through the public API (this applies to every adapter)idfirst) is rejected instead of failing a native assertionLeft as is: the
std::abort()inconfigureJNI(DatabasePlatformAndroid.cpp) whenGetJavaVMfails. It runs while the JSI binding is installed, before any adapter or promise exists, and a failingGetJavaVMmeans the JNI environment itself is broken.Verification:
xcodebuild: Unable to find a device matching the provided destination specifier(iPhone 16 / iOS 18.1). This happens on every CI run in the fork, so the cause is the workflow's simulator destination on the current macOS runner, not this change. The C++ was therefore not built for iOS.clang++ -fsyntax-onlyagainst the JSI and simdjson headers, and a standalone copy of the LRU/pinning logic ran against sqlite3 under AddressSanitizer.yarn eslint,yarn flow,tslint, andyarn jestall pass. Jest ran with the sqlite-node suite (src/adapters/sqlite/test.js) excluded, because better-sqlite3 does not build on the host Node, so CI's JavaScript jobs cover that suite.🤖 Generated with Claude Code