From eadcd97121e80e3eb015e98cc3f7208803687b20 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 1 Feb 2026 15:28:35 +0100 Subject: [PATCH] Update datamodel upgrade logic for iOS to fix transaction and pragma conflicts (#1576) --- .../RCTNativeVaultManager.mm | 4 ++++ .../ios/NativeVaultManager/VaultManager.swift | 11 +++++++++ apps/mobile-app/ios/Podfile.lock | 8 +++---- .../ios/VaultStoreKit/VaultStore+Query.swift | 24 +++++++++++++++---- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm index eff9bc5c8..9c6a12bff 100644 --- a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm +++ b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm @@ -61,6 +61,10 @@ [vaultManager rollbackTransaction:resolve rejecter:reject]; } +- (void)persistAndMarkDirty:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [vaultManager persistAndMarkDirty:resolve rejecter:reject]; +} + - (void)getAuthMethods:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [vaultManager getAuthMethods:resolve rejecter:reject]; } diff --git a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift index 1ed67fe78..59708f57e 100644 --- a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift +++ b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift @@ -309,6 +309,17 @@ public class VaultManager: NSObject { } } + @objc + func persistAndMarkDirty(_ resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + do { + try vaultStore.persistAndMarkDirty() + resolve(nil) + } catch { + reject("PERSIST_ERROR", "Failed to persist and mark dirty: \(error.localizedDescription)", error) + } + } + @objc func deriveKeyFromPassword(_ password: String, salt: String, diff --git a/apps/mobile-app/ios/Podfile.lock b/apps/mobile-app/ios/Podfile.lock index f11bd5423..220dd1bda 100644 --- a/apps/mobile-app/ios/Podfile.lock +++ b/apps/mobile-app/ios/Podfile.lock @@ -2534,8 +2534,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 + boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 + DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 Expo: a64c53f1ebaa36c439a87874759b6690785e60a4 ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6 @@ -2558,8 +2558,8 @@ SPEC CHECKSUMS: ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 07309209b7b914451b8f822544a18e2a0a85afff - fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6 - glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a + fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd + glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 44bb6fe76a6eb400d3a992e2d0b21946ae999fa9 Macaw: 7af8ea57aa2cab35b4a52a45e6f85eea753ea9ae OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2 diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Query.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Query.swift index b6ddc494b..4b3ec0774 100644 --- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Query.swift +++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Query.swift @@ -78,6 +78,9 @@ extension VaultStore { } /// Execute a raw SQL command on the database without parameters (for DDL operations like CREATE TABLE). + /// + /// Note: Migration SQL scripts handle their own transactions and PRAGMA statements. + /// PRAGMA foreign_keys statements MUST be executed outside of transactions to take effect. public func executeRaw(_ query: String) throws { guard let dbConnection = self.dbConnection else { throw NSError(domain: "VaultStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"]) @@ -89,14 +92,13 @@ extension VaultStore { for statement in statements { let trimmedStatement = statement.smartTrim() - // Skip empty statements and transaction control statements (handled externally) - if trimmedStatement.isEmpty || - trimmedStatement.uppercased().hasPrefix("BEGIN TRANSACTION") || - trimmedStatement.uppercased().hasPrefix("COMMIT") || - trimmedStatement.uppercased().hasPrefix("ROLLBACK") { + // Skip empty statements and SQL comments + if trimmedStatement.isEmpty || trimmedStatement.hasPrefix("--") { continue } + // Execute all statements including PRAGMA and transaction control + // This allows migration SQL to properly control its own transactions and PRAGMA settings try dbConnection.execute(trimmedStatement) } } @@ -198,6 +200,18 @@ extension VaultStore { try dbConnection.execute("ROLLBACK") } + /// Persist the in-memory database to encrypted storage and mark as dirty. + /// Used after migrations where SQL handles its own transactions but we need to persist and sync. + /// This does NOT commit any SQL transaction - it just persists the current state of the database. + public func persistAndMarkDirty() throws { + try persistDatabaseToEncryptedStorage() + + // Atomically mark vault as dirty and increment mutation sequence + // This ensures sync can properly detect local changes + setIsDirty(true) + _ = incrementMutationSequence() + } + // MARK: - Items (Using Repository Pattern) /// Get all items from the database using the new field-based model.