Repository navigation
Conversation
…non-JSI) bridge * Make SQLite setup failures reportable and recoverable on the legacy bridge 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> * Do not keep an iOS driver whose database could not be reopened 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> * Test recovery after a failed migration and mirror it in the node bridge 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> * Let reset recover from a failed schema setup 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> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Oct 31, 2026
|
Hi @joker-777, thank you for such a thorough write-up, the before/after table is great! In case it's useful: I maintain NitromelonDB, a maintained fork of WatermelonDB (New Architecture, Nitro Modules). It's New Architecture only, so the legacy (non-JSI) bridge this PR fixes doesn't exist there anymore. If you're on the New Architecture, you'd be very welcome to give it a try (migration guide). The fork was also rewritten in TypeScript (strict mode) to reduce the chance of bugs slipping through. The JSI counterpart (#1983) does still apply to the fork, though, so I'll leave a note there too. Thanks! |
This was referenced Nov 10, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: #1845 (app crashes at launch instead of reaching
onSetUpError), #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()from the app'sonSetUpErrorhandler). 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 needsyarn 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