Repository navigation
Make SQLite setup failures reportable and recoverable on the legacy (non-JSI) bridge - #5
Conversation
…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>
There was a problem hiding this comment.
🟡 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>
There was a problem hiding this comment.
🟡 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>
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>
There was a problem hiding this comment.
🔵 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
|
Offered upstream as Nozbe#1981, rebuilt on upstream master. |


Fixes two defects in the legacy (non-JSI) SQLite adapter path, i.e.
SQLiteAdapterwithjsi: falseon 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,WMDatabaseDriverandWMDatabaseBridgeraisedNSExceptions inside asyncRCT_EXPORT_METHODs. React Native's TurboModule interop only converts anNSExceptionto 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 JSonSetUpErrorcallback never runs. Android'ssetUpWithSchemahad notry/catch, unlikeinitializeandsetUpWithMigrations.Fix:
WMDatabase open/setUserVersionand the driver'ssetUpWithSchema/setUpWithMigrationsreport throughNSError **(same pattern as the otherWMDatabasemethods),driverWithName:/databaseWithPath:returnnilwith an error when the database cannot be opened, and the bridgerejects with the existingdb.<method>.errorcodes. Android'ssetUpWithSchemais wrapped likesetUpWithMigrations.Defect 2: no driver after a failed migration
Both bridges called
disconnectDriverafter 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, includingunsafeResetDatabase, hung silently ([nil addObject:]is a no-op). On Android every later call was rejected withNo 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 itsonSetUpErrorhandler). 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 aConnection.Failedstate and rejects them with the open error.On Android the bridge now creates the driver with the plain constructor and calls
unsafeResetDatabase/migrateon it (so the driver instance survives a failure instead of being lost in a throwing constructor, which also leaked its openSQLiteDatabase).WMDatabaseDriver.migratebecomes package-private for that.Failure paths, before and after
initializeNSExceptionOpenFailed→ process crash,onSetUpErrornever runsinitializerejects withdb.initialize.error;onSetUpErrorruns; later calls reject withNo driver with tag N availablesetUpWithSchema/setUpWithMigrationsNSException→ crashsetUpWithSchemaNSExceptionSetUpWithSchemaFailed→ crashdb.setUpWithSchema.error; queued operations rejected with the setup error; driver stays connected,unsafeResetDatabaseworksuser_versioncannot be setunsafeResetDatabase) hangs foreverdb.setUpWithMigrations.error; queued operations rejected with the setup error; driver stays connected,unsafeResetDatabaseworksfromVersionNSExceptionIncompatibleMigrations→ crashIncompatible migration set applied. DB: X, migration: Y(same recovery as above)WMDatabaseCompatibilityininitializeNSExceptionBadArgument→ crashdb.initialize.errorNo driver with tag N availablesetUpWithSchemathrows (open fails, schema SQL fails)@ReactMethod→ native crashsetUpWithMigrationsthrowsNo driver with tag N availableunsafeResetDatabaseworkssetUpWith*Connection.Failed)Unchanged on purpose: Android
initializestill throwsIllegalStateExceptionwhen the tag is already set up (iOS rejects), and the JSI path.What was and was not verified
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, includingerrors when migration failswith the new async-bridge assertions (queued operation rejected with the setup error, thenunsafeResetDatabase, 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: addingx86_64to the test app'sabiFilters(the AVD has no 32-bit x86 ABI) and forwarding Metro withadb reverse.NSError **/BRIDGE_METHOD/WITH_DRIVERpatterns already used in these files and the existingtry/catchin the Android bridge. The iOS side still needs a build of the Kenhub companion (Expo SDK 58, React Native 0.88.1) oryarn test:ioson a Mac, ideally with a deliberately broken migration (and an unreadable database file) to exerciseonSetUpError→unsafeResetDatabase.setUpWithMigrations/setUpWithSchema(_initPromiserejects anddevSetupCallbackcallsonSetUpError) and keeps dispatching to the same tag afterwards.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 thenunsafeResetDatabase, write and query. It runs on device throughsrc/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 thesqlite-nodebridge.src/adapters/sqlite/sqlite-node/DatabaseBridge.jsis 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, catchsetUpWithSchema). 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 fullsrc/adapters/sqlitesuite (135 tests) passes, and eslint is clean.🤖 Generated with Claude Code