Skip to content

Make SQLite setup failures reportable and recoverable on the legacy (non-JSI) bridge - #5

Merged
joker-777 merged 5 commits into
kenhubfrom
claude/native-setup-errors-e35f76
Oct 31, 2026
Merged

joker-777 merged 5 commits into
kenhubfrom
claude/native-setup-errors-e35f76

Conversation

@joker-777

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

Copy link
Copy Markdown

Fixes two defects in the legacy (non-JSI) SQLite adapter path, i.e. SQLiteAdapter with jsi: false on iOS and Android. The JSI path (native/shared/DatabaseBridge.cpp) is unchanged.

Related upstream issues: Nozbe#1845 (app crashes at launch instead of reaching onSetUpError), Nozbe#1609 (adapter unusable after a failed migration).

Defect 1: setup failures crash instead of rejecting (iOS) / are uncaught (Android)

On iOS, WMDatabase, WMDatabaseDriver and WMDatabaseBridge raised NSExceptions inside async RCT_EXPORT_METHODs. React Native's TurboModule interop only converts an NSException to a JS error for synchronous methods; for async ones it re-throws and the process dies. So a database file that cannot be opened crash-loops at launch and the JS onSetUpError callback never runs. Android's setUpWithSchema had no try/catch, unlike initialize and setUpWithMigrations.

Fix: WMDatabase open / setUserVersion and the driver's setUpWithSchema / setUpWithMigrations report through NSError ** (same pattern as the other WMDatabase methods), driverWithName: / databaseWithPath: return nil with an error when the database cannot be opened, and the bridge rejects with the existing db.<method>.error codes. Android's setUpWithSchema is wrapped like setUpWithMigrations.

Defect 2: no driver after a failed migration

Both bridges called disconnectDriver after a failed migration, which removed the driver for the tag. On iOS it also removed the queue, so the operations queued during startup never settled and every later call, including unsafeResetDatabase, hung silently ([nil addObject:] is a no-op). On Android every later call was rejected with No driver with tag N available. The database itself is consistent after a failed migration (it runs in one transaction with rollback), so there was nothing preventing recovery except the missing driver.

Fix: on a failed schema setup or migration, the operations queued while waiting for setup are rejected with the setup error, and the driver stays connected so JS can recover (e.g. database.unsafeResetDatabase(), which is what the Kenhub companion does from its onSetUpError handler). If the database could not be opened at all, there is no driver to keep: iOS now rejects later calls instead of hanging, Android keeps a Connection.Failed state and rejects them with the open error.

On Android the bridge now creates the driver with the plain constructor and calls unsafeResetDatabase / migrate on it (so the driver instance survives a failure instead of being lost in a throwing constructor, which also leaked its open SQLiteDatabase). WMDatabaseDriver.migrate becomes package-private for that.

Failure paths, before and after

Path Before After
iOS: database file cannot be opened during initialize NSException OpenFailed → process crash, onSetUpError never runs initialize rejects with db.initialize.error; onSetUpError runs; later calls reject with No driver with tag N available
iOS: database cannot be (re)opened during setUpWithSchema / setUpWithMigrations NSException → crash promise rejects; queued operations rejected with the same error; later calls reject
iOS: schema SQL fails in setUpWithSchema NSException SetUpWithSchemaFailed → crash rejects with db.setUpWithSchema.error; queued operations rejected with the setup error; driver stays connected, unsafeResetDatabase works
iOS: migration SQL fails or user_version cannot be set rejected, but driver and queue removed: queued promises never settle, every later call (incl. unsafeResetDatabase) hangs forever rejects with db.setUpWithMigrations.error; queued operations rejected with the setup error; driver stays connected, unsafeResetDatabase works
iOS: DB version does not match the migration set's fromVersion NSException IncompatibleMigrations → crash rejects with Incompatible migration set applied. DB: X, migration: Y (same recovery as above)
iOS: unexpected WMDatabaseCompatibility in initialize NSException BadArgument → crash rejects with db.initialize.error
iOS: operation for a tag with neither driver nor queue silently dropped, promise never settles rejects with No driver with tag N available
Android: setUpWithSchema throws (open fails, schema SQL fails) uncaught exception from a @ReactMethod → native crash rejects; queued operations rejected with the setup error; driver stays connected if the DB opened
Android: setUpWithMigrations throws rejected, but driver removed: queued and all later calls rejected with No driver with tag N available rejects; queued operations rejected with the setup error; driver stays connected, unsafeResetDatabase works
Android: database cannot be opened in setUpWith* (crash / "No driver") rejects; later calls rejected with the open error (Connection.Failed)

Unchanged on purpose: Android initialize still throws IllegalStateException when the tag is already set up (iOS rejects), and the JSI path.

What was and was not verified

  • Android: verified on an emulator. native/androidTest (./gradlew connectedAndroidTest, React Native 0.74.6 test app, Pixel-class AVD, API 36 x86_64) was built with this branch and run with a dev bundle served by Metro. All 244 shared adapter tests pass in both the async-mode and JSI-mode configurations, including errors when migration fails with the new async-bridge assertions (queued operation rejected with the setup error, then unsafeResetDatabase, write and query on the same adapter) and all other migration/reset tests. The Java bridge changes compiled as part of that build. Two local, uncommitted tweaks were needed only to run on a modern emulator: adding x86_64 to the test app's abiFilters (the AVD has no 32-bit x86 ABI) and forwarding Metro with adb reverse.
  • iOS was not compiled here. This machine has no Xcode, so the iOS changes were reviewed by reading only. They mirror the NSError ** / BRIDGE_METHOD / WITH_DRIVER patterns already used in these files and the existing try/catch in the Android bridge. The iOS side still needs a build of the Kenhub companion (Expo SDK 58, React Native 0.88.1) or yarn test:ios on a Mac, ideally with a deliberately broken migration (and an unreadable database file) to exercise onSetUpError → unsafeResetDatabase.
  • The JS adapter itself is unchanged: it already handles a rejected setUpWithMigrations / setUpWithSchema (_initPromise rejects and devSetupCallback calls onSetUpError) and keeps dispatching to the same tag afterwards.
  • Regression test: the shared adapter test errors when migration fails (src/adapters/__tests__/commonTests.js) now also checks, for the asynchronous (non-JSI) SQLite dispatcher, that an operation issued while the failing migration runs is rejected with the setup error, and that the same adapter can then unsafeResetDatabase, write and query. It runs on device through src/adapters/sqlite/integrationTest.js (async-mode configuration; the JSI configuration is skipped because the JSI bridge asserts on operations before setup) and under jest through the sqlite-node bridge.
  • src/adapters/sqlite/sqlite-node/DatabaseBridge.js is the JS mirror of the native bridges and had the same disconnect-on-failure behaviour, so it now mirrors the fix (keep driver, reject queued operations with the setup error, catch setUpWithSchema). Verified under jest with Node 22: the new test fails against the old node bridge (No driver for with tag N available) and passes with the new one; the full src/adapters/sqlite suite (135 tests) passes, and eslint is clean.

🤖 Generated with Claude Code

…ridge

On the non-JSI native path, a database that could not be opened, a schema
setup that failed or a migration whose SQL failed either crashed the app
(iOS raised an NSException inside an async RCT_EXPORT_METHOD, which React
Native cannot convert to a promise rejection; Android's setUpWithSchema had
no try/catch) or left the adapter unusable (both platforms disconnected the
driver after a failed migration, so iOS dropped the queued operations and
hung every later call, and Android rejected every later call with "No
driver with tag N available"). The JS onSetUpError callback therefore never
ran on iOS, and on neither platform could the app recover by calling
database.unsafeResetDatabase().

iOS: WMDatabase open and setUserVersion, and the driver's setUpWithSchema
and setUpWithMigrations, now report through NSError, and the bridge rejects
the promise. Android: setUpWithSchema is wrapped like setUpWithMigrations.

Both: after a failed schema setup or migration, the operations queued while
waiting for setup are rejected with the setup error, and the driver stays
connected (setup runs in a transaction, so the database is consistent), so
unsafeResetDatabase and other calls keep working. If the database could not
be opened at all, later calls are rejected (iOS: "No driver with tag N
available" instead of hanging; Android: with the open error).

The JSI path (native/shared/DatabaseBridge.cpp) is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 07:07

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 critical issue remains: failed database reopening can still publish a non-nil driver as connected, causing later operations to use an unopened database and potentially preventing reset retries.

1 open issue
What changed in this PR

This PR improves setup-error handling and recovery in the legacy, non-JSI SQLite bridges on iOS and Android. The JSI path is unchanged.

Changes:

  • Converts native setup failures into rejected promises.
  • Rejects queued operations after setup failure while retaining available drivers for recovery.
  • Adds Android connection-failure state and documents the behavior.
File Description
native/​ios/​WatermelonDB/​objc/​WMDatabaseDriver.m Propagates initialization, schema, and migration errors.
native/​ios/​WatermelonDB/​objc/​WMDatabaseDriver.h Declares error-reporting driver APIs.
native/​ios/​WatermelonDB/​objc/​WMDatabaseBridge.m Rejects setup failures and settles queued operations.
native/​ios/​WatermelonDB/​objc/​WMDatabase.m Reports open, reopen, and version-update errors.
native/​ios/​WatermelonDB/​objc/​WMDatabase.h Declares error-reporting database APIs.
native/​android/​src/​main/​java/​com/​nozbe/​watermelondb/​WMDatabaseDriver.java Makes migration execution accessible to the bridge.
native/​android/​src/​main/​java/​com/​nozbe/​watermelondb/​WMDatabaseBridge.java Catches setup failures and preserves drivers for recovery.
native/​android/​src/​main/​java/​com/​nozbe/​watermelondb/​Connection.java Adds a failed connection state carrying the setup error.
CHANGELOG-Unreleased.md Documents legacy SQLite error handling and recovery.

Review effort: Balanced

If setUpWithSchema deleted the database file and reopening it failed, the
driver was still published as connected, so later operations ran against a
closed database. Treat such a driver like a missing one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 07:16

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

An async-bridge regression test is still needed on iOS and Android to verify queued-operation rejection during failed migration and successful reset, write, and query using the same adapter/tag.

1 open issue
1 resolved since last review

Review effort: Balanced

The shared adapter test "errors when migration fails" now also checks, for
the asynchronous (non-JSI) SQLite dispatcher, that an operation issued
while the failing migration runs is rejected with the setup error instead
of hanging, and that the same adapter can then reset the database and
write and query again. It runs on device through
src/adapters/sqlite/integrationTest.js and under jest through the
sqlite-node bridge.

sqlite-node/DatabaseBridge.js is the JS mirror of the native bridges and
still disconnected the driver after a failed migration (and let
setUpWithSchema throw), so it gets the same treatment: setup failures are
rejected, queued operations are rejected with the setup error, and the
driver stays connected when its database could be opened.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 07:53

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

The native iOS and Android changes lack build and device validation for setup-error rejection, queued-operation settlement, and reset recovery, including iOS database-open failures.

1 open issue

Review effort: Balanced

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

Native builds and on-device failure-injection checks are still needed to validate iOS/Android promise rejection and database reset recovery.

1 open issue

Review effort: Balanced

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

Schema-setup failures still leave Android reset unable to recover from an empty database and the Node bridge treating a closed SQLite handle as connected.

2 open issues
1 resolved since last review

Review effort: Balanced

Android: after unsafeDestroyEverything the schema transaction may fail,
leaving sqlite_master empty. The next reset then crashed in getAllTables,
which read from the empty cursor without checking moveToFirst.

Node bridge: keep the driver after a failed setup only if its SQLite handle
is actually open, matching the iOS bridge; unsafeDestroyEverything can throw
between closing the handle and reopening it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 08: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

The native bridge changes need iOS and Android builds and on-device failure-path tests to validate promise rejection, queued-operation settlement, and reset recovery.

1 open issue
1 resolved since last review

Review effort: Balanced

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

The iOS and Android bridge failure/recovery paths still require native builds and on-device validation of async rejection, queued-operation settlement, and same-tag database reset recovery.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

…-errors-e35f76

# Conflicts:
#	CHANGELOG-Unreleased.md
Copilot AI balanced review requested due to automatic review settings October 31, 2026 09:04

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

The iOS/Android bridge changes remain uncompiled and untested on-device, requiring native validation of failure handling, queued promise settlement, and same-adapter reset recovery.

No open issues 🎉

Review effort: Balanced

@joker-777
joker-777 merged commit e62f5cd into kenhub Oct 31, 2026
1 check passed
@joker-777

Copy link
Copy Markdown
Author

Offered upstream as Nozbe#1981, rebuilt on upstream master.

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