From 98b4c78a30d3a42e457bb735b4be548e890ec339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=C3=ACnh=20Huy?= Date: Sat, 22 Aug 2026 15:58:11 +0700 Subject: [PATCH] fix: preserve loadFile transaction errors Give importSqlFile one transaction-finalization path, retain failing command context, and append rollback failures without masking the primary cause. Closes #308. --- .../project.pbxproj | 4 + .../ios/NitroSQLiteExample/AppDelegate.swift | 7 ++ .../load-file-with-error.sql | 2 + example/tests/unit/index.ts | 2 + .../unit/specs/operations/loadFile.spec.ts | 51 +++++++++++++ .../cpp/importSqlFile.cpp | 74 ++++++++++++------- 6 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 example/ios/NitroSQLiteExample/load-file-with-error.sql create mode 100644 example/tests/unit/specs/operations/loadFile.spec.ts diff --git a/example/ios/NitroSQLiteExample.xcodeproj/project.pbxproj b/example/ios/NitroSQLiteExample.xcodeproj/project.pbxproj index ead4c5bc..5ffa7897 100644 --- a/example/ios/NitroSQLiteExample.xcodeproj/project.pbxproj +++ b/example/ios/NitroSQLiteExample.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 7B5735B8E367752583C44170 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + D463F2F53B094E469119C69E /* load-file-with-error.sql in Resources */ = {isa = PBXBuildFile; fileRef = D463F2F53B094E469119C69F /* load-file-with-error.sql */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -24,6 +25,7 @@ 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = NitroSQLiteExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = NitroSQLiteExample/LaunchScreen.storyboard; sourceTree = ""; }; C3E89C92C507841F368080BD /* Pods_NitroSQLiteExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NitroSQLiteExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D463F2F53B094E469119C69F /* load-file-with-error.sql */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "load-file-with-error.sql"; path = "NitroSQLiteExample/load-file-with-error.sql"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -42,6 +44,7 @@ 13B07FAE1A68108700A75B9A /* NitroSQLiteExample */ = { isa = PBXGroup; children = ( + D463F2F53B094E469119C69F /* load-file-with-error.sql */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 761780EC2CA45674006654EE /* AppDelegate.swift */, 13B07FB61A68108700A75B9A /* Info.plist */, @@ -158,6 +161,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D463F2F53B094E469119C69E /* load-file-with-error.sql in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 7B5735B8E367752583C44170 /* PrivacyInfo.xcprivacy in Resources */, diff --git a/example/ios/NitroSQLiteExample/AppDelegate.swift b/example/ios/NitroSQLiteExample/AppDelegate.swift index d352d7d0..141cd201 100644 --- a/example/ios/NitroSQLiteExample/AppDelegate.swift +++ b/example/ios/NitroSQLiteExample/AppDelegate.swift @@ -14,6 +14,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { + if let loadFileFixturePath = Bundle.main.path( + forResource: "load-file-with-error", + ofType: "sql" + ) { + UserDefaults.standard.set(loadFileFixturePath, forKey: "loadFileFixturePath") + } + let delegate = ReactNativeDelegate() let factory = RCTReactNativeFactory(delegate: delegate) delegate.dependencyProvider = RCTAppDependencyProvider() diff --git a/example/ios/NitroSQLiteExample/load-file-with-error.sql b/example/ios/NitroSQLiteExample/load-file-with-error.sql new file mode 100644 index 00000000..eb93fd86 --- /dev/null +++ b/example/ios/NitroSQLiteExample/load-file-with-error.sql @@ -0,0 +1,2 @@ +INSERT INTO LoadFileRegression (value) VALUES ('rolled back'); +THIS IS NOT VALID SQL; diff --git a/example/tests/unit/index.ts b/example/tests/unit/index.ts index 5a51835b..b963771a 100644 --- a/example/tests/unit/index.ts +++ b/example/tests/unit/index.ts @@ -3,6 +3,7 @@ import { setupTestDb } from './common' import registerExecuteUnitTests from './specs/operations/execute.spec' import registerTransactionUnitTests from './specs/operations/transaction.spec' import registerExecuteBatchUnitTests from './specs/operations/executeBatch.spec' +import registerLoadFileUnitTests from './specs/operations/loadFile.spec' import registerTypeORMUnitTestsSpecs from './specs/typeorm.spec' import registerDatabaseQueueUnitTests from './specs/DatabaseQueue.spec' import registerSqliteVecUnitTestsSpecs from './specs/sqlite-vec.spec' @@ -14,6 +15,7 @@ export function registerUnitTests() { registerExecuteUnitTests() registerTransactionUnitTests() registerExecuteBatchUnitTests() + registerLoadFileUnitTests() }) registerDatabaseQueueUnitTests() diff --git a/example/tests/unit/specs/operations/loadFile.spec.ts b/example/tests/unit/specs/operations/loadFile.spec.ts new file mode 100644 index 00000000..66c3b813 --- /dev/null +++ b/example/tests/unit/specs/operations/loadFile.spec.ts @@ -0,0 +1,51 @@ +import { Platform, Settings } from 'react-native' +import { expect } from '@tests/unit/common' +import { describe, it } from '@tests/TestApi' +import { testDb } from '@tests/db' + +export default function registerLoadFileUnitTests() { + if (Platform.OS !== 'ios') { + return + } + + const loadFileFixturePath = Settings.get('loadFileFixturePath') + + describe('loadFile', () => { + it('preserves the SQL error context and rolls back the completed commands', () => { + expect(loadFileFixturePath).toBeTypeOf('string') + testDb.execute( + 'CREATE TABLE LoadFileRegression (value TEXT NOT NULL) STRICT;', + ) + + let errorMessage: string | undefined + try { + testDb.loadFile(loadFileFixturePath as string) + } catch (error) { + if (!(error instanceof Error)) { + throw error + } + + errorMessage = error.message + } + + expect(errorMessage).toContain('Could not load file:') + expect(errorMessage).toContain('load-file-with-error.sql') + expect(errorMessage).toContain('line 2') + expect(errorMessage).toContain('THIS IS NOT VALID SQL;') + expect(errorMessage).toContain('syntax error') + + const rollbackResult = testDb.execute( + 'SELECT COUNT(*) AS count FROM LoadFileRegression;', + ) + expect(rollbackResult.rows?._array).toEqual([{ count: 0 }]) + + testDb.execute( + "INSERT INTO LoadFileRegression (value) VALUES ('connection remains usable');", + ) + const usableConnectionResult = testDb.execute( + 'SELECT COUNT(*) AS count FROM LoadFileRegression;', + ) + expect(usableConnectionResult.rows?._array).toEqual([{ count: 1 }]) + }) + }) +} diff --git a/packages/react-native-nitro-sqlite/cpp/importSqlFile.cpp b/packages/react-native-nitro-sqlite/cpp/importSqlFile.cpp index 16857ad4..21e963bd 100644 --- a/packages/react-native-nitro-sqlite/cpp/importSqlFile.cpp +++ b/packages/react-native-nitro-sqlite/cpp/importSqlFile.cpp @@ -7,41 +7,61 @@ #include "operations.hpp" #include #include +#include namespace margelo::rnnitrosqlite { SQLiteOperationResult importSqlFile(const std::string& dbName, const std::string& fileLocation) { - std::string line; std::ifstream sqFile(fileLocation); - if (sqFile.is_open()) { - try { - int rowsAffected = 0; - int commands = 0; - sqliteExecuteCommand(dbName, "BEGIN EXCLUSIVE TRANSACTION"); - while (std::getline(sqFile, line, '\n')) { - if (!line.empty()) { - try { - SQLiteOperationResult result = sqliteExecuteCommand(dbName, line); - rowsAffected += result.rowsAffected; - commands++; - } catch (NitroSQLiteException& e) { - sqliteExecuteCommand(dbName, "ROLLBACK"); - sqFile.close(); - throw NitroSQLiteException::CouldNotLoadFile(fileLocation, "Transaction was rolled back"); - } - } + if (!sqFile.is_open()) { + throw NitroSQLiteException::CouldNotLoadFile(fileLocation); + } + + int rowsAffected = 0; + int commands = 0; + int lineNumber = 0; + bool transactionStarted = false; + std::string command = "BEGIN EXCLUSIVE TRANSACTION"; + std::optional commandLine; + + try { + sqliteExecuteCommand(dbName, command); + transactionStarted = true; + + std::string line; + while (std::getline(sqFile, line, '\n')) { + lineNumber++; + if (!line.empty()) { + command = line; + commandLine = lineNumber; + SQLiteOperationResult result = sqliteExecuteCommand(dbName, command); + rowsAffected += result.rowsAffected; + commands++; } + } - sqFile.close(); - sqliteExecuteCommand(dbName, "COMMIT"); - return {.rowsAffected = rowsAffected, .commands = commands}; - } catch (...) { - sqFile.close(); - sqliteExecuteCommand(dbName, "ROLLBACK"); - throw NitroSQLiteException(NitroSQLiteExceptionType::UnknownError, "Unexpected error. Transaction was rolled back"); + command = "COMMIT"; + commandLine.reset(); + sqliteExecuteCommand(dbName, command); + transactionStarted = false; + return {.rowsAffected = rowsAffected, .commands = commands}; + } catch (const std::exception& primaryError) { + std::string errorContext; + if (commandLine) { + errorContext = "line " + std::to_string(*commandLine) + " failed to execute `" + command + "`: " + primaryError.what(); + } else { + errorContext = "Failed to execute `" + command + "`: " + primaryError.what(); } - } else { - throw NitroSQLiteException::CouldNotLoadFile(fileLocation); + + if (transactionStarted) { + try { + sqliteExecuteCommand(dbName, "ROLLBACK"); + } catch (const std::exception& rollbackError) { + errorContext += ". ROLLBACK failed: " + std::string(rollbackError.what()); + } + } + + throw NitroSQLiteException::CouldNotLoadFile(fileLocation, errorContext); } }