Skip to content

Make JSI SQLite setup and reset failures reject instead of crashing - #12

Merged
joker-777 merged 5 commits into
kenhubfrom
claude/jsi-crash-paths-40d942
Oct 31, 2026
Merged

joker-777 merged 5 commits into
kenhubfrom
claude/jsi-crash-paths-40d942

Conversation

@joker-777

@joker-777 joker-777 commented Oct 31, 2026 •

Copy link
Copy Markdown

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.

  • Schema setup, migration and unsafeResetDatabase() failures now reject the promise and reach onSetUpError, instead of std::abort()
  • After a failed setup or reset the connection stays open, so the app can recover with database.unsafeResetDatabase(). Other calls on it reject until a reset succeeds, instead of failing an assertion
  • A failed database.unsafeResetDatabase() restores the adapter and clears record caches, so the reset can be retried through the public API (this applies to every adapter)
  • Every adapter clears its cached record IDs at the start of a reset, so a reset that fails early does not leave the adapter and JS record caches out of sync
  • A database file that cannot be opened is reported through the rejected setup promise, instead of a thrown pointer that no handler catches terminating the process
  • Calling a native method with the wrong number of arguments throws a JS error on Android too
  • Raw SQL the bridge cannot run (empty or comment-only, or a record query not selecting id first) is rejected instead of failing a native assertion
  • The prepared statement cache is bounded to 256 statements with least-recently-used eviction. Statements held by a caller are pinned and never evicted mid-use

Left as is: the std::abort() in configureJNI (DatabasePlatformAndroid.cpp) when GetJavaVM fails. It runs while the JSI binding is installed, before any adapter or promise exists, and a failing GetJavaVM means the JNI environment itself is broken.

Verification:

  • Android CI builds the C++ and runs the shared adapter suite on an emulator in async and JSI mode. The shared tests now also check recovery after a failed migration and after a failed reset on JSI, and they pass there.
  • iOS CI fails before building: 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.
  • Local: the C++ was not compiled with the NDK or Xcode here. It was syntax-checked with clang++ -fsyntax-only against the JSI and simdjson headers, and a standalone copy of the LRU/pinning logic ran against sqlite3 under AddressSanitizer. yarn eslint, yarn flow, tslint, and yarn jest all 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

- 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>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 22:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread native/shared/DatabaseBridge.cpp Resolved
Comment thread native/shared/Sqlite.cpp Resolved
…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>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 22:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread native/shared/DatabaseBridge.cpp Resolved
Comment thread native/shared/DatabaseBridge.cpp Resolved
…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>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 22:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A failed reset can clear JS models while preserving adapter-side cached IDs, leaving the caches inconsistent and causing subsequent reads of cached records to throw.

3 open issues

Review effort: Balanced

Comment thread src/Database/index.js Resolved
- 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>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 22:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed for cross-layer cache consistency after reset failures and potential native process aborts on empty or comment-only SQL.

3 open issues

Review effort: Balanced

- 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>
@joker-777

Copy link
Copy Markdown
Author

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 Q.unsafeSqlQuery can trigger. In prepareQuery, a statement that compiles to nothing (empty or comment-only SQL) now throws a JS error instead of hitting assert(statement != nullptr). In query, queryAsArray and queryIds, a first column other than id throws a JS error instead of failing an assertion, and release builds no longer read the wrong column as the id. count only requires a result column instead of asserting exactly one, matching the other bridges. The remaining asserts guard internal invariants or fixed SQL. New shared test "rejects raw SQL queries it cannot run (JSI)" runs on the Android device suite in JSI mode. CI on e8762d0: the JavaScript jobs (18 to 23) and Android pass. iOS fails at the simulator destination as on every fork run.

Copilot AI balanced review requested due to automatic review settings October 31, 2026 22:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

@joker-777

Copy link
Copy Markdown
Author

Re the review summary on d1b4c62 ("a schema-encoding failure before reset dispatch can leave JavaScript and driver caches inconsistent"): no code change. SQLiteAdapter.unsafeResetDatabase() encodes the app schema with encodeSchema(), and its only throw is in migration-step encoding (Unsupported migration step), which this path does not use. The schema has already passed appSchema() validation, and a schema that could not be encoded would fail first in setUpWithSchema, which encodes the same schema. Every failure that can happen once the reset reaches an adapter is covered by clearing the adapter's cached record IDs first (e8762d0), for all five adapters.

@joker-777
joker-777 merged commit 0a1615e into kenhub Oct 31, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants