diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5332ee65b7b6..039563fd0c25 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -95,6 +95,8 @@ RCTNewArchEnabled + RNNitroSQLite_DatabaseLocation + ApplicationSupport UIAppFonts ExpensifyNewKansas-Medium.otf diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a6f6309b529e..5ef8a0f1bf4f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3935,7 +3935,7 @@ PODS: - SocketRocket - Turf - Yoga - - RNNitroSQLite (9.6.0): + - RNNitroSQLite (9.8.1): - boost - DoubleConversion - fast_float @@ -5118,7 +5118,7 @@ SPEC CHECKSUMS: RNLiveMarkdown: 318b3defc20b75a21ca00d58342109230c044da5 RNLocalize: 05e367a873223683f0e268d0af9a8a8e6aed3b26 rnmapbox-maps: 8b7629ef3ae59dd96340470e568cbc7c08c54ff9 - RNNitroSQLite: a9b5965d511ed6e99ce903380e64934d043a0d2c + RNNitroSQLite: 06ff2497ab8b6c75b780f080f23c99555e7bf4a5 RNPermissions: 518f0a0c439acc74e2b9937e0e7d29e5031ae949 RNReactNativeHapticFeedback: 5f1542065f0b24c9252bd8cf3e83bc9c548182e4 RNReanimated: de3eaf0bd2aec5036554a125eb02c28b59de45ae diff --git a/package-lock.json b/package-lock.json index 29e82a430a0b..cd4d1281f05f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,7 +124,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-fetch": "1.5.4", "react-native-nitro-modules": "0.36.3", - "react-native-nitro-sqlite": "9.6.0", + "react-native-nitro-sqlite": "9.8.1", "react-native-onyx": "3.0.111", "react-native-pager-view": "9.0.4", "react-native-pdf": "7.0.2", @@ -36126,9 +36126,9 @@ } }, "node_modules/react-native-nitro-sqlite": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/react-native-nitro-sqlite/-/react-native-nitro-sqlite-9.6.0.tgz", - "integrity": "sha512-a/N1yGhM8RvCCnaYhEHhh35YS+HDOAcGKeKFsp2ExCzIjP8vPXuzQtHylgLQLeAh7rUaism5q0QQFfogXm1SXA==", + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/react-native-nitro-sqlite/-/react-native-nitro-sqlite-9.8.1.tgz", + "integrity": "sha512-5IjcynOKpxYpQ5SVWc0r4q7wSYQW5DGjDfU3068ytI0vmC0FsqY6PZyUNkrik4qN9LIbvSwLOxoWdFI8/+zRHg==", "license": "MIT", "dependencies": { "typeorm": "0.3.27" diff --git a/package.json b/package.json index 395a9c8e5bb3..2280bb41c41f 100644 --- a/package.json +++ b/package.json @@ -200,7 +200,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-fetch": "1.5.4", "react-native-nitro-modules": "0.36.3", - "react-native-nitro-sqlite": "9.6.0", + "react-native-nitro-sqlite": "9.8.1", "react-native-onyx": "3.0.111", "react-native-pager-view": "9.0.4", "react-native-pdf": "7.0.2", diff --git a/patches/react-native-nitro-sqlite/details.md b/patches/react-native-nitro-sqlite/details.md deleted file mode 100644 index 47198d20deda..000000000000 --- a/patches/react-native-nitro-sqlite/details.md +++ /dev/null @@ -1,35 +0,0 @@ -# react-native-nitro-sqlite patches - -### [react-native-nitro-sqlite+9.6.0+001+dont-mask-original-error-on-rollback-failure.patch](react-native-nitro-sqlite+9.6.0+001+dont-mask-original-error-on-rollback-failure.patch) - -- Reason: - - ``` - When a statement inside executeBatch fails and SQLite has already auto-rolled the transaction - back (disk-full I/O errors do this), the library's own ROLLBACK fails with "cannot rollback - - no transaction is active" and that error is thrown instead of the original one, hiding the real - failure (~2.8k masked log lines/day on iOS). The patch wraps both ROLLBACK calls in try/catch - so the original error is rethrown. - ``` - -- Upstream PR/issue: Already fixed upstream in 9.7.0 via https://github.com/margelo/react-native-nitro-sqlite/pull/292. We stay on 9.6.0 because the 9.7.0 podspec force-enables `SQLITE_THREADSAFE=0` on iOS and its new per-database queue breaks second opens of the same database (used by `src/libs/ExportOnyxState/index.native.ts`). The patch can be dropped when those are resolved and we bump. -- E/App issue: https://github.com/Expensify/App/issues/97908 -- PR introducing patch: https://github.com/Expensify/App/pull/97954 - -### [react-native-nitro-sqlite+9.6.0+002+store-database-outside-documents.patch](react-native-nitro-sqlite+9.6.0+002+store-database-outside-documents.patch) - -- Reason: - - ``` - The library stores SQLite databases in the iOS Documents directory, which is exposed to users - via the Files app when file sharing is enabled. This patch stores databases in - Library/Application Support instead (persistent, backed up, never user-visible) and migrates - databases created by older app versions out of Documents on first launch. The database and its - -wal/-shm journal files are copied as a set before the originals are deleted, and if the copy - fails the database keeps being opened from Documents and the migration retries on the next - launch, so committed writes are never separated from their WAL. - ``` - -- Upstream PR/issue: https://github.com/margelo/react-native-nitro-sqlite/issues/289 -- E/App issue: https://github.com/Expensify/App/issues/96649 -- PR introducing patch: https://github.com/Expensify/App/pull/96531 diff --git a/patches/react-native-nitro-sqlite/react-native-nitro-sqlite+9.6.0+001+dont-mask-original-error-on-rollback-failure.patch b/patches/react-native-nitro-sqlite/react-native-nitro-sqlite+9.6.0+001+dont-mask-original-error-on-rollback-failure.patch deleted file mode 100644 index 0dadc3344a4a..000000000000 --- a/patches/react-native-nitro-sqlite/react-native-nitro-sqlite+9.6.0+001+dont-mask-original-error-on-rollback-failure.patch +++ /dev/null @@ -1,30 +0,0 @@ -diff --git a/node_modules/react-native-nitro-sqlite/cpp/sqliteExecuteBatch.cpp b/node_modules/react-native-nitro-sqlite/cpp/sqliteExecuteBatch.cpp -index 4aca8b5..2ea8581 100644 ---- a/node_modules/react-native-nitro-sqlite/cpp/sqliteExecuteBatch.cpp -+++ b/node_modules/react-native-nitro-sqlite/cpp/sqliteExecuteBatch.cpp -@@ -51,7 +51,11 @@ SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::v - auto result = sqliteExecute(dbName, command.sql, command.params); - rowsAffected += result->getRowsAffected(); - } catch (NitroSQLiteException& e) { -- sqliteExecuteLiteral(dbName, "ROLLBACK"); -+ // A failed ROLLBACK (SQLite may have already rolled back on its own) must not mask the original error. -+ try { -+ sqliteExecuteLiteral(dbName, "ROLLBACK"); -+ } catch (...) { -+ } - throw e; - } - } -@@ -61,7 +65,11 @@ SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::v - .commands = (int)commandCount, - }; - } catch (NitroSQLiteException& e) { -- sqliteExecuteLiteral(dbName, "ROLLBACK"); -+ // A failed ROLLBACK (SQLite may have already rolled back on its own) must not mask the original error. -+ try { -+ sqliteExecuteLiteral(dbName, "ROLLBACK"); -+ } catch (...) { -+ } - throw e; - } - } diff --git a/patches/react-native-nitro-sqlite/react-native-nitro-sqlite+9.6.0+002+store-database-outside-documents.patch b/patches/react-native-nitro-sqlite/react-native-nitro-sqlite+9.6.0+002+store-database-outside-documents.patch deleted file mode 100644 index a98111df979c..000000000000 --- a/patches/react-native-nitro-sqlite/react-native-nitro-sqlite+9.6.0+002+store-database-outside-documents.patch +++ /dev/null @@ -1,95 +0,0 @@ -diff --git a/node_modules/react-native-nitro-sqlite/ios/OnLoad.mm b/node_modules/react-native-nitro-sqlite/ios/OnLoad.mm -index 6ce7258..65c29cb 100644 ---- a/node_modules/react-native-nitro-sqlite/ios/OnLoad.mm -+++ b/node_modules/react-native-nitro-sqlite/ios/OnLoad.mm -@@ -10,6 +10,63 @@ - using namespace margelo::nitro; - using namespace margelo::nitro::rnnitrosqlite; - -+// The Documents directory can be exposed to the user (Files app) when file sharing -+// is enabled, so databases are stored in Library/Application Support instead. -+// Databases created by older app versions are moved out of Documents on first launch. -+// -+// The "OnyxDB" name must match the database the app opens: DEFAULT_DB_NAME in -+// src/CONST/index.ts and DB_NAME in react-native-onyx's SQLiteProvider. A Jest test -+// (MoveFilesOutOfDocumentsTest) fails if the names drift apart. -+static void migrateDatabaseFiles(NSString *fromDirectory, NSString *toDirectory) { -+ NSFileManager *fileManager = [NSFileManager defaultManager]; -+ NSArray *databaseFiles = @[ @"OnyxDB", @"OnyxDB-wal", @"OnyxDB-shm" ]; -+ -+ if (![fileManager fileExistsAtPath:[fromDirectory stringByAppendingPathComponent:@"OnyxDB"]]) { -+ // Nothing to migrate. A previous run may have been interrupted after copying the set but -+ // before removing the journal files, so sweep any leftovers out of Documents. -+ [fileManager removeItemAtPath:[fromDirectory stringByAppendingPathComponent:@"OnyxDB-wal"] error:nil]; -+ [fileManager removeItemAtPath:[fromDirectory stringByAppendingPathComponent:@"OnyxDB-shm"] error:nil]; -+ return; -+ } -+ -+ // A database in Documents means an older build was writing there, so it is the live copy. -+ // Remove whatever sits at the destination (e.g. after a downgrade and re-upgrade) so a -wal -+ // from one database generation is never replayed into a database from another. -+ for (NSString *file in databaseFiles) { -+ [fileManager removeItemAtPath:[toDirectory stringByAppendingPathComponent:file] error:nil]; -+ } -+ -+ // Copy the whole set before deleting anything: committed-but-uncheckpointed writes live in -+ // the -wal file and SQLite only replays it when it sits next to its database, so the files -+ // must never be separated. If any copy fails, the intact originals in Documents keep being -+ // used (see the fallback in +load) and the migration retries on the next launch. -+ for (NSString *file in databaseFiles) { -+ NSString *sourcePath = [fromDirectory stringByAppendingPathComponent:file]; -+ if (![fileManager fileExistsAtPath:sourcePath]) { -+ continue; -+ } -+ -+ NSError *error = nil; -+ if (![fileManager copyItemAtPath:sourcePath toPath:[toDirectory stringByAppendingPathComponent:file] error:&error]) { -+ NSLog(@"Failed to migrate database file %@: %@", file, error.localizedDescription); -+ return; -+ } -+ } -+ -+ // The database file is deleted first, and the journals only once that succeeds: if the -+ // database cannot be removed, the existence check in +load keeps using the source database -+ // on this launch, so its -wal must stay next to it or committed writes would be lost. -+ // An interruption after the first delete can only leave journal files behind, which the -+ // sweep above removes on the next launch. -+ NSError *removeError = nil; -+ if (![fileManager removeItemAtPath:[fromDirectory stringByAppendingPathComponent:@"OnyxDB"] error:&removeError]) { -+ NSLog(@"Failed to remove migrated database from its old location: %@", removeError.localizedDescription); -+ return; -+ } -+ [fileManager removeItemAtPath:[fromDirectory stringByAppendingPathComponent:@"OnyxDB-wal"] error:nil]; -+ [fileManager removeItemAtPath:[fromDirectory stringByAppendingPathComponent:@"OnyxDB-shm"] error:nil]; -+} -+ - + (void)load { - // Get appGroupID value from Info.plist using key "AppGroup" - NSString *appGroupID = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RNNitroSQLite_AppGroup"]; -@@ -30,9 +80,24 @@ - - documentPath = [storeUrl path]; - } else { -- // Get iOS app's document directory (to safely store database .sqlite3 file) -- NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true); -+ // Store databases in Library/Application Support, which is persistent, backed up, -+ // and never exposed to the user via the Files app (unlike the Documents directory) -+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true); - documentPath = [paths objectAtIndex:0]; -+ -+ NSFileManager *fileManager = [NSFileManager defaultManager]; -+ if (![fileManager fileExistsAtPath:documentPath]) { -+ [fileManager createDirectoryAtPath:documentPath withIntermediateDirectories:YES attributes:nil error:nil]; -+ } -+ -+ NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) objectAtIndex:0]; -+ migrateDatabaseFiles(documentsDirectory, documentPath); -+ -+ // If the database could not be moved out of Documents, keep opening it there rather than -+ // creating a fresh empty one; the migration retries on the next launch. -+ if ([fileManager fileExistsAtPath:[documentsDirectory stringByAppendingPathComponent:@"OnyxDB"]]) { -+ documentPath = documentsDirectory; -+ } - } - - HybridNitroSQLite::docPath = [documentPath UTF8String]; diff --git a/tests/unit/MoveFilesOutOfDocumentsTest.ts b/tests/unit/MoveFilesOutOfDocumentsTest.ts index 0f92417f8531..b2736e6b3996 100644 --- a/tests/unit/MoveFilesOutOfDocumentsTest.ts +++ b/tests/unit/MoveFilesOutOfDocumentsTest.ts @@ -165,18 +165,12 @@ describe('MoveFilesOutOfDocuments migration (native)', () => { await expect(MoveFilesOutOfDocuments()).resolves.toBeUndefined(); }); - it('keeps the native database migration patch in sync with the database name', () => { - const patchDir = path.resolve(__dirname, '../../patches/react-native-nitro-sqlite'); - const patchFileName = fs.readdirSync(patchDir).find((fileName) => fileName.includes('store-database-outside-documents')); - expect(patchFileName).toBeDefined(); - const patchContent = fs.readFileSync(path.join(patchDir, String(patchFileName)), 'utf8'); - - // The patch migrates database files by name at app startup, before any JS runs, so the - // name is hardcoded there. If the database the app opens is ever renamed, the patch (and - // this test) must be updated with it, or the migration would silently strand user data. - expect(patchContent).toContain(`@"${CONST.DEFAULT_DB_NAME}"`); - + it('keeps the Onyx database name aligned with the upstream per-database migration', () => { const sqliteProviderContent = fs.readFileSync(path.resolve(__dirname, '../../node_modules/react-native-onyx/dist/storage/providers/SQLiteProvider.js'), 'utf8'); expect(sqliteProviderContent).toContain(`'${CONST.DEFAULT_DB_NAME}'`); + + // NitroSQLite now migrates each database when it opens, using the caller's database name. + const nitroSQLiteContent = fs.readFileSync(path.resolve(__dirname, '../../node_modules/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp'), 'utf8'); + expect(nitroSQLiteContent).toContain('return migrateDatabase(dbName,'); }); });