diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index b9ca999..a91a4db 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -49,6 +49,10 @@ add_library( ../cpp/JSIHelper.cpp ../cpp/ThreadPool.h ../cpp/ThreadPool.cpp + ../cpp/sqlfileloader.h + ../cpp/sqlfileloader.cpp + ../cpp/sqlbatchexecutor.h + ../cpp/sqlbatchexecutor.cpp cpp-adapter.cpp ) diff --git a/cpp/installer.cpp b/cpp/installer.cpp index 385fbf6..bc5dc3c 100644 --- a/cpp/installer.cpp +++ b/cpp/installer.cpp @@ -12,9 +12,9 @@ #include "logs.h" #include "JSIHelper.h" #include "ThreadPool.h" +#include "sqlfileloader.h" +#include "sqlbatchexecutor.h" #include -#include -#include #include using namespace std; @@ -23,55 +23,6 @@ using namespace facebook; string docPathStr; std::shared_ptr invoker; -/** - * Local function to handle SQL File Import in order to reuse with Sync and Async operations - */ -SequelBatchOperationResult importSQLFile(string dbName, string fileLocation) -{ - string line; - ifstream sqFile(fileLocation); - if (sqFile.is_open()) - { - try - { - int affectedRows = 0; - int commands = 0; - sequel_execute_literal_update(dbName, "BEGIN EXCLUSIVE TRANSACTION"); - while (std::getline(sqFile, line, '\n')) - { - if (!line.empty()) - { - SequelLiteralUpdateResult result = sequel_execute_literal_update(dbName, line); - if (result.type == SequelResultError) - { - sequel_execute_literal_update(dbName, "ROLLBACK"); - sqFile.close(); - return {SequelResultError, result.message, 0, commands}; - } - else - { - affectedRows += result.affectedRows; - commands++; - } - } - } - sqFile.close(); - sequel_execute_literal_update(dbName, "COMMIT"); - return {SequelResultOk, "", affectedRows, commands}; - } - catch (...) - { - sqFile.close(); - sequel_execute_literal_update(dbName, "ROLLBACK"); - return {SequelResultError, "[react-native-quick-sqlite][loadSQLFile] Unexpected error, transaction was rolledback", 0, 0}; - } - } - else - { - return {SequelResultError, "[react-native-quick-sqlite][loadSQLFile] Could not open file", 0, 0}; - } -} - jsi::Object createError(jsi::Runtime &rt, string message) { auto res = jsi::Object(rt); @@ -235,7 +186,7 @@ void install(jsi::Runtime &rt, std::shared_ptr jsCallInvoker // Converting results into a JSI Response auto jsiResult = createSequelQueryExecutionResult(rt, status, &results); - return jsiResult; + return move(jsiResult); }); // Execute a batch of SQL queries in a transaction @@ -251,80 +202,90 @@ void install(jsi::Runtime &rt, std::shared_ptr jsCallInvoker return createError(rt, "[react-native-quick-sqlite][execSQLBatch] - Incorrect parameter count"); } - const string dbName = args[0].asString(rt).utf8(rt); const jsi::Value ¶ms = args[1]; if (params.isNull() || params.isUndefined()) { return createError(rt, "[react-native-quick-sqlite][execSQLBatch] - An array of SQL commands or parameters is needed"); } + const string dbName = args[0].asString(rt).utf8(rt); + const jsi::Array &batchParams = params.asObject(rt).asArray(rt); + vector commands; + jsiBatchParametersToQuickArguments(rt, batchParams, &commands); + + auto batchResult = executeBatch(dbName, &commands); + if(batchResult.type == SequelResultOk) + { + auto res = jsi::Object(rt); + res.setProperty(rt, "status", jsi::Value(0)); + res.setProperty(rt, "rowsAffected", jsi::Value(batchResult.affectedRows)); + return move(res); + } else + { + return createError(rt, batchResult.message); + } + }); + + auto execSQLBatchAsync = jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "sequel_execSQLBatchAsync"), + 3, + [pool](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value + { + if (sizeof(args) < 3) + { + jsi::detail::throwJSError(rt, "[react-native-quick-sqlite][asyncExecuteSqlBatch] Incorrect parameter count"); + return {}; + } + + const jsi::Value ¶ms = args[1]; + const jsi::Value &callbackHolder = args[2]; + if(!callbackHolder.isObject() || !callbackHolder.asObject(rt).isFunction(rt)) { + jsi::detail::throwJSError(rt, "[react-native-quick-sqlite][asyncExecuteSqlBatch] The callback argument must be a function"); + return {}; + } + + if (params.isNull() || params.isUndefined()) + { + jsi::detail::throwJSError(rt, "[react-native-quick-sqlite][asyncExecuteSqlBatch] - An array of SQL commands or parameters is needed"); + return {}; + } - int rowsAffected = 0; + const string dbName = args[0].asString(rt).utf8(rt); const jsi::Array &batchParams = params.asObject(rt).asArray(rt); - try + auto callback = make_shared(callbackHolder.asObject(rt)); + + vector commands; + jsiBatchParametersToQuickArguments(rt, batchParams, &commands); + + auto task = + [&rt, dbName, commands = make_shared>(commands), callback]() { - sequel_execute(rt, dbName, "BEGIN TRANSACTION", jsi::Value::undefined()); - for (int i = 0; i < batchParams.length(rt); i++) + try { - const jsi::Array &command = batchParams.getValueAtIndex(rt, i).asObject(rt).asArray(rt); - if (command.length(rt) == 0) - { - sequel_execute(rt, dbName, "ROLLBACK", jsi::Value::undefined()); - return createError(rt, "[react-native-quick-sqlite][execSQLBatch] - No SQL Commands found on batch index " + std::to_string(i)); - } - const string query = command.getValueAtIndex(rt, 0).asString(rt).utf8(rt); - const jsi::Value &commandParams = command.length(rt) > 1 ? command.getValueAtIndex(rt, 1) : jsi::Value::undefined(); - if (!commandParams.isUndefined() && commandParams.asObject(rt).isArray(rt) && commandParams.asObject(rt).asArray(rt).length(rt) > 0 && commandParams.asObject(rt).asArray(rt).getValueAtIndex(rt, 0).isObject()) - { - // This arguments are an array of arrays, like a batch update of a single sql command. - const jsi::Array &batchUpdateParams = commandParams.asObject(rt).asArray(rt); - for (int x = 0; x < batchUpdateParams.length(rt); x++) - { - const jsi::Value &p = batchUpdateParams.getValueAtIndex(rt, x); - SequelResult result = sequel_execute(rt, dbName, query, p); - if (result.type == SequelResultError) - { - sequel_execute(rt, dbName, "ROLLBACK", jsi::Value::undefined()); - return createError(rt, result.message.c_str()); - } - else - { - if (result.value.getObject(rt).hasProperty(rt, jsi::PropNameID::forAscii(rt, "rowsAffected"))) - { - rowsAffected += result.value.getObject(rt).getProperty(rt, jsi::PropNameID::forAscii(rt, "rowsAffected")).asNumber(); - } - } - } - } - else + // Inside the new worker thread, we can now call sqlite operations + auto batchResult = executeBatch(dbName, commands.get()); + invoker->invokeAsync([&rt, batchResult = move(batchResult), callback] { - SequelResult result = sequel_execute(rt, dbName, query, commandParams); - if (result.type == SequelResultError) + if(batchResult.type == SequelResultOk) { - sequel_execute(rt, dbName, "ROLLBACK", jsi::Value::undefined()); - - return createError(rt, result.message.c_str()); - } - else + auto res = jsi::Object(rt); + res.setProperty(rt, "status", jsi::Value(0)); + res.setProperty(rt, "rowsAffected", jsi::Value(batchResult.affectedRows)); + callback->asObject(rt).asFunction(rt).call(rt, move(res)); + } else { - if (result.value.getObject(rt).hasProperty(rt, jsi::PropNameID::forAscii(rt, "rowsAffected"))) - { - rowsAffected += result.value.getObject(rt).getProperty(rt, jsi::PropNameID::forAscii(rt, "rowsAffected")).asNumber(); - } + callback->asObject(rt).asFunction(rt).call(rt, createError(rt, batchResult.message)); } - } + }); } - sequel_execute(rt, dbName, "COMMIT", jsi::Value::undefined()); - } - catch (...) - { - sequel_execute(rt, dbName, "ROLLBACK", jsi::Value::undefined()); - return createError(rt, "[react-native-quick-sqlite][execSQLBatch] - Unexpected error"); - } - - auto res = jsi::Object(rt); - res.setProperty(rt, "status", jsi::Value(0)); - res.setProperty(rt, "rowsAffected", jsi::Value(rowsAffected)); - return move(res); + catch (std::exception &exc) + { + invoker->invokeAsync([&rt, callback, &exc] + { callback->asObject(rt).asFunction(rt).call(rt, createError(rt, exc.what())); }); + } + }; + pool->queueWork(task); + return {}; }); // Load SQL File from disk @@ -359,9 +320,21 @@ void install(jsi::Runtime &rt, std::shared_ptr jsCallInvoker 3, [pool](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { + if (sizeof(args) < 3) + { + jsi::detail::throwJSError(rt, "[react-native-quick-sqlite][asyncLoadSqlFile] Incorrect parameter count"); + return {}; + } + + const jsi::Value &callbackHolder = args[2]; + if(!callbackHolder.isObject() || !callbackHolder.asObject(rt).isFunction(rt)) { + jsi::detail::throwJSError(rt, "[react-native-quick-sqlite][asyncLoadSqlFile] The callback argument must be a function"); + return {}; + } + const string dbName = args[0].asString(rt).utf8(rt); const string sqlFileName = args[1].asString(rt).utf8(rt); - auto callback = make_shared((args[2].asObject(rt))); + auto callback = make_shared(callbackHolder.asObject(rt)); auto task = [&rt, dbName, sqlFileName, callback]() @@ -405,14 +378,20 @@ void install(jsi::Runtime &rt, std::shared_ptr jsCallInvoker { if (count < 4) { - jsi::detail::throwJSError(rt, "[react-native-quick-sqlite] Incorrect arguments for asyncExecuteSQL"); + jsi::detail::throwJSError(rt, "[react-native-quick-sqlite][asyncExecuteSql] Incorrect arguments for asyncExecuteSQL"); + return {}; + } + + const jsi::Value &callbackHolder = args[3]; + if(!callbackHolder.isObject() || !callbackHolder.asObject(rt).isFunction(rt)) { + jsi::detail::throwJSError(rt, "[react-native-quick-sqlite][asyncExecuteSql] The callback argument must be a function"); return {}; } const string dbName = args[0].asString(rt).utf8(rt); const string query = args[1].asString(rt).utf8(rt); const jsi::Value &originalParams = args[2]; - auto callback = make_shared(args[3].asObject(rt)); + auto callback = make_shared(callbackHolder.asObject(rt)); // Converting query parameters inside the javascript caller thread vector params; @@ -456,6 +435,7 @@ void install(jsi::Runtime &rt, std::shared_ptr jsCallInvoker module.setProperty(rt, "executeSql", move(execSQL)); module.setProperty(rt, "asyncExecuteSql", move(asyncExecSQL)); module.setProperty(rt, "executeSqlBatch", move(execSQLBatch)); + module.setProperty(rt, "asyncExecuteSqlBatch", move(execSQLBatchAsync)); module.setProperty(rt, "loadSqlFile", move(loadSQLFile)); module.setProperty(rt, "asyncLoadSqlFile", move(loadSQLFileAsync)); diff --git a/cpp/sqlbatchexecutor.cpp b/cpp/sqlbatchexecutor.cpp new file mode 100644 index 0000000..8e1ce67 --- /dev/null +++ b/cpp/sqlbatchexecutor.cpp @@ -0,0 +1,89 @@ +/** + * Batch execution implementation +*/ +#include "sqlbatchexecutor.h" + +void jsiBatchParametersToQuickArguments(jsi::Runtime &rt, jsi::Array const &batchParams, vector *commands) +{ + for (int i = 0; i < batchParams.length(rt); i++) + { + const jsi::Array &command = batchParams.getValueAtIndex(rt, i).asObject(rt).asArray(rt); + if (command.length(rt) == 0) + { + continue; + } + + const string query = command.getValueAtIndex(rt, 0).asString(rt).utf8(rt); + const jsi::Value &commandParams = command.length(rt) > 1 ? command.getValueAtIndex(rt, 1) : jsi::Value::undefined(); + if (!commandParams.isUndefined() && commandParams.asObject(rt).isArray(rt) && commandParams.asObject(rt).asArray(rt).length(rt) > 0 && commandParams.asObject(rt).asArray(rt).getValueAtIndex(rt, 0).isObject()) + { + // This arguments is an array of arrays, like a batch update of a single sql command. + const jsi::Array &batchUpdateParams = commandParams.asObject(rt).asArray(rt); + for (int x = 0; x < batchUpdateParams.length(rt); x++) + { + const jsi::Value &p = batchUpdateParams.getValueAtIndex(rt, x); + vector params; + jsiQueryArgumentsToSequelParam(rt, p, ¶ms); + commands->push_back(QuickQueryArguments{ + query, + make_shared>(params) + }); + } + } + else + { + vector params; + jsiQueryArgumentsToSequelParam(rt, commandParams, ¶ms); + commands->push_back(QuickQueryArguments{ + query, + make_shared>(params) + }); + } + } +} + +SequelBatchOperationResult executeBatch(std::string dbName, vector *commands) +{ + size_t commandCount = commands->size(); + if(commandCount <= 0) + { + return SequelBatchOperationResult { + .type = SequelResultError, + .message = "No SQL commands provided", + }; + } + + try + { + int affectedRows = 0; + sequel_execute_literal_update(dbName, "BEGIN EXCLUSIVE TRANSACTION"); + for(int i = 0; iat(i); + // We do not provide a datastructure to receive query data because we don't need/want to handle this results in a batch execution + auto result = sequel_execute3(dbName, command.sql, command.params.get(), NULL); + if(result.type == SequelResultError) + { + return SequelBatchOperationResult { + .type = SequelResultError, + .message = result.errorMessage, + }; + } else + { + affectedRows += result.rowsAffected; + } + } + sequel_execute_literal_update(dbName, "COMMIT"); + return SequelBatchOperationResult { + .type = SequelResultOk, + .affectedRows = affectedRows, + .commands = (int) commandCount, + }; + } catch(std::exception &exc) + { + sequel_execute_literal_update(dbName, "ROLLBACK"); + return SequelBatchOperationResult { + .type = SequelResultError, + .message = exc.what(), + }; + } +} diff --git a/cpp/sqlbatchexecutor.h b/cpp/sqlbatchexecutor.h new file mode 100644 index 0000000..400e9b7 --- /dev/null +++ b/cpp/sqlbatchexecutor.h @@ -0,0 +1,24 @@ +/** + * SQL Batch execution implementation using default sqliteBridge implementation +*/ +#include "JSIHelper.h" +#include "sqliteBridge.h" + +using namespace std; +using namespace facebook; + +struct QuickQueryArguments { + string sql; + shared_ptr> params; +}; + +/** + * Local Helper method to translate JSI objects QuickQueryArguments datastructure + * MUST be called in the JavaScript Thread +*/ +void jsiBatchParametersToQuickArguments(jsi::Runtime &rt, jsi::Array const &batchParams, vector *commands); + +/** + * Execute a batch of commands in a exclusive transaction +*/ +SequelBatchOperationResult executeBatch(std::string dbName, vector *commands); diff --git a/cpp/sqlfileloader.cpp b/cpp/sqlfileloader.cpp new file mode 100644 index 0000000..da4cd8c --- /dev/null +++ b/cpp/sqlfileloader.cpp @@ -0,0 +1,54 @@ +/** + * SQL File Loader implementation +*/ +#include "sqlfileloader.h" +#include +#include + +using namespace std; + +SequelBatchOperationResult importSQLFile(string dbName, string fileLocation) +{ + string line; + ifstream sqFile(fileLocation); + if (sqFile.is_open()) + { + try + { + int affectedRows = 0; + int commands = 0; + sequel_execute_literal_update(dbName, "BEGIN EXCLUSIVE TRANSACTION"); + while (std::getline(sqFile, line, '\n')) + { + if (!line.empty()) + { + SequelLiteralUpdateResult result = sequel_execute_literal_update(dbName, line); + if (result.type == SequelResultError) + { + sequel_execute_literal_update(dbName, "ROLLBACK"); + sqFile.close(); + return {SequelResultError, result.message, 0, commands}; + } + else + { + affectedRows += result.affectedRows; + commands++; + } + } + } + sqFile.close(); + sequel_execute_literal_update(dbName, "COMMIT"); + return {SequelResultOk, "", affectedRows, commands}; + } + catch (...) + { + sqFile.close(); + sequel_execute_literal_update(dbName, "ROLLBACK"); + return {SequelResultError, "[react-native-quick-sqlite][loadSQLFile] Unexpected error, transaction was rolledback", 0, 0}; + } + } + else + { + return {SequelResultError, "[react-native-quick-sqlite][loadSQLFile] Could not open file", 0, 0}; + } +} diff --git a/cpp/sqlfileloader.h b/cpp/sqlfileloader.h new file mode 100644 index 0000000..c301035 --- /dev/null +++ b/cpp/sqlfileloader.h @@ -0,0 +1,10 @@ + +/** + * SQL File Loader + * Utilizes the regular sqlite bridge to load an SQLFile inside a transaction + * +*/ + +#include "JSIHelper.h" +#include "sqliteBridge.h" +SequelBatchOperationResult importSQLFile(std::string dbName, std::string fileLocation); \ No newline at end of file diff --git a/cpp/sqliteBridge.cpp b/cpp/sqliteBridge.cpp index 56889ea..e78d58d 100644 --- a/cpp/sqliteBridge.cpp +++ b/cpp/sqliteBridge.cpp @@ -334,6 +334,11 @@ SequelOperationStatus sequel_execute3(string const dbName, string const &query, switch (result) { case SQLITE_ROW: + if(results == NULL) + { + break; + } + i = 0; row = map(); count = sqlite3_column_count(statement); diff --git a/src/index.ts b/src/index.ts index bbfce05..3f897f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -90,6 +90,11 @@ interface ISQLite { dbName: string, commands: SQLBatchParams[] ) => BatchQueryResult; + asyncExecuteSqlBatch: ( + dbName: string, + commands: SQLBatchParams[], + cb: (res: BatchQueryResult) => void + ) => void; loadSqlFile: (dbName: string, location: string) => FileLoadResult; asyncLoadSqlFile: ( dbName: string, @@ -124,6 +129,10 @@ interface IDBConnection { commands: SQLBatchParams[], callback?: (res: BatchQueryResult) => void ) => void; + asyncExecuteSqlBatch: ( + commands: SQLBatchParams[], + cb: (res: BatchQueryResult) => void + ) => void; close: (ok: (res: any) => void, fail: (msg: string) => void) => void; loadSqlFile: ( location: string, @@ -189,6 +198,12 @@ export const openDatabase = ( const response = sqlite.executeSqlBatch(options.name, commands); if (callback) callback(response); }, + asyncExecuteSqlBatch: ( + commands: SQLBatchParams[], + cb: (res: BatchQueryResult) => void + ) => { + sqlite.asyncExecuteSqlBatch(options.name, commands, cb); + }, close: (ok: any, fail: any) => { try { sqlite.close(options.name);