From 1dbc8289e1a04841b1ae08b7fbbf3c849a3454a2 Mon Sep 17 00:00:00 2001 From: ospfranco Date: Mon, 12 Aug 2024 17:01:19 -0400 Subject: [PATCH] Brought back non host object API --- cpp/DBHostObject.cpp | 196 +++++----- cpp/PreparedStatementHostObject.cpp | 4 +- cpp/bindings.cpp | 6 +- cpp/bridge.cpp | 168 ++++++++- cpp/bridge.h | 5 +- cpp/macros.h | 4 +- cpp/types.h | 19 +- cpp/utils.cpp | 51 ++- cpp/utils.h | 2 + example/ios/Podfile.lock | 4 +- example/src/App.tsx | 12 +- example/src/tests/blob.spec.ts | 6 +- example/src/tests/dbsetup.spec.ts | 2 +- example/src/tests/hooks.spec.ts | 6 +- example/src/tests/preparedStatements.spec.ts | 23 +- example/src/tests/queries.spec.ts | 373 ++++--------------- example/src/tests/reactive.spec.ts | 6 +- src/index.ts | 112 ++++-- 18 files changed, 499 insertions(+), 500 deletions(-) diff --git a/cpp/DBHostObject.cpp b/cpp/DBHostObject.cpp index 92fe4495..7fcf756e 100644 --- a/cpp/DBHostObject.cpp +++ b/cpp/DBHostObject.cpp @@ -39,7 +39,7 @@ void DBHostObject::auto_register_update_hook() { if (operation != "DELETE") { std::string query = "SELECT * FROM " + table_name + " where rowid = " + std::to_string(rowId) + ";"; - opsqlite_execute(name, query, ¶ms, &results, metadata); + opsqlite_execute_host_objects(name, query, ¶ms, &results, metadata); } jsCallInvoker->invokeAsync( @@ -57,7 +57,7 @@ void DBHostObject::auto_register_update_hook() { res.setProperty( rt, "row", jsi::Object::createFromHostObject( - rt, std::make_shared(results->at(0)))); + rt, std::make_shared(results->at(0)))); } callback->asObject(rt).asFunction(rt).call(rt, res); @@ -192,7 +192,7 @@ DBHostObject::DBHostObject(jsi::Runtime &rt, std::string &base_path, }; void DBHostObject::create_jsi_functions() { - auto attach = HOSTFN("attach", 4) { + auto attach = HOSTFN("attach") { if (count < 3) { throw jsi::JSError(rt, "[op-sqlite][attach] Incorrect number of arguments"); @@ -230,7 +230,7 @@ void DBHostObject::create_jsi_functions() { return {}; }); - auto detach = HOSTFN("detach", 2) { + auto detach = HOSTFN("detach") { if (count < 2) { throw std::runtime_error( "[op-sqlite][detach] Incorrect number of arguments"); @@ -256,7 +256,7 @@ void DBHostObject::create_jsi_functions() { return {}; }); - auto close = HOSTFN("close", 0) { + auto close = HOSTFN("close") { #ifdef OP_SQLITE_USE_LIBSQL BridgeResult result = opsqlite_libsql_close(db_name); #else @@ -270,8 +270,7 @@ void DBHostObject::create_jsi_functions() { return {}; }); - auto remove = HOSTFN("delete", 1) { - + auto remove = HOSTFN("delete") { std::string path = std::string(base_path); if (count == 1 && !args[0].isUndefined() && !args[0].isNull()) { @@ -306,35 +305,7 @@ void DBHostObject::create_jsi_functions() { return {}; }); - auto execute = HOSTFN("execute", 2) { - const std::string query = args[0].asString(rt).utf8(rt); - std::vector params; - - if (count == 2) { - const jsi::Value &originalParams = args[1]; - params = to_variant_vec(rt, originalParams); - } - - std::vector results; - std::shared_ptr> metadata = - std::make_shared>(); - -#ifdef OP_SQLITE_USE_LIBSQL - auto status = - opsqlite_libsql_execute(db_name, query, ¶ms, &results, metadata); -#else - auto status = opsqlite_execute(db_name, query, ¶ms, &results, metadata); -#endif - - if (status.type == SQLiteError) { - throw std::runtime_error(status.message); - } - - auto jsiResult = createResult(rt, status, &results, metadata); - return jsiResult; - }); - - auto execute_raw_async = HOSTFN("executeRawAsync", 2) { + auto execute_raw = HOSTFN("executeRaw") { const std::string query = args[0].asString(rt).utf8(rt); std::vector params; @@ -344,7 +315,7 @@ void DBHostObject::create_jsi_functions() { } auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise"); - auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor", 2) { + auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor") { auto resolve = std::make_shared(rt, args[0]); auto reject = std::make_shared(rt, args[1]); @@ -359,10 +330,6 @@ void DBHostObject::create_jsi_functions() { #else auto status = opsqlite_execute_raw(db_name, query, ¶ms, &results); #endif - // - // if (invalidated) { - // return; - // } invoker->invokeAsync([&rt, results = std::move(results), status = std::move(status), resolve, reject] { @@ -395,8 +362,70 @@ void DBHostObject::create_jsi_functions() { return promise; }); + + auto execute = HOSTFN("execute") { + const std::string query = args[0].asString(rt).utf8(rt); + std::vector params; + + if (count == 2) { + params = to_variant_vec(rt, args[1]); + } + + auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise"); + auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor") { + auto resolve = std::make_shared(rt, args[0]); + auto reject = std::make_shared(rt, args[1]); + + auto task = [&rt, this, query, params = std::move(params), resolve, + reject, invoker = this->jsCallInvoker]() { + try { + + #ifdef OP_SQLITE_USE_LIBSQL + auto status = opsqlite_libsql_execute(db_name, query, ¶ms, + &results, metadata); + #else + auto status = opsqlite_execute(db_name, query, ¶ms); + #endif + + // if (invalidated) { + // return; + // } + + invoker->invokeAsync( + [&rt, status = std::move(status), resolve, reject] { + if (status.type == SQLiteOk) { + auto jsiResult = create_js_rows(rt, status); + resolve->asObject(rt).asFunction(rt).call( + rt, std::move(jsiResult)); + } else { + auto errorCtr = + rt.global().getPropertyAsFunction(rt, "Error"); + auto error = errorCtr.callAsConstructor( + rt, jsi::String::createFromUtf8(rt, status.message)); + reject->asObject(rt).asFunction(rt).call(rt, error); + } + }); + + } catch (std::exception &exc) { + std::cout << "Exception executing function" << exc.what() << std::endl; + invoker->invokeAsync([&rt, exc = std::move(exc), reject] { + auto errorCtr = rt.global().getPropertyAsFunction(rt, "Error"); + auto error = errorCtr.callAsConstructor( + rt, jsi::String::createFromAscii(rt, exc.what())); + reject->asObject(rt).asFunction(rt).call(rt, error); + }); + } + }; + + thread_pool->queueWork(task); + + return {}; + })); - auto execute_async = HOSTFN("executeAsync", 2) { + return promise; + }); + + auto execute_with_host_objects = HOSTFN("executeWithHostObjects") { const std::string query = args[0].asString(rt).utf8(rt); std::vector params; @@ -406,7 +435,7 @@ void DBHostObject::create_jsi_functions() { } auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise"); - auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor", 2) { + auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor") { auto resolve = std::make_shared(rt, args[0]); auto reject = std::make_shared(rt, args[1]); @@ -420,8 +449,8 @@ void DBHostObject::create_jsi_functions() { auto status = opsqlite_libsql_execute(db_name, query, ¶ms, &results, metadata); #else - auto status = - opsqlite_execute(db_name, query, ¶ms, &results, metadata); + auto status = opsqlite_execute_host_objects(db_name, query, ¶ms, + &results, metadata); #endif // if (invalidated) { @@ -464,36 +493,7 @@ void DBHostObject::create_jsi_functions() { return promise; }); - auto execute_batch = HOSTFN("executeBatch", 1) { - if (sizeof(args) < 1) { - throw std::runtime_error( - "[op-sqlite][executeBatch] - Incorrect parameter count"); - } - - const jsi::Value ¶ms = args[0]; - if (params.isNull() || params.isUndefined()) { - throw std::runtime_error("[op-sqlite][executeBatch] - An array of SQL " - "commands or parameters is needed"); - } - const jsi::Array &batchParams = params.asObject(rt).asArray(rt); - std::vector commands; - to_batch_arguments(rt, batchParams, &commands); - -#ifdef OP_SQLITE_USE_LIBSQL - auto batchResult = opsqlite_libsql_execute_batch(db_name, &commands); -#else - auto batchResult = opsqlite_execute_batch(db_name, &commands); -#endif - if (batchResult.type == SQLiteOk) { - auto res = jsi::Object(rt); - res.setProperty(rt, "rowsAffected", jsi::Value(batchResult.affectedRows)); - return std::move(res); - } else { - throw std::runtime_error(batchResult.message); - } - }); - - auto execute_batch_async = HOSTFN("executeBatchAsync", 1) { + auto execute_batch = HOSTFN("executeBatch") { if (sizeof(args) < 1) { throw std::runtime_error( "[op-sqlite][executeAsyncBatch] Incorrect parameter count"); @@ -515,7 +515,7 @@ void DBHostObject::create_jsi_functions() { to_batch_arguments(rt, batchParams, &commands); auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise"); - auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor", 2) { + auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor") { auto resolve = std::make_shared(rt, args[0]); auto reject = std::make_shared(rt, args[1]); @@ -558,17 +558,15 @@ void DBHostObject::create_jsi_functions() { }); #ifdef OP_SQLITE_USE_LIBSQL - auto sync = HOSTFN("sync", 0) { + auto sync = HOSTFN("sync") { BridgeResult result = opsqlite_libsql_sync(db_name); if (result.type == SQLiteError) { throw std::runtime_error(result.message); } return {}; }); -#endif - -#ifndef OP_SQLITE_USE_LIBSQL - auto load_file = HOSTFN("loadFile", 1) { +#else + auto load_file = HOSTFN("loadFile") { if (sizeof(args) < 1) { throw std::runtime_error( "[op-sqlite][loadFile] Incorrect parameter count"); @@ -578,17 +576,17 @@ void DBHostObject::create_jsi_functions() { const std::string sqlFileName = args[0].asString(rt).utf8(rt); auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise"); - auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor", 2) + auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor") { auto resolve = std::make_shared(rt, args[0]); auto reject = std::make_shared(rt, args[1]); auto task = [&rt, this, sqlFileName, resolve, reject]() { try { - const auto importResult = importSQLFile(db_name, sqlFileName); + const auto result = importSQLFile(db_name, sqlFileName); - jsCallInvoker->invokeAsync([&rt, result = std::move(importResult), - resolve, reject] { + jsCallInvoker->invokeAsync([&rt, result = std::move(result), resolve, + reject] { if (result.type == SQLiteOk) { auto res = jsi::Object(rt); res.setProperty(rt, "rowsAffected", @@ -614,7 +612,7 @@ void DBHostObject::create_jsi_functions() { return promise; }); - auto update_hook = HOSTFN("updateHook", 1) { + auto update_hook = HOSTFN("updateHook") { auto callback = std::make_shared(rt, args[0]); if (callback->isUndefined() || callback->isNull()) { @@ -626,7 +624,7 @@ void DBHostObject::create_jsi_functions() { return {}; }); - auto commit_hook = HOSTFN("commitHook", 1) { + auto commit_hook = HOSTFN("commitHook") { if (sizeof(args) < 1) { throw std::runtime_error("[op-sqlite][commitHook] callback needed"); return {}; @@ -649,7 +647,7 @@ void DBHostObject::create_jsi_functions() { return {}; }); - auto rollback_hook = HOSTFN("rollbackHook", 1) { + auto rollback_hook = HOSTFN("rollbackHook") { if (sizeof(args) < 1) { throw std::runtime_error("[op-sqlite][rollbackHook] callback needed"); return {}; @@ -672,7 +670,7 @@ void DBHostObject::create_jsi_functions() { return {}; }); - auto load_extension = HOSTFN("loadExtension", 1) { + auto load_extension = HOSTFN("loadExtension") { auto path = args[0].asString(rt).utf8(rt); std::string entry_point = ""; if (count > 1 && args[1].isString()) { @@ -686,7 +684,7 @@ void DBHostObject::create_jsi_functions() { return {}; }); - auto reactive_execute = HOSTFN("reactiveExecute", 0) { + auto reactive_execute = HOSTFN("reactiveExecute") { auto query = args[0].asObject(rt); // if (!query.hasProperty(rt, "query") || !query.hasProperty(rt, "args") // || @@ -746,7 +744,7 @@ void DBHostObject::create_jsi_functions() { auto_register_update_hook(); - auto unsubscribe = HOSTFN("unsubscribe", 0) { + auto unsubscribe = HOSTFN("unsubscribe") { auto it = std::find(reactive_queries.begin(), reactive_queries.end(), reactiveQuery); if (it != reactive_queries.end()) { @@ -761,7 +759,7 @@ void DBHostObject::create_jsi_functions() { #endif - auto prepare_statement = HOSTFN("prepareStatement", 1) { + auto prepare_statement = HOSTFN("prepareStatement") { auto query = args[0].asString(rt).utf8(rt); #ifdef OP_SQLITE_USE_LIBSQL libsql_stmt_t statement = opsqlite_libsql_prepare_statement(db_name, query); @@ -774,7 +772,7 @@ void DBHostObject::create_jsi_functions() { return jsi::Object::createFromHostObject(rt, preparedStatementHostObject); }); - auto get_db_path = HOSTFN("getDbPath", 1) { + auto get_db_path = HOSTFN("getDbPath") { std::string path = std::string(base_path); if (count == 1 && !args[0].isUndefined() && !args[0].isNull()) { if (!args[0].isString()) { @@ -800,12 +798,11 @@ void DBHostObject::create_jsi_functions() { function_map["attach"] = std::move(attach); function_map["detach"] = std::move(detach); function_map["close"] = std::move(close); - function_map["executeRawAsync"] = std::move(execute_raw_async); function_map["execute"] = std::move(execute); - function_map["executeAsync"] = std::move(execute_async); + function_map["executeRaw"] = std::move(execute_raw); + function_map["executeWithHostObjects"] = std::move(execute_with_host_objects); function_map["delete"] = std::move(remove); function_map["executeBatch"] = std::move(execute_batch); - function_map["executeBatchAsync"] = std::move(execute_batch_async); function_map["prepareStatement"] = std::move(prepare_statement); function_map["getDbPath"] = std::move(get_db_path); #ifdef OP_SQLITE_USE_LIBSQL @@ -845,8 +842,8 @@ jsi::Value DBHostObject::get(jsi::Runtime &rt, if (name == "execute") { return jsi::Value(rt, function_map["execute"]); } - if (name == "executeAsync") { - return jsi::Value(rt, function_map["executeAsync"]); + if (name == "executeWithHostObjects") { + return jsi::Value(rt, function_map["executeWithHostObjects"]); } if (name == "delete") { return jsi::Value(rt, function_map["delete"]); @@ -854,9 +851,6 @@ jsi::Value DBHostObject::get(jsi::Runtime &rt, if (name == "executeBatch") { return jsi::Value(rt, function_map["executeBatch"]); } - if (name == "executeBatchAsync") { - return jsi::Value(rt, function_map["executeBatchAsync"]); - } if (name == "prepareStatement") { return jsi::Value(rt, function_map["prepareStatement"]); } diff --git a/cpp/PreparedStatementHostObject.cpp b/cpp/PreparedStatementHostObject.cpp index 0fab265e..fd69a921 100644 --- a/cpp/PreparedStatementHostObject.cpp +++ b/cpp/PreparedStatementHostObject.cpp @@ -23,7 +23,7 @@ jsi::Value PreparedStatementHostObject::get(jsi::Runtime &rt, auto name = propNameID.utf8(rt); if (name == "bind") { - return HOSTFN("bind", 1) { + return HOSTFN("bind") { if (_stmt == nullptr) { throw std::runtime_error("statement has been freed"); } @@ -41,7 +41,7 @@ jsi::Value PreparedStatementHostObject::get(jsi::Runtime &rt, } if (name == "execute") { - return HOSTFN("execute", 1) { + return HOSTFN("execute") { if (_stmt == nullptr) { throw std::runtime_error("statement has been freed"); } diff --git a/cpp/bindings.cpp b/cpp/bindings.cpp index ba96593e..e197a371 100644 --- a/cpp/bindings.cpp +++ b/cpp/bindings.cpp @@ -49,7 +49,7 @@ void install(jsi::Runtime &rt, std::shared_ptr invoker, _crsqlite_path = std::string(crsqlite_path); _invoker = invoker; - auto open = HOSTFN("open", 1) { + auto open = HOSTFN("open") { jsi::Object options = args[0].asObject(rt); std::string name = options.getProperty(rt, "name").asString(rt).utf8(rt); std::string path = std::string(_base_path); @@ -88,7 +88,7 @@ void install(jsi::Runtime &rt, std::shared_ptr invoker, return jsi::Object::createFromHostObject(rt, db); }); - auto is_sqlcipher = HOSTFN("isSQLCipher", 0) { + auto is_sqlcipher = HOSTFN("isSQLCipher") { #ifdef OP_SQLITE_USE_SQLCIPHER return true; #else @@ -96,7 +96,7 @@ void install(jsi::Runtime &rt, std::shared_ptr invoker, #endif }); - auto is_libsql = HOSTFN("isLibsql", 0) { + auto is_libsql = HOSTFN("isLibsql") { #ifdef OP_SQLITE_USE_LIBSQL return true; #else diff --git a/cpp/bridge.cpp b/cpp/bridge.cpp index 769b1d9a..e6802c96 100644 --- a/cpp/bridge.cpp +++ b/cpp/bridge.cpp @@ -123,8 +123,7 @@ BridgeResult opsqlite_attach(std::string const &mainDBName, std::string dbPath = opsqlite_get_db_path(databaseToAttach, docPath); std::string statement = "ATTACH DATABASE '" + dbPath + "' AS " + alias; - BridgeResult result = - opsqlite_execute(mainDBName, statement, nullptr, nullptr, nullptr); + BridgeResult result = opsqlite_execute(mainDBName, statement, nullptr); if (result.type == SQLiteError) { return { @@ -141,8 +140,7 @@ BridgeResult opsqlite_attach(std::string const &mainDBName, BridgeResult opsqlite_detach(std::string const &mainDBName, std::string const &alias) { std::string statement = "DETACH DATABASE " + alias; - BridgeResult result = - opsqlite_execute(mainDBName, statement, nullptr, nullptr, nullptr); + BridgeResult result = opsqlite_execute(mainDBName, statement, nullptr); if (result.type == SQLiteError) { return BridgeResult{ .type = SQLiteError, @@ -368,12 +366,152 @@ sqlite3_stmt *opsqlite_prepare_statement(std::string const &dbName, return statement; } +BridgeResult opsqlite_execute(std::string const &name, std::string const &query, + const std::vector *params) { + check_db_open(name); + + sqlite3 *db = dbMap[name]; + + sqlite3_stmt *statement; + const char *errorMessage; + const char *remainingStatement = nullptr; + + bool isFailed = false; + int result, i, count, column_type; + std::string column_name, column_declared_type; + std::vector column_names; + std::vector> rows; + std::vector row; + + do { + const char *queryStr = + remainingStatement == nullptr ? query.c_str() : remainingStatement; + + int statementStatus = + sqlite3_prepare_v2(db, queryStr, -1, &statement, &remainingStatement); + + if (statementStatus != SQLITE_OK) { + errorMessage = sqlite3_errmsg(db); + return {.type = SQLiteError, + .message = + "[op-sqlite] SQL prepare error: " + std::string(errorMessage), + .affectedRows = 0}; + } + + if (params != nullptr && params->size() > 0) { + opsqlite_bind_statement(statement, params); + } + + bool isConsuming = true; + + while (isConsuming) { + result = sqlite3_step(statement); + + switch (result) { + case SQLITE_ROW: + i = 0; + row = std::vector(); + count = sqlite3_column_count(statement); + + while (i < count) { + column_type = sqlite3_column_type(statement, i); + column_name = sqlite3_column_name(statement, i); + column_names.push_back(column_name); + + 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/ospfranco/react-native-quick-sqlite/issues/16 + * for more context. + */ + double column_value = sqlite3_column_double(statement, i); + row.push_back(JSVariant(column_value)); + break; + } + + case SQLITE_FLOAT: { + double column_value = sqlite3_column_double(statement, i); + row.push_back(JSVariant(column_value)); + break; + } + + case SQLITE_TEXT: { + const char *column_value = reinterpret_cast( + sqlite3_column_text(statement, i)); + int byteLen = sqlite3_column_bytes(statement, i); + // Specify length too; in case string contains NULL in the middle + // (which SQLite supports!) + row.push_back(JSVariant(std::string(column_value, byteLen))); + break; + } + + case SQLITE_BLOB: { + int blob_size = sqlite3_column_bytes(statement, i); + const void *blob = sqlite3_column_blob(statement, i); + uint8_t *data = new uint8_t[blob_size]; + memcpy(data, blob, blob_size); + row.push_back( + JSVariant(ArrayBuffer{.data = std::shared_ptr{data}, + .size = static_cast(blob_size)})); + break; + } + + case SQLITE_NULL: + // Intentionally left blank to switch to default case + default: + row.push_back(JSVariant(nullptr)); + break; + } + i++; + } + + rows.push_back(row); + break; + + case SQLITE_DONE: + isConsuming = false; + break; + + default: + isFailed = true; + isConsuming = false; + } + } + + sqlite3_finalize(statement); + } while (remainingStatement != NULL && strcmp(remainingStatement, "") != 0 && + !isFailed); + + if (isFailed) { + const char *message = sqlite3_errmsg(db); + return {.type = SQLiteError, + .message = + "[op-sqlite] SQL execution error: " + std::string(message), + .affectedRows = 0, + .insertId = 0}; + } + + int changedRowCount = sqlite3_changes(db); + long long latestInsertRowId = sqlite3_last_insert_rowid(db); + return {.type = SQLiteOk, + .affectedRows = changedRowCount, + .insertId = static_cast(latestInsertRowId), + .rows = std::move(rows), + .column_names = std::move(column_names)}; +} + /// Base execution function, returns HostObjects to the JS environment -BridgeResult -opsqlite_execute(std::string const &dbName, std::string const &query, - const std::vector *params, - std::vector *results, - std::shared_ptr> metadatas) { +BridgeResult opsqlite_execute_host_objects( + std::string const &dbName, std::string const &query, + const std::vector *params, std::vector *results, + std::shared_ptr> metadatas) { check_db_open(dbName); @@ -879,16 +1017,14 @@ BatchResult opsqlite_execute_batch(std::string dbName, try { int affectedRows = 0; - opsqlite_execute(dbName, "BEGIN EXCLUSIVE TRANSACTION", nullptr, nullptr, - nullptr); + opsqlite_execute(dbName, "BEGIN EXCLUSIVE TRANSACTION", nullptr); for (int i = 0; i < commandCount; i++) { auto command = commands->at(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 = opsqlite_execute(dbName, command.sql, command.params.get(), - nullptr, nullptr); + auto result = opsqlite_execute(dbName, command.sql, command.params.get()); if (result.type == SQLiteError) { - opsqlite_execute(dbName, "ROLLBACK", nullptr, nullptr, nullptr); + opsqlite_execute(dbName, "ROLLBACK", nullptr); return BatchResult{ .type = SQLiteError, .message = result.message, @@ -897,14 +1033,14 @@ BatchResult opsqlite_execute_batch(std::string dbName, affectedRows += result.affectedRows; } } - opsqlite_execute(dbName, "COMMIT", nullptr, nullptr, nullptr); + opsqlite_execute(dbName, "COMMIT", nullptr); return BatchResult{ .type = SQLiteOk, .affectedRows = affectedRows, .commands = static_cast(commandCount), }; } catch (std::exception &exc) { - opsqlite_execute(dbName, "ROLLBACK", nullptr, nullptr, nullptr); + opsqlite_execute(dbName, "ROLLBACK", nullptr); return BatchResult{ .type = SQLiteError, .message = exc.what(), diff --git a/cpp/bridge.h b/cpp/bridge.h index 91bf0f96..c933f31e 100644 --- a/cpp/bridge.h +++ b/cpp/bridge.h @@ -44,8 +44,11 @@ BridgeResult opsqlite_attach(std::string const &mainDBName, BridgeResult opsqlite_detach(std::string const &mainDBName, std::string const &alias); +BridgeResult opsqlite_execute(std::string const &name, std::string const &query, + const std::vector *params); + BridgeResult -opsqlite_execute(std::string const &dbName, std::string const &query, +opsqlite_execute_host_objects(std::string const &dbName, std::string const &query, const std::vector *params, std::vector *results, std::shared_ptr> metadatas); diff --git a/cpp/macros.h b/cpp/macros.h index 592e0c4e..f87ea6a7 100644 --- a/cpp/macros.h +++ b/cpp/macros.h @@ -1,11 +1,11 @@ #ifndef macros_h #define macros_h -#define HOSTFN(name, basecount) \ +#define HOSTFN(name) \ jsi::Function::createFromHostFunction( \ rt, \ jsi::PropNameID::forAscii(rt, name), \ -basecount, \ +0, \ [=](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value #endif /* macros_h */ diff --git a/cpp/types.h b/cpp/types.h index 50b527a3..5e7de46f 100644 --- a/cpp/types.h +++ b/cpp/types.h @@ -4,6 +4,15 @@ #include #include #include +#include + +struct ArrayBuffer { + std::shared_ptr data; + size_t size; +}; + +using JSVariant = std::variant; enum ResultType { SQLiteOk, SQLiteError }; @@ -12,6 +21,8 @@ struct BridgeResult { std::string message; int affectedRows; double insertId; + std::vector> rows; + std::vector column_names; }; struct BatchResult { @@ -21,14 +32,6 @@ struct BatchResult { int commands; }; -struct ArrayBuffer { - std::shared_ptr data; - size_t size; -}; - -using JSVariant = std::variant; - struct BatchArguments { std::string sql; std::shared_ptr> params; diff --git a/cpp/utils.cpp b/cpp/utils.cpp index f0890ce9..15d1ca90 100644 --- a/cpp/utils.cpp +++ b/cpp/utils.cpp @@ -158,6 +158,46 @@ std::vector to_variant_vec(jsi::Runtime &rt, jsi::Value const &xs) { return res; } +jsi::Value create_js_rows(jsi::Runtime &rt, BridgeResult status) { + if (status.type == SQLiteError) { + throw std::invalid_argument(status.message); + } + + jsi::Object res = jsi::Object(rt); + + res.setProperty(rt, "rowsAffected", status.affectedRows); + if (status.affectedRows > 0 && status.insertId != 0) { + res.setProperty(rt, "insertId", jsi::Value(status.insertId)); + } + + size_t rowCount = status.rows.size(); + auto rows = jsi::Array(rt, rowCount); + + if (rowCount > 0) { + + auto row = jsi::Array(rt, status.column_names.size()); + for (int i = 0; i < rowCount; i++) { + std::vector native_row = status.rows[i]; + for (int j = 0; j < native_row.size(); j++) { + auto value = toJSI(rt, status.rows[i][j]); + row.setValueAtIndex(rt, j, value); + } + rows.setValueAtIndex(rt, i, std::move(row)); + } + } + res.setProperty(rt, "rawRows", std::move(rows)); + + size_t column_count = status.column_names.size(); + auto column_array = jsi::Array(rt, column_count); + for (int i = 0; i < column_count; i++) { + auto column = status.column_names.at(i); + column_array.setValueAtIndex( + rt, i, toJSI(rt, column)); + } + res.setProperty(rt, "columnNames", std::move(column_array)); + return res; +} + jsi::Value createResult(jsi::Runtime &rt, BridgeResult status, std::vector *results, @@ -266,14 +306,13 @@ BatchResult importSQLFile(std::string dbName, std::string fileLocation) { try { int affectedRows = 0; int commands = 0; - opsqlite_execute(dbName, "BEGIN EXCLUSIVE TRANSACTION", nullptr, nullptr, - nullptr); + opsqlite_execute(dbName, "BEGIN EXCLUSIVE TRANSACTION", nullptr); while (std::getline(sqFile, line, '\n')) { if (!line.empty()) { BridgeResult result = - opsqlite_execute(dbName, line, nullptr, nullptr, nullptr); + opsqlite_execute(dbName, line, nullptr); if (result.type == SQLiteError) { - opsqlite_execute(dbName, "ROLLBACK", nullptr, nullptr, nullptr); + opsqlite_execute(dbName, "ROLLBACK", nullptr); sqFile.close(); return {SQLiteError, result.message, 0, commands}; } else { @@ -283,11 +322,11 @@ BatchResult importSQLFile(std::string dbName, std::string fileLocation) { } } sqFile.close(); - opsqlite_execute(dbName, "COMMIT", nullptr, nullptr, nullptr); + opsqlite_execute(dbName, "COMMIT", nullptr); return {SQLiteOk, "", affectedRows, commands}; } catch (...) { sqFile.close(); - opsqlite_execute(dbName, "ROLLBACK", nullptr, nullptr, nullptr); + opsqlite_execute(dbName, "ROLLBACK", nullptr); return {SQLiteError, "[op-sqlite][loadSQLFile] Unexpected error, transaction was " "rolledback", diff --git a/cpp/utils.h b/cpp/utils.h index cd2599b8..6f6eb330 100644 --- a/cpp/utils.h +++ b/cpp/utils.h @@ -23,9 +23,11 @@ std::vector to_int_vec(jsi::Runtime &rt, jsi::Value const &xs); jsi::Value createResult(jsi::Runtime &rt, BridgeResult status, std::vector *results, std::shared_ptr> metadata); +jsi::Value create_js_rows(jsi::Runtime &rt, BridgeResult status); jsi::Value create_raw_result(jsi::Runtime &rt, BridgeResult status, const std::vector> *results); + void to_batch_arguments(jsi::Runtime &rt, jsi::Array const &batchParams, std::vector *commands); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 79325530..2d3484ae 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -7,7 +7,7 @@ PODS: - hermes-engine (0.74.0): - hermes-engine/Pre-built (= 0.74.0) - hermes-engine/Pre-built (0.74.0) - - op-sqlite (7.0.0): + - op-sqlite (7.1.0): - React - React-callinvoker - React-Core @@ -1358,7 +1358,7 @@ SPEC CHECKSUMS: fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: 6eae7edb2f563ee41d7c1f91f4f2e57c26d8a5c3 - op-sqlite: a8bc5990d5d1774aafbbad7da708da10e03343e3 + op-sqlite: 743e833721d4b4a0af7e7fefbff4375dc2d4e559 RCT-Folly: 045d6ecaa59d826c5736dfba0b2f4083ff8d79df RCTDeprecation: 3ca8b6c36bfb302e1895b72cfe7db0de0c92cd47 RCTRequired: 9fc183af555fd0c89a366c34c1ae70b7e03b1dc5 diff --git a/example/src/App.tsx b/example/src/App.tsx index 71ea747a..0b10ad42 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -38,13 +38,13 @@ export default function App() { useEffect(() => { setResults([]); runTests( - dbSetupTests, + // dbSetupTests, queriesTests, - blobTests, - registerHooksTests, - preparedStatementsTests, - constantsTests, - reactiveTests, + // blobTests, + // registerHooksTests, + // preparedStatementsTests, + // constantsTests, + // reactiveTests, ).then(setResults); }, []); diff --git a/example/src/tests/blob.spec.ts b/example/src/tests/blob.spec.ts index 02311099..da834649 100644 --- a/example/src/tests/blob.spec.ts +++ b/example/src/tests/blob.spec.ts @@ -7,7 +7,7 @@ let expect = chai.expect; let db: DB; export function blobTests() { - beforeEach(() => { + beforeEach(async () => { try { if (db) { db.close(); @@ -19,8 +19,8 @@ export function blobTests() { encryptionKey: 'test', }); - db.execute('DROP TABLE IF EXISTS BlobTable;'); - db.execute( + await db.execute('DROP TABLE IF EXISTS BlobTable;'); + await db.execute( 'CREATE TABLE BlobTable ( id INT PRIMARY KEY, content BLOB) STRICT;', ); } catch (e) { diff --git a/example/src/tests/dbsetup.spec.ts b/example/src/tests/dbsetup.spec.ts index 195a9ed8..e14da2fa 100644 --- a/example/src/tests/dbsetup.spec.ts +++ b/example/src/tests/dbsetup.spec.ts @@ -27,7 +27,7 @@ export function dbSetupTests() { encryptionKey: 'test', }); - const res = db.execute('select sqlite_version();'); + const res = await db.execute('select sqlite_version();'); expect(res.rows?._array[0]['sqlite_version()']).to.equal(expectedVersion); db.close(); diff --git a/example/src/tests/hooks.spec.ts b/example/src/tests/hooks.spec.ts index 4f4a368e..7560c98b 100644 --- a/example/src/tests/hooks.spec.ts +++ b/example/src/tests/hooks.spec.ts @@ -15,7 +15,7 @@ const chance = new Chance(); let db: DB; export function registerHooksTests() { - beforeEach(() => { + beforeEach(async () => { try { if (db) { db.close(); @@ -24,8 +24,8 @@ export function registerHooksTests() { db = open(DB_CONFIG); - db.execute('DROP TABLE IF EXISTS User;'); - db.execute( + await db.execute('DROP TABLE IF EXISTS User;'); + await db.execute( 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;', ); } catch (e) { diff --git a/example/src/tests/preparedStatements.spec.ts b/example/src/tests/preparedStatements.spec.ts index d9f4f13e..0f3d7123 100644 --- a/example/src/tests/preparedStatements.spec.ts +++ b/example/src/tests/preparedStatements.spec.ts @@ -7,7 +7,7 @@ let expect = chai.expect; let db: DB; export function preparedStatementsTests() { - beforeEach(() => { + beforeEach(async () => { try { if (db) { db.close(); @@ -19,11 +19,22 @@ export function preparedStatementsTests() { encryptionKey: 'test', }); - db.execute('DROP TABLE IF EXISTS User;'); - db.execute('CREATE TABLE User ( id INT PRIMARY KEY, name TEXT) STRICT;'); - db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [1, 'Oscar']); - db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [2, 'Pablo']); - db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [3, 'Carlos']); + await db.execute('DROP TABLE IF EXISTS User;'); + await db.execute( + 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT) STRICT;', + ); + await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ + 1, + 'Oscar', + ]); + await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ + 2, + 'Pablo', + ]); + await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ + 3, + 'Carlos', + ]); } catch (e) { console.warn('error on before each', e); } diff --git a/example/src/tests/queries.spec.ts b/example/src/tests/queries.spec.ts index 95c1c925..19e502b2 100644 --- a/example/src/tests/queries.spec.ts +++ b/example/src/tests/queries.spec.ts @@ -7,7 +7,7 @@ import { type DB, type SQLBatchTuple, } from '@op-engineering/op-sqlite'; -import {beforeEach, describe, it} from './MochaRNAdapter'; +import {beforeEach, describe, it, itOnly} from './MochaRNAdapter'; import chai from 'chai'; const expect = chai.expect; @@ -15,7 +15,7 @@ const chance = new Chance(); let db: DB; export function queriesTests() { - beforeEach(() => { + beforeEach(async () => { try { if (db) { db.close(); @@ -27,8 +27,10 @@ export function queriesTests() { encryptionKey: 'test', }); - db.execute('DROP TABLE IF EXISTS User;'); - db.execute( + await db.execute('DROP TABLE IF EXISTS User;'); + await db.execute('DROP TABLE IF EXISTS T1;'); + await db.execute('DROP TABLE IF EXISTS T2;'); + await db.execute( 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;', ); } catch (e) { @@ -45,7 +47,7 @@ export function queriesTests() { 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', }); - const res = remoteDb.execute('SELECT 1'); + const res = await remoteDb.execute('SELECT 1'); expect(res.rowsAffected).to.equal(0); }); @@ -59,7 +61,7 @@ export function queriesTests() { syncInterval: 1000, }); - const res = remoteDb.execute('SELECT 1'); + const res = await remoteDb.execute('SELECT 1'); remoteDb.sync(); @@ -72,14 +74,14 @@ export function queriesTests() { const name = chance.name(); const age = chance.integer(); const networth = chance.floating(); - const res = db.execute( + const res = await db.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); expect(res.rowsAffected).to.equal(1); expect(res.insertId).to.equal(1); - expect(res.metadata).to.eql([]); + // expect(res.metadata).to.eql([]); expect(res.rows?._array).to.eql([]); expect(res.rows?.length).to.equal(0); expect(res.rows?.item).to.be.a('function'); @@ -90,12 +92,15 @@ export function queriesTests() { const name = chance.name(); const age = chance.integer(); const networth = chance.floating(); - db.execute( + await db.execute( 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); - const res = db.execute('SELECT * FROM User'); + console.log('🟩 INSERTED'); + + const res = await db.execute('SELECT * FROM User'); + console.log('🟩 SELECTED'); expect(res.rowsAffected).to.equal(1); expect(res.insertId).to.equal(1); @@ -115,12 +120,12 @@ export function queriesTests() { const name = chance.name(); const age = chance.integer(); const networth = chance.floating(); - db.execute( + await db.execute( 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); - const res = db.execute('SELECT * FROM User WHERE id = ?', [id]); + const res = await db.execute('SELECT * FROM User WHERE id = ?', [id]); expect(res.rowsAffected).to.equal(1); expect(res.insertId).to.equal(1); @@ -142,14 +147,16 @@ export function queriesTests() { const networth = chance.floating(); // COUNT(*) - db.execute( + await db.execute( 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); - const countRes = db.execute('SELECT COUNT(*) as count FROM User'); + const countRes = await db.execute('SELECT COUNT(*) as count FROM User'); + + console.log(countRes); - expect(countRes.metadata?.[0]?.type).to.equal('UNKNOWN'); + // expect(countRes.metadata?.[0]?.type).to.equal('UNKNOWN'); expect(countRes.rows?._array.length).to.equal(1); expect(countRes.rows?.item(0).count).to.equal(1); @@ -159,21 +166,25 @@ export function queriesTests() { const age2 = chance.integer(); const networth2 = chance.floating(); - db.execute( + await db.execute( 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', [id2, name2, age2, networth2], ); - const sumRes = db.execute('SELECT SUM(age) as sum FROM User;'); + const sumRes = await db.execute('SELECT SUM(age) as sum FROM User;'); - expect(sumRes.metadata?.[0]?.type).to.equal('UNKNOWN'); + // expect(sumRes.metadata?.[0]?.type).to.equal('UNKNOWN'); expect(sumRes.rows?.item(0).sum).to.equal(age + age2); // MAX(networth), MIN(networth) - const maxRes = db.execute('SELECT MAX(networth) as `max` FROM User;'); - const minRes = db.execute('SELECT MIN(networth) as `min` FROM User;'); - expect(maxRes.metadata?.[0]?.type).to.equal('UNKNOWN'); - expect(minRes.metadata?.[0]?.type).to.equal('UNKNOWN'); + const maxRes = await db.execute( + 'SELECT MAX(networth) as `max` FROM User;', + ); + const minRes = await db.execute( + 'SELECT MIN(networth) as `min` FROM User;', + ); + // expect(maxRes.metadata?.[0]?.type).to.equal('UNKNOWN'); + // expect(minRes.metadata?.[0]?.type).to.equal('UNKNOWN'); const maxNetworth = Math.max(networth, networth2); const minNetworth = Math.min(networth, networth2); @@ -185,21 +196,25 @@ export function queriesTests() { if (isLibsql()) { return; } - db.execute( + await db.execute( `CREATE TABLE T1 ( id INT PRIMARY KEY) STRICT; CREATE TABLE T2 ( id INT PRIMARY KEY) STRICT;`, ); - let t1name = db.execute( + let t1name = await db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='T1';", ); + console.log('t1 🟦🟦🟦🟦🟦', t1name); + expect(t1name.rows?._array[0].name).to.equal('T1'); - let t2name = db.execute( + let t2name = await db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='T2';", ); + console.log('t2 🟦🟦🟦🟦', t2name); + expect(t2name.rows?._array[0].name).to.equal('T2'); }); @@ -210,7 +225,7 @@ export function queriesTests() { const networth = chance.string(); // expect( try { - db.execute( + await db.execute( 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); @@ -223,24 +238,6 @@ export function queriesTests() { } }); - it('Async Insert', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res = await db.executeAsync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).to.equal(1); - expect(res.insertId).to.equal(1); - expect(res.metadata).to.eql([]); - expect(res.rows?._array).to.eql([]); - expect(res.rows?.length).to.equal(0); - expect(res.rows?.item).to.be.a('function'); - }); - it('Transaction, auto commit', async () => { const id = chance.integer(); const name = chance.name(); @@ -248,20 +245,20 @@ export function queriesTests() { const networth = chance.floating(); await db.transaction(async tx => { - const res = tx.execute( + const res = await tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); expect(res.rowsAffected).to.equal(1); expect(res.insertId).to.equal(1); - expect(res.metadata).to.eql([]); + // expect(res.metadata).to.eql([]); expect(res.rows?._array).to.eql([]); expect(res.rows?.length).to.equal(0); expect(res.rows?.item).to.be.a('function'); }); - const res = db.execute('SELECT * FROM User'); + const res = await db.execute('SELECT * FROM User'); expect(res.rows?._array).to.eql([ { id, @@ -280,22 +277,23 @@ export function queriesTests() { const networth = chance.floating(); await db.transaction(async tx => { - const res = tx.execute( + const res = await tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); expect(res.rowsAffected).to.equal(1); expect(res.insertId).to.equal(1); - expect(res.metadata).to.eql([]); + // expect(res.metadata).to.eql([]); expect(res.rows?._array).to.eql([]); expect(res.rows?.length).to.equal(0); expect(res.rows?.item).to.be.a('function'); - tx.commit(); + await tx.commit(); }); - const res = db.execute('SELECT * FROM User'); + const res = await db.execute('SELECT * FROM User'); + console.log(res); expect(res.rows?._array).to.eql([ { id, @@ -322,7 +320,7 @@ export function queriesTests() { for (let iteration = 1; iteration <= iterations; iteration++) { const promised = db.transaction(async tx => { // ACT: Upsert statement to create record / increment the value - tx.execute( + await tx.execute( ` INSERT OR REPLACE INTO [User] ([id], [name], [age], [networth]) SELECT ?, ?, ?, @@ -336,7 +334,7 @@ export function queriesTests() { ); // ACT: Select statement to get incremented value and store it for checking later - const results = tx.execute( + const results = await tx.execute( 'SELECT [networth] FROM [User] WHERE [id] = ?', [id], ); @@ -367,28 +365,28 @@ export function queriesTests() { const networth = chance.floating(); await db.transaction(async tx => { - const res = tx.execute( + const res = await tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); expect(res.rowsAffected).to.equal(1); expect(res.insertId).to.equal(1); - expect(res.metadata).to.eql([]); + // expect(res.metadata).to.eql([]); expect(res.rows?._array).to.eql([]); expect(res.rows?.length).to.equal(0); expect(res.rows?.item).to.be.a('function'); - tx.commit(); + await tx.commit(); try { - tx.execute('SELECT * FROM "User"'); + await tx.execute('SELECT * FROM "User"'); } catch (e) { expect(!!e).to.equal(true); } }); - const res = db.execute('SELECT * FROM User'); + const res = await db.execute('SELECT * FROM User'); expect(res.rows?._array).to.eql([ { id, @@ -408,7 +406,7 @@ export function queriesTests() { await db.transaction(async tx => { try { - tx.execute( + await tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); @@ -417,17 +415,17 @@ export function queriesTests() { } }); - const res = db.execute('SELECT * FROM User'); + const res = await db.execute('SELECT * FROM User'); expect(res.rows?._array).to.eql([]); }); - it('Correctly throws', () => { + it('Correctly throws', async () => { const id = chance.string(); const name = chance.name(); const age = chance.integer(); const networth = chance.floating(); try { - db.execute( + await db.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); @@ -443,12 +441,12 @@ export function queriesTests() { const networth = chance.floating(); await db.transaction(async tx => { - tx.execute( + await tx.execute( 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); tx.rollback(); - const res = db.execute('SELECT * FROM User'); + const res = await db.execute('SELECT * FROM User'); expect(res.rows?._array).to.eql([]); }); }); @@ -471,7 +469,7 @@ export function queriesTests() { it('Transaction, rejects on invalid query', async () => { const promised = db.transaction(async tx => { - tx.execute('SELECT * FROM [tableThatDoesNotExist];'); + await tx.execute('SELECT * FROM [tableThatDoesNotExist];'); }); // ASSERT: should return a promise that eventually rejects @@ -503,230 +501,7 @@ export function queriesTests() { expect(ranCallback).to.equal(true, 'Should handle async callback'); }); - it('Async transaction, auto commit', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - const res = await tx.executeAsync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).to.equal(1); - expect(res.insertId).to.equal(1); - expect(res.metadata).to.eql([]); - expect(res.rows?._array).to.eql([]); - expect(res.rows?.length).to.equal(0); - expect(res.rows?.item).to.be.a('function'); - }); - - const res = db.execute('SELECT * FROM User'); - expect(res.rows?._array).to.eql([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it('Async transaction, auto rollback', async () => { - const id = chance.string(); // Causes error because it should be an integer - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - try { - await db.transaction(async tx => { - await tx.executeAsync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - }); - } catch (error) { - expect(error).to.be.instanceOf(Error); - expect((error as Error).message) - .to.include('error') - .and.to.include('cannot store TEXT value in INT column User.id'); - - const res = db.execute('SELECT * FROM User'); - expect(res.rows?._array).to.eql([]); - } - }); - - it('Async transaction, manual commit', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - await tx.executeAsync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - tx.commit(); - }); - - const res = db.execute('SELECT * FROM User'); - expect(res.rows?._array).to.eql([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it('Async transaction, manual rollback', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - await tx.executeAsync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - tx.rollback(); - }); - - const res = db.execute('SELECT * FROM User'); - expect(res.rows?._array).to.eql([]); - }); - - it('Async transaction, executed in order', async () => { - // ARRANGE: Setup for multiple transactions - const iterations = 10; - const actual: unknown[] = []; - - // ARRANGE: Generate expected data - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - - // ACT: Start multiple async transactions to upsert and select the same record - const promises = []; - for (let iteration = 1; iteration <= iterations; iteration++) { - const promised = db.transaction(async tx => { - // ACT: Upsert statement to create record / increment the value - await tx.executeAsync( - `INSERT OR REPLACE INTO [User] ([id], [name], [age], [networth]) - SELECT ?, ?, ?, - IFNULL(( - SELECT [networth] + 1000 - FROM [User] - WHERE [id] = ? - ), 0) - `, - [id, name, age, id], - ); - - // ACT: Select statement to get incremented value and store it for checking later - const results = await tx.executeAsync( - 'SELECT [networth] FROM [User] WHERE [id] = ?', - [id], - ); - - actual.push(results.rows?._array[0].networth); - }); - - promises.push(promised); - } - - // ACT: Wait for all transactions to complete - await Promise.all(promises); - - // ASSERT: That the expected values where returned - const expected = Array(iterations) - .fill(0) - .map((_, index) => index * 1000); - expect(actual).to.eql( - expected, - 'Each transaction should read a different value', - ); - }); - - it('Async transaction, rejects on callback error', async () => { - const promised = db.transaction(async () => { - throw new Error('Error from callback'); - }); - - // ASSERT: should return a promise that eventually rejects - expect(promised).to.have.property('then').that.is.a('function'); - try { - await promised; - expect.fail('Should not resolve'); - } catch (e) { - expect(e).to.be.a.instanceof(Error); - expect((e as Error)?.message).to.equal('Error from callback'); - } - }); - - it('Async transaction, rejects on invalid query', async () => { - const promised = db.transaction(async tx => { - await tx.executeAsync('SELECT * FROM [tableThatDoesNotExist];'); - }); - - // ASSERT: should return a promise that eventually rejects - expect(promised).to.have.property('then').that.is.a('function'); - try { - await promised; - expect.fail('Should not resolve'); - } catch (e) { - expect(e).to.be.a.instanceof(Error); - expect((e as Error)?.message).to.include( - 'no such table: tableThatDoesNotExist', - ); - } - }); - - it('Batch execute', () => { - const id1 = chance.integer(); - const name1 = chance.name(); - const age1 = chance.integer(); - const networth1 = chance.floating(); - - const id2 = chance.integer(); - const name2 = chance.name(); - const age2 = chance.integer(); - const networth2 = chance.floating(); - - const commands: SQLBatchTuple[] = [ - [ - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id1, name1, age1, networth1], - ], - [ - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id2, name2, age2, networth2], - ], - ]; - - db.executeBatch(commands); - - const res = db.execute('SELECT * FROM User'); - expect(res.rows?._array).to.eql([ - {id: id1, name: name1, age: age1, networth: networth1, nickname: null}, - { - id: id2, - name: name2, - age: age2, - networth: networth2, - nickname: null, - }, - ]); - }); - - it('Async batch execute', async () => { + it('Batch execute', async () => { const id1 = chance.integer(); const name1 = chance.name(); const age1 = chance.integer(); @@ -748,9 +523,9 @@ export function queriesTests() { ], ]; - await db.executeBatchAsync(commands); + await db.executeBatch(commands); - const res = db.execute('SELECT * FROM User'); + const res = await db.execute('SELECT * FROM User'); expect(res.rows?._array).to.eql([ {id: id1, name: name1, age: age1, networth: networth1, nickname: null}, { @@ -768,12 +543,12 @@ export function queriesTests() { const name = chance.name(); const age = chance.integer(); const networth = chance.floating(); - db.execute( + await db.execute( 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); - const res = db.execute('SELECT * FROM User'); + const res = await db.executeWithHostObjects('SELECT * FROM User'); expect(res.rowsAffected).to.equal(1); expect(res.insertId).to.equal(1); @@ -797,12 +572,12 @@ export function queriesTests() { const name = chance.name(); const age = chance.integer(); const networth = chance.floating(); - db.execute( + await db.execute( 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); - const res = db.execute('SELECT * FROM User'); + const res = await db.executeWithHostObjects('SELECT * FROM User'); expect(res.rowsAffected).to.equal(1); expect(res.insertId).to.equal(1); @@ -826,25 +601,25 @@ export function queriesTests() { const name = chance.name(); const age = chance.integer(); const networth = chance.floating(); - db.execute( + await db.execute( 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', [id, name, age, networth], ); - const res = await db.executeRawAsync( + const res = await db.executeRaw( 'SELECT id, name, age, networth FROM User', ); expect(res).to.eql([[id, name, age, networth]]); }); - it('Create fts5 virtual table', () => { + it('Create fts5 virtual table', async () => { db.execute('CREATE VIRTUAL TABLE fts5_table USING fts5(name, content);'); db.execute('INSERT INTO fts5_table (name, content) VALUES(?, ?)', [ 'test', 'test content', ]); - const res = db.execute('SELECT * FROM fts5_table'); + const res = await db.execute('SELECT * FROM fts5_table'); expect(res.rows?._array).to.eql([ {name: 'test', content: 'test content'}, ]); diff --git a/example/src/tests/reactive.spec.ts b/example/src/tests/reactive.spec.ts index 167fdec7..c22fa33f 100644 --- a/example/src/tests/reactive.spec.ts +++ b/example/src/tests/reactive.spec.ts @@ -9,7 +9,7 @@ const chance = new Chance(); let db: DB; export function reactiveTests() { - beforeEach(() => { + beforeEach(async () => { try { if (db) { db.close(); @@ -21,8 +21,8 @@ export function reactiveTests() { encryptionKey: 'test', }); - db.execute('DROP TABLE IF EXISTS User;'); - db.execute( + await db.execute('DROP TABLE IF EXISTS User;'); + await db.execute( 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;', ); } catch (e) { diff --git a/src/index.ts b/src/index.ts index 86bc9428..af0cf247 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,6 +71,10 @@ export type QueryResult = { */ item: (idx: number) => any; }; + // An array of intermediate results, just values without column names + rawRows?: any[]; + columnNames?: string[]; + /** * Query metadata, available only for select query results */ @@ -119,13 +123,9 @@ export interface FileLoadResult extends BatchQueryResult { } export interface Transaction { - commit: () => QueryResult; - execute: (query: string, params?: any[]) => QueryResult; - executeAsync: ( - query: string, - params?: any[] | undefined - ) => Promise; - rollback: () => QueryResult; + commit: () => Promise; + execute: (query: string, params?: any[]) => Promise; + rollback: () => Promise; } export interface PendingTransaction { @@ -157,10 +157,12 @@ export type DB = { ) => void; detach: (mainDbName: string, alias: string) => void; transaction: (fn: (tx: Transaction) => Promise) => Promise; - execute: (query: string, params?: any[]) => QueryResult; - executeAsync: (query: string, params?: any[]) => Promise; - executeBatch: (commands: SQLBatchTuple[]) => BatchQueryResult; - executeBatchAsync: (commands: SQLBatchTuple[]) => Promise; + execute: (query: string, params?: any[]) => Promise; + executeWithHostObjects: ( + query: string, + params?: any[] + ) => Promise; + executeBatch: (commands: SQLBatchTuple[]) => Promise; loadFile: (location: string) => Promise; updateHook: ( callback?: @@ -176,7 +178,7 @@ export type DB = { rollbackHook: (callback?: (() => void) | null) => void; prepareStatement: (query: string) => PreparedStatementObj; loadExtension: (path: string, entryPoint?: string) => void; - executeRawAsync: (query: string, params?: any[]) => Promise; + executeRaw: (query: string, params?: any[]) => Promise; getDbPath: (location?: string) => string; reactiveExecute: (params: { query: string; @@ -260,14 +262,12 @@ function enhanceDB(db: DB, options: any): DB { attach: db.attach, detach: db.detach, executeBatch: db.executeBatch, - executeBatchAsync: db.executeBatchAsync, - loadFile: db.loadFile, updateHook: db.updateHook, commitHook: db.commitHook, rollbackHook: db.rollbackHook, loadExtension: db.loadExtension, - executeRawAsync: db.executeRawAsync, + executeRaw: db.executeRaw, getDbPath: db.getDbPath, reactiveExecute: db.reactiveExecute, sync: db.sync, @@ -275,7 +275,10 @@ function enhanceDB(db: DB, options: any): DB { db.close(); delete locks[options.url]; }, - execute: (query: string, params?: any[] | undefined): QueryResult => { + executeWithHostObjects: async ( + query: string, + params?: any[] | undefined + ): Promise => { const sanitizedParams = params?.map((p) => { if (ArrayBuffer.isView(p)) { return p.buffer; @@ -284,11 +287,11 @@ function enhanceDB(db: DB, options: any): DB { return p; }); - const result = db.execute(query, sanitizedParams); + const result = await db.executeWithHostObjects(query, sanitizedParams); enhanceQueryResult(result); return result; }, - executeAsync: async ( + execute: async ( query: string, params?: any[] | undefined ): Promise => { @@ -299,10 +302,31 @@ function enhanceDB(db: DB, options: any): DB { return p; }); + console.log('🟧'); + let intermediateResult = await db.execute(query, sanitizedParams); + console.log('🟦'); + console.log('query', query); + console.log('intermediate result', intermediateResult); + let rows: any[] = []; + for (let i = 0; i < (intermediateResult.rawRows?.length ?? 0); i++) { + let row: any = {}; + for (let j = 0; j < intermediateResult.columnNames!.length ?? 0; j++) { + let columnName = intermediateResult.columnNames![j]!; + row[columnName] = intermediateResult.rawRows![i][j]; + } + rows.push(row); + } - const result = await db.executeAsync(query, sanitizedParams); - enhanceQueryResult(result); - return result; + let res = { + ...intermediateResult, + rows: { + _array: rows, + length: rows.length, + item: (idx: number) => rows[idx], + }, + }; + console.log('returning', res); + return res; }, prepareStatement: (query: string) => { const stmt = db.prepareStatement(query); @@ -331,55 +355,67 @@ function enhanceDB(db: DB, options: any): DB { ): Promise => { let isFinalized = false; - // Local transaction context object implementation - const execute = (query: string, params?: any[]): QueryResult => { + const execute = async (query: string, params?: any[] | undefined) => { if (isFinalized) { throw Error( `OP-Sqlite Error: Database: ${options.url}. Cannot execute query on finalized transaction` ); } - return enhancedDb.execute(query, params); - }; - - const executeAsync = (query: string, params?: any[] | undefined) => { - if (isFinalized) { - throw Error( - `OP-Sqlite Error: Database: ${options.url}. Cannot execute query on finalized transaction` - ); + let intermediateResult = await enhancedDb.execute(query, params); + let rows: any[] = []; + for (let i = 0; i < (intermediateResult.rawRows?.length ?? 0); i++) { + let row: any = {}; + for ( + let j = 0; + j < intermediateResult.columnNames!.length ?? 0; + j++ + ) { + let columnName = intermediateResult.columnNames![j]!; + row[columnName] = intermediateResult.rawRows![i][j]; + } + rows.push(row); } - return enhancedDb.executeAsync(query, params); + + let res = { + ...intermediateResult, + rows: { + _array: rows, + length: 0, + item: (idx: number) => rows[idx], + }, + }; + return res; }; - const commit = () => { + const commit = async (): Promise => { if (isFinalized) { throw Error( `OP-Sqlite Error: Database: ${options.url}. Cannot execute query on finalized transaction` ); } - const result = enhancedDb.execute('COMMIT;'); + const result = await enhancedDb.execute('COMMIT;'); isFinalized = true; return result; }; - const rollback = () => { + const rollback = async (): Promise => { if (isFinalized) { throw Error( `OP-Sqlite Error: Database: ${options.url}. Cannot execute query on finalized transaction` ); } - const result = enhancedDb.execute('ROLLBACK;'); + const result = await enhancedDb.execute('ROLLBACK;'); isFinalized = true; return result; }; async function run() { try { - await enhancedDb.executeAsync('BEGIN TRANSACTION;'); + await enhancedDb.execute('BEGIN TRANSACTION;'); await fn({ commit, execute, - executeAsync, rollback, });