Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/content/docs/guides/load-sql-file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ INSERT INTO tags (name) VALUES ('home');

The loader counts executed lines in `commands` and adds their affected row counts in `rowsAffected`. It rolls back when a statement fails. Choose `loadFileAsync()` for a larger file to keep the database work off the JavaScript thread. Use [batch operations](/docs/guides/batch-operations) when statements need bound parameters or when you already have the commands in JavaScript.

## Import failures

SQLite can reject a statement or fail to commit the import. NitroSQLite attempts one rollback after a failed import transaction, preserving the source path and the original SQL error. Statement failures also include the one-based file line and failing command. A failed BEGIN does not trigger rollback of an existing transaction. If rollback itself fails, its error is appended to the original import error.

The managed methods throw or reject with `NitroSQLiteError` and native category `CouldNotLoadFile`. After a successful rollback, completed statements from the file are discarded and the connection can be used again. The error includes the failing SQL text, so avoid including sensitive literal values in files whose errors may be logged.

Both methods return a [`FileLoadResult`](/api/react-native-nitro-sqlite/interfaces/FileLoadResult) with optional `commands` and `rowsAffected` numbers.
4 changes: 4 additions & 0 deletions example/ios/NitroSQLiteExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
7B5735B8E367752583C44170 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
A7EB6D102DCB00010003AF01 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7EB6D0F2DCB00010003AF01 /* SceneDelegate.swift */; };
D463F2F53B094E469119C69E /* load-file-with-error.sql in Resources */ = {isa = PBXBuildFile; fileRef = D463F2F53B094E469119C69F /* load-file-with-error.sql */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -29,6 +30,7 @@
A7EB6D0F2DCB00010003AF01 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = NitroSQLiteExample/SceneDelegate.swift; sourceTree = "<group>"; };
C3E89C92C507841F368080BD /* Pods_NitroSQLiteExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NitroSQLiteExample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
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 = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -46,6 +48,7 @@
13B07FAE1A68108700A75B9A /* NitroSQLiteExample */ = {
isa = PBXGroup;
children = (
D463F2F53B094E469119C69F /* load-file-with-error.sql */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
761780EC2CA45674006654EE /* AppDelegate.swift */,
4A947A180A327B2E26F1CC50 /* BenchmarkMemory.m */,
Expand Down Expand Up @@ -164,6 +167,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 */,
Expand Down
7 changes: 7 additions & 0 deletions example/ios/NitroSQLiteExample/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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()
Expand Down
2 changes: 2 additions & 0 deletions example/ios/NitroSQLiteExample/load-file-with-error.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
INSERT INTO LoadFileRegression (value) VALUES ('rolled back');
THIS IS NOT VALID SQL;
2 changes: 2 additions & 0 deletions example/tests/unit/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { beforeEach, describe } from '../TestApi'
import { setupTestDb } from './common'
import registerLoadFileUnitTests from './specs/operations/loadFile.spec'
import registerExecuteUnitTests from './specs/operations/execute.spec'
import registerTransactionUnitTests from './specs/operations/transaction.spec'
import registerExecuteBatchUnitTests from './specs/operations/executeBatch.spec'
Expand All @@ -16,6 +17,7 @@ export function registerUnitTests() {
describe('operations/execute.spec.ts', registerExecuteUnitTests)
describe('operations/transaction.spec.ts', registerTransactionUnitTests)
describe('operations/executeBatch.spec.ts', registerExecuteBatchUnitTests)
describe('operations/loadFile.spec.ts', registerLoadFileUnitTests)
describe(
'operations/preparedStatement.spec.ts',
registerPreparedStatementUnitTests,
Expand Down
53 changes: 53 additions & 0 deletions example/tests/unit/specs/operations/loadFile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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', () => {
if (typeof loadFileFixturePath !== 'string') {
throw new Error('SQL import fixture path is missing')
}
testDb.execute(
'CREATE TABLE LoadFileRegression (value TEXT NOT NULL) STRICT;',
)

let errorMessage: string | undefined
try {
testDb.loadFile(loadFileFixturePath)
} 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 }])
})
})
}
74 changes: 47 additions & 27 deletions packages/react-native-nitro-sqlite/cpp/NitroSQLiteImportSqlFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,62 @@
#include "NitroSQLiteOperations.hpp"
#include <fstream>
#include <iostream>
#include <optional>

namespace margelo::nitro::rnnitrosqlite {

SQLiteOperationResult importSqlFile(const SQLiteConnectionPtr& connection, const std::string& fileLocation) {
std::lock_guard lock(connection->mutex);
std::string line;
std::ifstream sqFile(fileLocation);
if (sqFile.is_open()) {
try {
int rowsAffected = 0;
int commands = 0;
sqliteExecuteCommand(connection, "BEGIN EXCLUSIVE TRANSACTION");
while (std::getline(sqFile, line, '\n')) {
if (!line.empty()) {
try {
SQLiteOperationResult result = sqliteExecuteCommand(connection, line);
rowsAffected += result.rowsAffected;
commands++;
} catch (NitroSQLiteException& e) {
sqliteExecuteCommand(connection, "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<int> commandLine;

try {
sqliteExecuteCommand(connection, command);
transactionStarted = true;

std::string line;
while (std::getline(sqFile, line, '\n')) {
lineNumber++;
if (!line.empty()) {
command = line;
commandLine = lineNumber;
SQLiteOperationResult result = sqliteExecuteCommand(connection, command);
rowsAffected += result.rowsAffected;
commands++;
}
}

sqFile.close();
sqliteExecuteCommand(connection, "COMMIT");
return {.rowsAffected = rowsAffected, .commands = commands};
} catch (...) {
sqFile.close();
sqliteExecuteCommand(connection, "ROLLBACK");
throw NitroSQLiteException(NitroSQLiteExceptionType::UnknownError, "Unexpected error. Transaction was rolled back");
command = "COMMIT";
commandLine.reset();
sqliteExecuteCommand(connection, 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(connection, "ROLLBACK");
} catch (const std::exception& rollbackError) {
errorContext += ". ROLLBACK failed: " + std::string(rollbackError.what());
}
}

throw NitroSQLiteException::CouldNotLoadFile(fileLocation, errorContext);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ namespace margelo::nitro::rnnitrosqlite {

struct SQLiteConnection;

/** Import one nonempty line per statement in an exclusive transaction.
* Holds the retained connection's lock for the entire import.
* File and SQL failures throw CouldNotLoadFile with the path and original SQL/line context.
* Attempts rollback only after BEGIN succeeds; rollback errors are appended.
*/
SQLiteOperationResult importSqlFile(const std::shared_ptr<SQLiteConnection>& connection, const std::string& fileLocation);

} // namespace margelo::nitro::rnnitrosqlite
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,17 @@ export interface NitroSQLite
* @param dbName Name of an open database.
* @param location Path to the SQL file.
* @returns Number of executed commands and affected rows.
* @throws An Error with native category CouldNotLoadFile on an import
* failure, including the path, failing SQL/line, and any rollback failure.
* Completed commands are discarded when rollback succeeds.
*/
loadFile(dbName: string, location: string): FileLoadResult
/** Import a SQL file on a background thread.
* @param dbName Name of an open database.
* @param location Path to the SQL file.
* @returns A promise of the command and affected row counts.
* @throws An Error with native category CouldNotLoadFile, preserving
* the original import error and any rollback failure. Rejects asynchronously.
*/
loadFileAsync(dbName: string, location: string): Promise<FileLoadResult>
}
6 changes: 6 additions & 0 deletions packages/react-native-nitro-sqlite/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,16 @@ export interface NitroSQLiteConnection {
* `location` is a path to the SQL file; multi-line statements are unsupported.
* @param location Path to the SQL file.
* @returns Number of executed commands and affected rows.
* @throws NitroSQLiteError with native category CouldNotLoadFile on an import
* failure, including the path, failing SQL/line, and any rollback failure.
* Completed commands are discarded when rollback succeeds.
*/
loadFile(location: string): FileLoadResult
/** Queue the file import and resolve with its command and row counts.
* @param location Path to the SQL file.
* @returns A promise of the command and affected row counts.
* @throws NitroSQLiteError with native category CouldNotLoadFile, preserving
* the original import error and any rollback failure. Rejects asynchronously.
*/
loadFileAsync(location: string): Promise<FileLoadResult>
}
Expand Down