diff --git a/cpp/JSIHelper.cpp b/cpp/JSIHelper.cpp index ac86be4..a2d4da2 100644 --- a/cpp/JSIHelper.cpp +++ b/cpp/JSIHelper.cpp @@ -205,3 +205,45 @@ jsi::Value createSequelQueryExecutionResult(jsi::Runtime &rt, SQLiteOPResult sta return move(res); } + +jsi::Value createSequelQueryExecutionResult2(jsi::Runtime &rt, SQLiteOPResult status, jsi::Array &results, vector *metadata) +{ + if(status.type == SQLiteError) { + throw std::invalid_argument(status.errorMessage); + } + + jsi::Object res = jsi::Object(rt); + + res.setProperty(rt, "rowsAffected", jsi::Value(status.rowsAffected)); + if (status.rowsAffected > 0 && status.insertId != 0) + { + res.setProperty(rt, "insertId", jsi::Value(status.insertId)); + } + + // Converting row results into objects + size_t rowCount = results.size(rt); + jsi::Object rows = jsi::Object(rt); + if (rowCount > 0) + { + rows.setProperty(rt, "_array", move(results)); + res.setProperty(rt, "rows", move(rows)); + } + + if(metadata != NULL) + { + size_t column_count = metadata->size(); + auto column_array = jsi::Array(rt, column_count); + for (int i = 0; i < column_count; i++) { + auto column = metadata->at(i); + jsi::Object column_object = jsi::Object(rt); + column_object.setProperty(rt, "columnName", jsi::String::createFromUtf8(rt, column.colunmName.c_str())); + column_object.setProperty(rt, "columnDeclaredType", jsi::String::createFromUtf8(rt, column.columnDeclaredType.c_str())); + column_object.setProperty(rt, "columnIndex", jsi::Value(column.columnIndex)); + column_array.setValueAtIndex(rt, i, move(column_object)); + } + res.setProperty(rt, "metadata", move(column_array)); + } + rows.setProperty(rt, "length", jsi::Value((int)rowCount)); + + return move(res); +} \ No newline at end of file diff --git a/cpp/JSIHelper.h b/cpp/JSIHelper.h index 9372937..e261265 100644 --- a/cpp/JSIHelper.h +++ b/cpp/JSIHelper.h @@ -110,5 +110,6 @@ QuickValue createInt64QuickValue(long long value); QuickValue createDoubleQuickValue(double value); QuickValue createArrayBufferQuickValue(uint8_t *arrayBufferValue, size_t arrayBufferSize); jsi::Value createSequelQueryExecutionResult(jsi::Runtime &rt, SQLiteOPResult status, vector> *results, vector *metadata); +jsi::Value createSequelQueryExecutionResult2(jsi::Runtime &rt, SQLiteOPResult status, jsi::Array &results, vector *metadata); #endif /* JSIHelper_h */ diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp index 34adcd0..bb0d999 100644 --- a/cpp/bindings.cpp +++ b/cpp/bindings.cpp @@ -206,6 +206,51 @@ void install(jsi::Runtime &rt, std::shared_ptr jsCallInvoker } }); + + auto execute2 = HOSTFN("execute2", 4) + { + const string dbName = args[0].asString(rt).utf8(rt); + const string query = args[1].asString(rt).utf8(rt); + vector params; + bool returnArrays = false; + if(count > 2) { + const jsi::Value &originalParams = args[2]; + jsiQueryArgumentsToSequelParam(rt, originalParams, ¶ms); + } + + if (count == 4) { + returnArrays = args[3].asBool(); + } + + vector results; + vector metadata; + + // Converting results into a JSI Response + try { + auto status = sqliteExecute2(rt, dbName, query, ¶ms, returnArrays, &results, &metadata); + + if(status.type == SQLiteError) { +// throw std::runtime_error(status.errorMessage); + throw jsi::JSError(rt, status.errorMessage); +// return {}; + } + + jsi::Array ar = jsi::Array(rt, results.size()); + + int i = 0; + for (auto const& result : results) { + ar.setValueAtIndex(rt, i, move(result)); + i++; + } + + auto jsiResult = createSequelQueryExecutionResult2(rt, status, ar, &metadata); + + return jsiResult; + } catch(std::exception &e) { + throw jsi::JSError(rt, e.what()); + } + }); + auto executeAsync = HOSTFN("executeAsync", 3) { if (count < 3) @@ -263,6 +308,73 @@ void install(jsi::Runtime &rt, std::shared_ptr jsCallInvoker return promise; }); + + auto executeAsync2 = HOSTFN("executeAsync2", 4) + { + if (count < 4) + { + throw jsi::JSError(rt, "[react-native-quick-sqlite][executeAsync2] Incorrect arguments for executeAsync2"); + } + + const string dbName = args[0].asString(rt).utf8(rt); + const string query = args[1].asString(rt).utf8(rt); + const jsi::Value &originalParams = args[2]; + const bool returnArrays = args[3].asBool(); + + // Converting query parameters inside the javascript caller thread + vector params; + jsiQueryArgumentsToSequelParam(rt, originalParams, ¶ms); + + auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise"); + auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor", 2) { + auto resolve = std::make_shared(rt, args[0]); + auto reject = std::make_shared(rt, args[1]); + + auto task = + [&rt, dbName, query, params = make_shared>(params), returnArrays, resolve, reject]() + { + try + { + invoker->invokeAsync([&rt, dbName, query, params, returnArrays, resolve, reject] + { + vector results; + vector metadata; + auto status = sqliteExecute2(rt, dbName, query, params.get(), returnArrays, &results, &metadata); + if(status.type == SQLiteOk) { + jsi::Array ar = jsi::Array(rt, results.size()); + + int i = 0; + for (auto const& result : results) { + ar.setValueAtIndex(rt, i, move(result)); + i++; + } + + auto jsiResult = createSequelQueryExecutionResult2(rt, status, ar, &metadata); + resolve->asObject(rt).asFunction(rt).call(rt, move(jsiResult)); + } else { + auto errorCtr = rt.global().getPropertyAsFunction(rt, "Error"); + auto error = errorCtr.callAsConstructor(rt, jsi::String::createFromUtf8(rt, status.errorMessage)); + reject->asObject(rt).asFunction(rt).call(rt, error); + } + }); + + } + catch (std::exception &exc) + { + invoker->invokeAsync([&rt, &exc] { + jsi::JSError(rt, exc.what()); + }); + } + }; + + pool->queueWork(task); + + return {}; + })); + + return promise; + }); + // Execute a batch of SQL queries in a transaction // Parameters can be: [[sql: string, arguments: any[] | arguments: any[][] ]] auto executeBatch = HOSTFN("executeBatch", 2) @@ -438,7 +550,9 @@ void install(jsi::Runtime &rt, std::shared_ptr jsCallInvoker module.setProperty(rt, "detach", move(detach)); module.setProperty(rt, "delete", move(remove)); module.setProperty(rt, "execute", move(execute)); + module.setProperty(rt, "execute2", move(execute2)); module.setProperty(rt, "executeAsync", move(executeAsync)); + module.setProperty(rt, "executeAsync2", move(executeAsync2)); module.setProperty(rt, "executeBatch", move(executeBatch)); module.setProperty(rt, "executeBatchAsync", move(executeBatchAsync)); module.setProperty(rt, "loadFile", move(loadFile)); diff --git a/cpp/sqlbatchexecutor.h b/cpp/sqlbatchexecutor.h index 43dccd9..1aa9bbe 100644 --- a/cpp/sqlbatchexecutor.h +++ b/cpp/sqlbatchexecutor.h @@ -1,24 +1,25 @@ /** * SQL Batch execution implementation using default sqliteBridge implementation -*/ + */ #include "JSIHelper.h" #include "sqliteBridge.h" using namespace std; using namespace facebook; -struct QuickQueryArguments { +struct QuickQueryArguments +{ string sql; shared_ptr> params; }; /** - * Local Helper method to translate JSI objects QuickQueryArguments datastructure + * 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 -*/ + * Execute a batch of commands in a exclusive transaction + */ SequelBatchOperationResult sqliteExecuteBatch(std::string dbName, vector *commands); diff --git a/cpp/sqliteBridge.cpp b/cpp/sqliteBridge.cpp index 9ffe010..ac89c58 100644 --- a/cpp/sqliteBridge.cpp +++ b/cpp/sqliteBridge.cpp @@ -411,6 +411,217 @@ SQLiteOPResult sqliteExecute(string const dbName, string const &query, vector(latestInsertRowId)}; } + +SQLiteOPResult sqliteExecute2(jsi::Runtime &rt, string const dbName, string const &query, vector *params, bool const returnArrays, vector *results, vector *metadata) +{ + + if (dbMap.count(dbName) == 0) + { + return SQLiteOPResult{ + .type = SQLiteError, + .errorMessage = "[react-native-quick-sqlite]: Database " + dbName + " is not open", + .rowsAffected = 0 + }; + } + + sqlite3 *db = dbMap[dbName]; + + sqlite3_stmt *statement; + + int statementStatus = sqlite3_prepare_v2(db, query.c_str(), -1, &statement, NULL); + + if (statementStatus == SQLITE_OK) // statemnet is correct, bind the passed parameters + { + bindStatement(statement, params); + } + else + { + const char *message = sqlite3_errmsg(db); + return SQLiteOPResult{ + .type = SQLiteError, + .errorMessage = "[react-native-quick-sqlite] SQL execution error: " + string(message), + .rowsAffected = 0}; + } + + bool isConsuming = true; + bool isFailed = false; + + int result, i, count, column_type; + string column_name, column_declared_type; + const char *column_name_char; + jsi::Array arrayRow = jsi::Array(rt, 0); + jsi::Object objectRow = jsi::Object(rt); + + while (isConsuming) + { + result = sqlite3_step(statement); + + switch (result) + { + case SQLITE_ROW: + if(results == NULL) + { + break; + } + + i = 0; + count = sqlite3_column_count(statement); + if (returnArrays) { + arrayRow = jsi::Array(rt, count); + } else { + objectRow = jsi::Object(rt); + } + + while (i < count) + { + column_type = sqlite3_column_type(statement, i); + column_name = sqlite3_column_name(statement, i); + + if (!returnArrays) { + column_name_char = column_name.c_str(); + } + + switch (column_type) + { + + case SQLITE_INTEGER: + { + /** + * It's not possible to send a int64_t in a jsi::Value because JS cannot represent the whole number range. + * Instead, we're sending a double, which can represent all integers up to 53 bits long, which is more + * than what was there before (a 32-bit int). + * + * See https://github.com/margelo/react-native-quick-sqlite/issues/16 for more context. + */ + double column_value = sqlite3_column_double(statement, i); + if (returnArrays) { + arrayRow.setValueAtIndex(rt, i, column_value); + } else { + objectRow.setProperty(rt, column_name_char, column_value); + } + break; + } + + case SQLITE_FLOAT: + { + double column_value = sqlite3_column_double(statement, i); + if (returnArrays) { + arrayRow.setValueAtIndex(rt, i, column_value); + } else { + objectRow.setProperty(rt, column_name_char, column_value); + } + break; + } + + case SQLITE_TEXT: + { + const unsigned char *column_value = sqlite3_column_text(statement, i); + int byteLen = sqlite3_column_bytes(statement, i); + if (byteLen > 0) { + // Specify length too; in case string contains NULL in the middle (which SQLite supports!) + jsi::String jsiValue = jsi::String::createFromUtf8(rt, column_value, byteLen); + if (returnArrays) { + arrayRow.setValueAtIndex(rt, i, jsiValue); + } else { + objectRow.setProperty(rt, column_name_char, jsiValue); + } + } else { + if (returnArrays) { + arrayRow.setValueAtIndex(rt, i, jsi::Value::null()); + } else { + objectRow.setProperty(rt, column_name_char, jsi::Value::null()); + } + } + break; + } + + case SQLITE_BLOB: + { + int blob_size = sqlite3_column_bytes(statement, i); + const void *blob = sqlite3_column_blob(statement, i); + + jsi::Function array_buffer_ctor = rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); + jsi::Object o = array_buffer_ctor.callAsConstructor(rt, blob_size).getObject(rt); + jsi::ArrayBuffer buf = o.getArrayBuffer(rt); + memcpy(buf.data(rt), blob, blob_size); + + if (returnArrays) { + arrayRow.setValueAtIndex(rt, i, o); + } else { + objectRow.setProperty(rt, column_name_char, o); + } + break; + } + + case SQLITE_NULL: + // Intentionally left blank to switch to default case + default: + if (returnArrays) { + arrayRow.setValueAtIndex(rt, i, jsi::Value::null()); + } else { + objectRow.setProperty(rt, column_name_char, jsi::Value::null()); + } + break; + } + i++; + } + + if (returnArrays) { + results->push_back(move(arrayRow)); + } else { + results->push_back(move(objectRow)); + } + + break; + case SQLITE_DONE: + if(metadata != NULL) + { + i = 0; + count = sqlite3_column_count(statement); + while (i < count) + { + column_name = sqlite3_column_name(statement, i); + const char *tp = sqlite3_column_decltype(statement, i); + column_declared_type = tp != NULL ? tp : "UNKNOWN"; + QuickColumnMetadata meta = { + .colunmName = column_name, + .columnIndex = i, + .columnDeclaredType = column_declared_type, + }; + metadata->push_back(meta); + i++; + } + } + isConsuming = false; + break; + + default: + isFailed = true; + isConsuming = false; + } + } + + sqlite3_finalize(statement); + + if (isFailed) + { + const char *message = sqlite3_errmsg(db); + return SQLiteOPResult{ + .type = SQLiteError, + .errorMessage = "[react-native-quick-sqlite] SQL execution error: " + string(message), + .rowsAffected = 0, + .insertId = 0 + }; + } + + int changedRowCount = sqlite3_changes(db); + long long latestInsertRowId = sqlite3_last_insert_rowid(db); + return SQLiteOPResult{ + .type = SQLiteOk, + .rowsAffected = changedRowCount, + .insertId = static_cast(latestInsertRowId)}; +} + SequelLiteralUpdateResult sqliteExecuteLiteral(string const dbName, string const &query) { // Check if db connection is opened diff --git a/cpp/sqliteBridge.h b/cpp/sqliteBridge.h index 9696ea9..07f5ab9 100644 --- a/cpp/sqliteBridge.h +++ b/cpp/sqliteBridge.h @@ -25,6 +25,8 @@ SQLiteOPResult sqliteDetachDb(string const mainDBName, string const alias); SQLiteOPResult sqliteExecute(string const dbName, string const &query, vector *values, vector> *result, vector *metadata); +SQLiteOPResult sqliteExecute2(jsi::Runtime &rt, string const dbName, string const &query, vector *values, bool const returnArrays, vector *result, vector *metadata); + SequelLiteralUpdateResult sqliteExecuteLiteral(string const dbName, string const &query); void sqliteCloseAll(); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9ac66cc..41ae831 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -234,7 +234,7 @@ PODS: - React-jsinspector (0.71.1) - React-logger (0.71.1): - glog - - react-native-quick-sqlite (8.0.5): + - react-native-quick-sqlite (8.0.6): - React - React-callinvoker - React-Core @@ -469,7 +469,7 @@ SPEC CHECKSUMS: React-jsiexecutor: 60cf272aababc5212410e4249d17cea14fc36caa React-jsinspector: ff56004b0c974b688a6548c156d5830ad751ae07 React-logger: 60a0b5f8bed667ecf9e24fecca1f30d125de6d75 - react-native-quick-sqlite: 28be360af451ddd14fac8cccf5ffcac8ae9ca28a + react-native-quick-sqlite: e0e23b749382a85e4b57146f753de737a6c3a9e1 react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc React-perflogger: ec8eef2a8f03ecfa6361c2c5fb9197ef4a29cc85 React-RCTActionSheet: a0c023b86cf4c862fa9c4eb0f6f91fbe878fb2de diff --git a/src/index.ts b/src/index.ts index b3e7fc7..8bcacd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -112,10 +112,16 @@ export interface FileLoadResult extends BatchQueryResult { export interface Transaction { commit: () => QueryResult; execute: (query: string, params?: any[]) => QueryResult; + execute2: (query: string, params?: any[], returnArrays?: boolean) => QueryResult; executeAsync: ( query: string, params?: any[] | undefined ) => Promise; + executeAsync2: ( + query: string, + params?: any[] | undefined, + returnArrays?: boolean + ) => Promise; rollback: () => QueryResult; } @@ -148,11 +154,18 @@ interface ISQLite { fn: (tx: Transaction) => Promise | void ) => Promise; execute: (dbName: string, query: string, params?: any[]) => QueryResult; + execute2: (dbName: string, query: string, params?: any[], returnArrays?: boolean) => QueryResult; executeAsync: ( dbName: string, query: string, params?: any[] ) => Promise; + executeAsync2: ( + dbName: string, + query: string, + params?: any[], + returnArrays?: boolean + ) => Promise; executeBatch: (dbName: string, commands: SQLBatchTuple[]) => BatchQueryResult; executeBatchAsync: ( dbName: string, @@ -210,6 +223,18 @@ QuickSQLite.execute = ( return result; }; +const _execute2 = QuickSQLite.execute2; +QuickSQLite.execute2 = ( + dbName: string, + query: string, + params?: any[] | undefined, + returnArrays?: boolean +): QueryResult => { + const result = _execute2(dbName, query, params, returnArrays); + enhanceQueryResult(result); + return result; +}; + const _executeAsync = QuickSQLite.executeAsync; QuickSQLite.executeAsync = async ( dbName: string, @@ -221,6 +246,18 @@ QuickSQLite.executeAsync = async ( return res; }; +const _executeAsync2 = QuickSQLite.executeAsync2; +QuickSQLite.executeAsync2 = async ( + dbName: string, + query: string, + params?: any[] | undefined, + returnArrays?: boolean +): Promise => { + const res = await _executeAsync2(dbName, query, params, returnArrays); + enhanceQueryResult(res); + return res; +}; + QuickSQLite.transaction = async ( dbName: string, fn: (tx: Transaction) => Promise @@ -241,6 +278,16 @@ QuickSQLite.transaction = async ( return QuickSQLite.execute(dbName, query, params); }; + // Local transaction context object implementation + const execute2 = (query: string, params?: any[], returnArrays?: boolean): QueryResult => { + if (isFinalized) { + throw Error( + `Quick SQLite Error: Cannot execute query on finalized transaction: ${dbName}` + ); + } + return QuickSQLite.execute2(dbName, query, params, returnArrays); + }; + const executeAsync = (query: string, params?: any[] | undefined) => { if (isFinalized) { throw Error( @@ -250,6 +297,15 @@ QuickSQLite.transaction = async ( return QuickSQLite.executeAsync(dbName, query, params); }; + const executeAsync2 = (query: string, params?: any[] | undefined, returnArrays?: boolean) => { + if (isFinalized) { + throw Error( + `Quick SQLite Error: Cannot execute query on finalized transaction: ${dbName}` + ); + } + return QuickSQLite.executeAsync2(dbName, query, params, returnArrays); + }; + const commit = () => { if (isFinalized) { throw Error( @@ -279,7 +335,9 @@ QuickSQLite.transaction = async ( await fn({ commit, execute, + execute2, executeAsync, + executeAsync2, rollback, }); @@ -422,7 +480,9 @@ export type QuickSQLiteConnection = { detach: (alias: string) => void; transaction: (fn: (tx: Transaction) => Promise | void) => Promise; execute: (query: string, params?: any[]) => QueryResult; + execute2: (query: string, params?: any[], returnArrays?: boolean) => QueryResult; executeAsync: (query: string, params?: any[]) => Promise; + executeAsync2: (query: string, params?: any[], returnArrays?: boolean) => Promise; executeBatch: (commands: SQLBatchTuple[]) => BatchQueryResult; executeBatchAsync: (commands: SQLBatchTuple[]) => Promise; loadFile: (location: string) => FileLoadResult; @@ -445,11 +505,19 @@ export const open = (options: { QuickSQLite.transaction(options.name, fn), execute: (query: string, params?: any[] | undefined): QueryResult => QuickSQLite.execute(options.name, query, params), + execute2: (query: string, params?: any[] | undefined, returnArrays?: boolean | undefined): QueryResult => + QuickSQLite.execute2(options.name, query, params, returnArrays), executeAsync: ( query: string, params?: any[] | undefined ): Promise => QuickSQLite.executeAsync(options.name, query, params), + executeAsync2: ( + query: string, + params?: any[] | undefined, + returnArrays?: boolean | undefined + ): Promise => + QuickSQLite.executeAsync2(options.name, query, params, returnArrays), executeBatch: (commands: SQLBatchTuple[]) => QuickSQLite.executeBatch(options.name, commands), executeBatchAsync: (commands: SQLBatchTuple[]) =>