diff --git a/cpp/bridge.cpp b/cpp/bridge.cpp index adafe298..8033f9be 100644 --- a/cpp/bridge.cpp +++ b/cpp/bridge.cpp @@ -111,9 +111,8 @@ sqlite3 *opsqlite_open(std::string const &name, std::string const &path, // buffer with explicit length so embedded zero bytes in the key are // preserved, and it never enters trace/log surfaces — defense in // depth, not an exploit class on its own. - int key_status = sqlite3_key_v2( - db, "main", encryption_key.data(), - static_cast(encryption_key.size())); + int key_status = sqlite3_key_v2(db, "main", encryption_key.data(), + static_cast(encryption_key.size())); if (key_status != SQLITE_OK) { const char *message = sqlite3_errmsg(db); throw std::runtime_error( @@ -652,8 +651,7 @@ BridgeResult opsqlite_execute_host_objects( .insertId = static_cast(latestInsertRowId)}; } -/// Executes returning data in raw arrays, a small performance optimization -/// for certain use cases +/// Executes returning data in raw arrays BridgeResult opsqlite_execute_raw(sqlite3 *db, std::string const &query, const std::vector *params, @@ -666,6 +664,7 @@ opsqlite_execute_raw(sqlite3 *db, std::string const &query, bool isFailed = false; int step = SQLITE_OK; + std::vector column_names; do { const char *queryStr = @@ -696,6 +695,13 @@ opsqlite_execute_raw(sqlite3 *db, std::string const &query, int column_count = sqlite3_column_count(statement); + column_names.clear(); + column_names.reserve(column_count); + for (int column_index = 0; column_index < column_count; column_index++) { + column_name = sqlite3_column_name(statement, column_index); + column_names.emplace_back(column_name); + } + while (isConsuming) { step = sqlite3_step(statement); @@ -781,7 +787,8 @@ opsqlite_execute_raw(sqlite3 *db, std::string const &query, long long latestInsertRowId = sqlite3_last_insert_rowid(db); return {.affectedRows = changedRowCount, - .insertId = static_cast(latestInsertRowId)}; + .insertId = static_cast(latestInsertRowId), + .column_names = std::move(column_names)}; } std::string operation_to_string(int operation_type) { diff --git a/cpp/libsql/bridge.cpp b/cpp/libsql/bridge.cpp index 444dbe65..0b096262 100644 --- a/cpp/libsql/bridge.cpp +++ b/cpp/libsql/bridge.cpp @@ -23,18 +23,18 @@ namespace opsqlite { std::string opsqlite_get_db_path(std::string const &db_name, std::string const &location) { - if (location == ":memory:") { - return location; - } + if (location == ":memory:") { + return location; + } - // Will return false if the directory already exists, no need to check - std::filesystem::create_directories(location); + // Will return false if the directory already exists, no need to check + std::filesystem::create_directories(location); - if (!location.empty() && location.back() != '/') { - return location + "/" + db_name; - } + if (!location.empty() && location.back() != '/') { + return location + "/" + db_name; + } - return location + db_name; + return location + db_name; } DB opsqlite_libsql_open_sync(std::string const &name, @@ -43,460 +43,458 @@ DB opsqlite_libsql_open_sync(std::string const &name, std::string const &auth_token, int sync_interval, bool offline, std::string const &encryption_key, std::string const &remote_encryption_key) { - std::string path = opsqlite_get_db_path(name, base_path); - - int status; - libsql_database_t db; - libsql_connection_t c; - const char *err = nullptr; - - libsql_config config = { - .db_path = path.c_str(), - .primary_url = url.c_str(), - .auth_token = auth_token.c_str(), - .read_your_writes = '1', - .encryption_key = - encryption_key.empty() ? nullptr : encryption_key.c_str(), - .remote_encryption_key = remote_encryption_key.empty() - ? nullptr - : remote_encryption_key.c_str(), - .sync_interval = sync_interval, - .with_webpki = '1', - .offline = offline, - }; - - status = libsql_open_sync_with_config(config, &db, &err); - if (status != 0) { - throw std::runtime_error(err); - } - - status = libsql_connect(db, &c, &err); - - if (status != 0) { - throw std::runtime_error(err); - } - - return {.db = db, .c = c}; + std::string path = opsqlite_get_db_path(name, base_path); + + int status; + libsql_database_t db; + libsql_connection_t c; + const char *err = nullptr; + + libsql_config config = { + .db_path = path.c_str(), + .primary_url = url.c_str(), + .auth_token = auth_token.c_str(), + .read_your_writes = '1', + .encryption_key = + encryption_key.empty() ? nullptr : encryption_key.c_str(), + .remote_encryption_key = remote_encryption_key.empty() + ? nullptr + : remote_encryption_key.c_str(), + .sync_interval = sync_interval, + .with_webpki = '1', + .offline = offline, + }; + + status = libsql_open_sync_with_config(config, &db, &err); + if (status != 0) { + throw std::runtime_error(err); + } + + status = libsql_connect(db, &c, &err); + + if (status != 0) { + throw std::runtime_error(err); + } + + return {.db = db, .c = c}; } DB opsqlite_libsql_open(std::string const &name, std::string const &last_path, std::string const &crsqlitePath) { - std::string path = opsqlite_get_db_path(name, last_path); + std::string path = opsqlite_get_db_path(name, last_path); - int status; - libsql_database_t db; - libsql_connection_t c; - const char *err = nullptr; + int status; + libsql_database_t db; + libsql_connection_t c; + const char *err = nullptr; - status = libsql_open_file(path.c_str(), &db, &err); + status = libsql_open_file(path.c_str(), &db, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - status = libsql_connect(db, &c, &err); + status = libsql_connect(db, &c, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } #ifdef OP_SQLITE_USE_CRSQLITE - const char *errMsg; - const char *crsqliteEntryPoint = "sqlite3_crsqlite_init"; + const char *errMsg; + const char *crsqliteEntryPoint = "sqlite3_crsqlite_init"; - status = libsql_load_extension(c, crsqlitePath.c_str(), crsqliteEntryPoint, - &errMsg); + status = libsql_load_extension(c, crsqlitePath.c_str(), crsqliteEntryPoint, + &errMsg); - if (status != 0) { - throw std::runtime_error(errMsg); - } else { - LOGI("Loaded CRSQlite successfully"); - } + if (status != 0) { + throw std::runtime_error(errMsg); + } else { + LOGI("Loaded CRSQlite successfully"); + } #endif - return {.db = db, .c = c}; + return {.db = db, .c = c}; } DB opsqlite_libsql_open_remote(std::string const &url, std::string const &auth_token) { - int status; - libsql_database_t db; - libsql_connection_t c; - const char *err = nullptr; + int status; + libsql_database_t db; + libsql_connection_t c; + const char *err = nullptr; - status = libsql_open_remote_with_webpki(url.c_str(), auth_token.c_str(), - &db, &err); + status = libsql_open_remote_with_webpki(url.c_str(), auth_token.c_str(), &db, + &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - status = libsql_connect(db, &c, &err); + status = libsql_connect(db, &c, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - return {.db = db, .c = c}; + return {.db = db, .c = c}; } void opsqlite_libsql_close(DB &db) { - if (db.c != nullptr) { - libsql_disconnect(db.c); - db.c = nullptr; - } - if (db.db != nullptr) { - libsql_close(db.db); - db.db = nullptr; - } + if (db.c != nullptr) { + libsql_disconnect(db.c); + db.c = nullptr; + } + if (db.db != nullptr) { + libsql_close(db.db); + db.db = nullptr; + } } void opsqlite_libsql_attach(DB const &db, std::string const &docPath, std::string const &databaseToAttach, std::string const &alias) { - std::string dbPath = opsqlite_get_db_path(databaseToAttach, docPath); - std::vector params = {dbPath, alias}; - opsqlite_libsql_execute(db, "ATTACH DATABASE ? AS ?", ¶ms); + std::string dbPath = opsqlite_get_db_path(databaseToAttach, docPath); + std::vector params = {dbPath, alias}; + opsqlite_libsql_execute(db, "ATTACH DATABASE ? AS ?", ¶ms); } void opsqlite_libsql_detach(DB const &db, std::string const &alias) { - std::vector params = {alias}; - opsqlite_libsql_execute(db, "DETACH DATABASE ?", ¶ms); + std::vector params = {alias}; + opsqlite_libsql_execute(db, "DETACH DATABASE ?", ¶ms); } int32_t opsqlite_libsql_get_reserved_bytes(DB const &db) { - const char *err = nullptr; - int32_t reserved_bytes = 0; + const char *err = nullptr; + int32_t reserved_bytes = 0; - int status = libsql_get_reserved_bytes(db.c, &reserved_bytes, &err); + int status = libsql_get_reserved_bytes(db.c, &reserved_bytes, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - return reserved_bytes; + return reserved_bytes; } void opsqlite_libsql_set_reserved_bytes(DB const &db, int32_t reserved_bytes) { - const char *err = nullptr; + const char *err = nullptr; - int status = libsql_set_reserved_bytes(db.c, reserved_bytes, &err); + int status = libsql_set_reserved_bytes(db.c, reserved_bytes, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } } void opsqlite_libsql_sync(DB const &db) { - const char *err = nullptr; + const char *err = nullptr; - int status = libsql_sync(db.db, &err); + int status = libsql_sync(db.db, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } } void opsqlite_libsql_remove(DB &db, std::string const &name, std::string const &path) { - opsqlite_libsql_close(db); + opsqlite_libsql_close(db); - std::string full_path = opsqlite_get_db_path(name, path); + std::string full_path = opsqlite_get_db_path(name, path); - if (!file_exists(full_path)) { - throw std::runtime_error("[op-sqlite]: Database file not found" + - full_path); - } + if (!file_exists(full_path)) { + throw std::runtime_error("[op-sqlite]: Database file not found" + + full_path); + } - remove(full_path.c_str()); + remove(full_path.c_str()); } void opsqlite_libsql_bind_statement(libsql_stmt_t statement, const std::vector *values) { - const char *err; - size_t size = values->size(); - - for (int ii = 0; ii < size; ii++) { - int index = ii + 1; - JSVariant value = values->at(ii); - int status; - - if (std::holds_alternative(value)) { - status = - libsql_bind_int(statement, index, - static_cast(std::get(value)), &err); - } else if (std::holds_alternative(value)) { - status = - libsql_bind_int(statement, index, std::get(value), &err); - } else if (std::holds_alternative(value)) { - status = libsql_bind_int(statement, index, - std::get(value), &err); - } else if (std::holds_alternative(value)) { - status = libsql_bind_float(statement, index, - std::get(value), &err); - } else if (std::holds_alternative(value)) { - std::string str = std::get(value); - status = libsql_bind_string(statement, index, str.c_str(), &err); - } else if (std::holds_alternative(value)) { - ArrayBuffer buffer = std::get(value); - status = libsql_bind_blob(statement, index, buffer.data.get(), - static_cast(buffer.size), &err); - } else { - status = libsql_bind_null(statement, index, &err); - } - - if (status != 0) { - throw std::runtime_error(err); - } + const char *err; + size_t size = values->size(); + + for (int ii = 0; ii < size; ii++) { + int index = ii + 1; + JSVariant value = values->at(ii); + int status; + + if (std::holds_alternative(value)) { + status = libsql_bind_int(statement, index, + static_cast(std::get(value)), &err); + } else if (std::holds_alternative(value)) { + status = libsql_bind_int(statement, index, std::get(value), &err); + } else if (std::holds_alternative(value)) { + status = + libsql_bind_int(statement, index, std::get(value), &err); + } else if (std::holds_alternative(value)) { + status = + libsql_bind_float(statement, index, std::get(value), &err); + } else if (std::holds_alternative(value)) { + std::string str = std::get(value); + status = libsql_bind_string(statement, index, str.c_str(), &err); + } else if (std::holds_alternative(value)) { + ArrayBuffer buffer = std::get(value); + status = libsql_bind_blob(statement, index, buffer.data.get(), + static_cast(buffer.size), &err); + } else { + status = libsql_bind_null(statement, index, &err); + } + + if (status != 0) { + throw std::runtime_error(err); } + } } BridgeResult opsqlite_libsql_execute_prepared_statement( DB const &db, libsql_stmt_t stmt, std::vector *results, const std::shared_ptr> &metadatas) { - libsql_rows_t rows; - libsql_row_t row; + libsql_rows_t rows; + libsql_row_t row; - int status; - const char *err = nullptr; + int status; + const char *err = nullptr; - status = libsql_query_stmt(stmt, &rows, &err); + status = libsql_query_stmt(stmt, &rows, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } + + bool metadata_set = false; - bool metadata_set = false; - - int num_cols = libsql_column_count(rows); - while ((status = libsql_next_row(rows, &row, &err)) == 0) { - - if (!err && !row) { - break; - } - - DumbHostObject row_host_object = DumbHostObject(metadatas); - - for (int col = 0; col < num_cols; col++) { - int type; - - libsql_column_type(rows, row, col, &type, &err); - - switch (type) { - case LIBSQL_INT: - long long int_value; - status = libsql_get_int(row, col, &int_value, &err); - row_host_object.values.emplace_back(int_value); - break; - - case LIBSQL_FLOAT: - double float_value; - status = libsql_get_float(row, col, &float_value, &err); - row_host_object.values.emplace_back(float_value); - break; - - case LIBSQL_TEXT: - const char *text_value; - status = libsql_get_string(row, col, &text_value, &err); - row_host_object.values.emplace_back(text_value); - break; - - case LIBSQL_BLOB: { - blob value_blob; - libsql_get_blob(row, col, &value_blob, &err); - auto *data = new uint8_t[value_blob.len]; - // You cannot share raw memory between native and JS - // always copy the data - memcpy(data, value_blob.ptr, value_blob.len); - libsql_free_blob(value_blob); - row_host_object.values.emplace_back( - ArrayBuffer{.data = std::shared_ptr{data}, - .size = static_cast(value_blob.len)}); - break; - } - - case LIBSQL_NULL: - // intentional fall-through - default: - row_host_object.values.emplace_back(nullptr); - break; - } - - if (status != 0) { - fprintf(stderr, "%s\n", err); - throw std::runtime_error("libsql error"); - } - - // On the first interation through the columns, set the metadata - if (!metadata_set && metadatas != nullptr) { - const char *col_name; - status = libsql_column_name(rows, col, &col_name, &err); - - auto metadata = SmartHostObject(); - metadata.fields.emplace_back("name", col_name); - metadata.fields.emplace_back("index", col); - metadata.fields.emplace_back("type", "UNKNOWN"); - // metadata.fields.push_back( - // std::make_pair("type", type == -1 ? - // "UNKNOWN" : type)); - - metadatas->push_back(metadata); - } - } - - if (results != nullptr) { - results->push_back(row_host_object); - } - - metadata_set = true; - err = nullptr; + int num_cols = libsql_column_count(rows); + while ((status = libsql_next_row(rows, &row, &err)) == 0) { + + if (!err && !row) { + break; } - if (status != 0) { + DumbHostObject row_host_object = DumbHostObject(metadatas); + + for (int col = 0; col < num_cols; col++) { + int type; + + libsql_column_type(rows, row, col, &type, &err); + + switch (type) { + case LIBSQL_INT: + long long int_value; + status = libsql_get_int(row, col, &int_value, &err); + row_host_object.values.emplace_back(int_value); + break; + + case LIBSQL_FLOAT: + double float_value; + status = libsql_get_float(row, col, &float_value, &err); + row_host_object.values.emplace_back(float_value); + break; + + case LIBSQL_TEXT: + const char *text_value; + status = libsql_get_string(row, col, &text_value, &err); + row_host_object.values.emplace_back(text_value); + break; + + case LIBSQL_BLOB: { + blob value_blob; + libsql_get_blob(row, col, &value_blob, &err); + auto *data = new uint8_t[value_blob.len]; + // You cannot share raw memory between native and JS + // always copy the data + memcpy(data, value_blob.ptr, value_blob.len); + libsql_free_blob(value_blob); + row_host_object.values.emplace_back( + ArrayBuffer{.data = std::shared_ptr{data}, + .size = static_cast(value_blob.len)}); + break; + } + + case LIBSQL_NULL: + // intentional fall-through + default: + row_host_object.values.emplace_back(nullptr); + break; + } + + if (status != 0) { fprintf(stderr, "%s\n", err); + throw std::runtime_error("libsql error"); + } + + // On the first interation through the columns, set the metadata + if (!metadata_set && metadatas != nullptr) { + const char *col_name; + status = libsql_column_name(rows, col, &col_name, &err); + + auto metadata = SmartHostObject(); + metadata.fields.emplace_back("name", col_name); + metadata.fields.emplace_back("index", col); + metadata.fields.emplace_back("type", "UNKNOWN"); + // metadata.fields.push_back( + // std::make_pair("type", type == -1 ? + // "UNKNOWN" : type)); + + metadatas->push_back(metadata); + } } - libsql_free_rows(rows); + if (results != nullptr) { + results->push_back(row_host_object); + } + + metadata_set = true; + err = nullptr; + } + + if (status != 0) { + fprintf(stderr, "%s\n", err); + } + + libsql_free_rows(rows); - unsigned long long changes = libsql_changes(db.c); - long long insert_row_id = libsql_last_insert_rowid(db.c); + unsigned long long changes = libsql_changes(db.c); + long long insert_row_id = libsql_last_insert_rowid(db.c); - libsql_reset_stmt(stmt, &err); + libsql_reset_stmt(stmt, &err); - return {.affectedRows = static_cast(changes), - .insertId = static_cast(insert_row_id)}; + return {.affectedRows = static_cast(changes), + .insertId = static_cast(insert_row_id)}; } libsql_stmt_t opsqlite_libsql_prepare_statement(DB const &db, std::string const &query) { - libsql_stmt_t stmt; + libsql_stmt_t stmt; - const char *err; + const char *err; - int status = libsql_prepare(db.c, query.c_str(), &stmt, &err); + int status = libsql_prepare(db.c, query.c_str(), &stmt, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - return stmt; + return stmt; } BridgeResult opsqlite_libsql_execute(DB const &db, std::string const &query, const std::vector *params) { - std::vector column_names; - std::vector> out_rows; - std::vector out_row; - libsql_rows_t rows; - libsql_row_t row; - libsql_stmt_t stmt; - int status; - const char *err = nullptr; + std::vector column_names; + std::vector> out_rows; + std::vector out_row; + libsql_rows_t rows; + libsql_row_t row; + libsql_stmt_t stmt; + int status; + const char *err = nullptr; - status = libsql_prepare(db.c, query.c_str(), &stmt, &err); + status = libsql_prepare(db.c, query.c_str(), &stmt, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - if (params != nullptr && !params->empty()) { - opsqlite_libsql_bind_statement(stmt, params); - } + if (params != nullptr && !params->empty()) { + opsqlite_libsql_bind_statement(stmt, params); + } - status = libsql_query_stmt(stmt, &rows, &err); + status = libsql_query_stmt(stmt, &rows, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - // Get the column names on the first pass - int column_count = libsql_column_count(rows); - const char *col_name; + // Get the column names on the first pass + int column_count = libsql_column_count(rows); + const char *col_name; - for (int i = 0; i < column_count; i++) { - status = libsql_column_name(rows, i, &col_name, &err); - if (status != 0) { - throw std::runtime_error(err); - } - column_names.emplace_back(col_name); + for (int i = 0; i < column_count; i++) { + status = libsql_column_name(rows, i, &col_name, &err); + if (status != 0) { + throw std::runtime_error(err); + } + column_names.emplace_back(col_name); + } + + long long int_value; + double float_value; + const char *text_value; + blob blob_value; + + status = libsql_next_row(rows, &row, &err); + while (status == 0) { + out_row = std::vector(); + + if (!err && !row) { + break; + } + + for (int col = 0; col < column_count; col++) { + int type; + + libsql_column_type(rows, row, col, &type, &err); + + switch (type) { + case LIBSQL_INT: + status = libsql_get_int(row, col, &int_value, &err); + out_row.emplace_back(int_value); + break; + + case LIBSQL_FLOAT: + status = libsql_get_float(row, col, &float_value, &err); + out_row.emplace_back(float_value); + break; + + case LIBSQL_TEXT: + status = libsql_get_string(row, col, &text_value, &err); + out_row.emplace_back(text_value); + break; + + case LIBSQL_BLOB: { + libsql_get_blob(row, col, &blob_value, &err); + auto data = new uint8_t[blob_value.len]; + // You cannot share raw memory between native and JS + // always copy the data + memcpy(data, blob_value.ptr, blob_value.len); + libsql_free_blob(blob_value); + out_row.emplace_back( + ArrayBuffer{.data = std::shared_ptr{data}, + .size = static_cast(blob_value.len)}); + break; + } + + case LIBSQL_NULL: + // intentional fall-through + default: + out_row.emplace_back(nullptr); + break; + } + + if (status != 0) { + throw std::runtime_error(err); + } } - long long int_value; - double float_value; - const char *text_value; - blob blob_value; - + out_rows.emplace_back(out_row); + err = nullptr; status = libsql_next_row(rows, &row, &err); - while (status == 0) { - out_row = std::vector(); - - if (!err && !row) { - break; - } - - for (int col = 0; col < column_count; col++) { - int type; - - libsql_column_type(rows, row, col, &type, &err); - - switch (type) { - case LIBSQL_INT: - status = libsql_get_int(row, col, &int_value, &err); - out_row.emplace_back(int_value); - break; - - case LIBSQL_FLOAT: - status = libsql_get_float(row, col, &float_value, &err); - out_row.emplace_back(float_value); - break; - - case LIBSQL_TEXT: - status = libsql_get_string(row, col, &text_value, &err); - out_row.emplace_back(text_value); - break; - - case LIBSQL_BLOB: { - libsql_get_blob(row, col, &blob_value, &err); - auto data = new uint8_t[blob_value.len]; - // You cannot share raw memory between native and JS - // always copy the data - memcpy(data, blob_value.ptr, blob_value.len); - libsql_free_blob(blob_value); - out_row.emplace_back( - ArrayBuffer{.data = std::shared_ptr{data}, - .size = static_cast(blob_value.len)}); - break; - } - - case LIBSQL_NULL: - // intentional fall-through - default: - out_row.emplace_back(nullptr); - break; - } - - if (status != 0) { - throw std::runtime_error(err); - } - } - - out_rows.emplace_back(out_row); - err = nullptr; - status = libsql_next_row(rows, &row, &err); - } + } - libsql_free_rows(rows); - libsql_free_stmt(stmt); + libsql_free_rows(rows); + libsql_free_stmt(stmt); - unsigned long long changes = libsql_changes(db.c); - long long insert_row_id = libsql_last_insert_rowid(db.c); + unsigned long long changes = libsql_changes(db.c); + long long insert_row_id = libsql_last_insert_rowid(db.c); - return {.affectedRows = static_cast(changes), - .insertId = static_cast(insert_row_id), - .rows = std::move(out_rows), - .column_names = std::move(column_names)}; + return {.affectedRows = static_cast(changes), + .insertId = static_cast(insert_row_id), + .rows = std::move(out_rows), + .column_names = std::move(column_names)}; } BridgeResult opsqlite_libsql_execute_with_host_objects( @@ -504,126 +502,126 @@ BridgeResult opsqlite_libsql_execute_with_host_objects( const std::vector *params, std::vector *results, const std::shared_ptr> &metadatas) { - libsql_rows_t rows; - libsql_row_t row; - libsql_stmt_t stmt; - int status; - const char *err = nullptr; - - status = libsql_prepare(db.c, query.c_str(), &stmt, &err); - - if (status != 0) { + libsql_rows_t rows; + libsql_row_t row; + libsql_stmt_t stmt; + int status; + const char *err = nullptr; + + status = libsql_prepare(db.c, query.c_str(), &stmt, &err); + + if (status != 0) { + throw std::runtime_error(err); + } + + if (params != nullptr && !params->empty()) { + opsqlite_libsql_bind_statement(stmt, params); + } + + status = libsql_query_stmt(stmt, &rows, &err); + + if (status != 0) { + throw std::runtime_error(err); + } + + bool metadata_set = false; + + int num_cols = libsql_column_count(rows); + while ((status = libsql_next_row(rows, &row, &err)) == 0) { + + if (!err && !row) { + break; + } + + DumbHostObject row_host_object = DumbHostObject(metadatas); + + for (int col = 0; col < num_cols; col++) { + int type; + + libsql_column_type(rows, row, col, &type, &err); + + switch (type) { + case LIBSQL_INT: + long long int_value; + status = libsql_get_int(row, col, &int_value, &err); + row_host_object.values.emplace_back(int_value); + break; + + case LIBSQL_FLOAT: + double float_value; + status = libsql_get_float(row, col, &float_value, &err); + row_host_object.values.emplace_back(float_value); + break; + + case LIBSQL_TEXT: + const char *text_value; + status = libsql_get_string(row, col, &text_value, &err); + row_host_object.values.emplace_back(text_value); + break; + + case LIBSQL_BLOB: { + blob value_blob; + libsql_get_blob(row, col, &value_blob, &err); + auto *data = new uint8_t[value_blob.len]; + // You cannot share raw memory between native and JS + // always copy the data + memcpy(data, value_blob.ptr, value_blob.len); + libsql_free_blob(value_blob); + row_host_object.values.emplace_back( + ArrayBuffer{.data = std::shared_ptr{data}, + .size = static_cast(value_blob.len)}); + break; + } + + case LIBSQL_NULL: + // intentional fall-through + default: + row_host_object.values.emplace_back(nullptr); + break; + } + + if (status != 0) { + fprintf(stderr, "%s\n", err); throw std::runtime_error(err); - } + } - if (params != nullptr && !params->empty()) { - opsqlite_libsql_bind_statement(stmt, params); - } + // On the first interation through the columns, set the metadata + if (!metadata_set && metadatas != nullptr) { + const char *col_name; + status = libsql_column_name(rows, col, &col_name, &err); - status = libsql_query_stmt(stmt, &rows, &err); + auto metadata = SmartHostObject(); + metadata.fields.emplace_back("name", col_name); + metadata.fields.emplace_back("index", col); + metadata.fields.emplace_back("type", "UNKNOWN"); + // metadata.fields.push_back( + // std::make_pair("type", type == -1 ? + // "UNKNOWN" : type)); - if (status != 0) { - throw std::runtime_error(err); + metadatas->push_back(metadata); + } } - bool metadata_set = false; - - int num_cols = libsql_column_count(rows); - while ((status = libsql_next_row(rows, &row, &err)) == 0) { - - if (!err && !row) { - break; - } - - DumbHostObject row_host_object = DumbHostObject(metadatas); - - for (int col = 0; col < num_cols; col++) { - int type; - - libsql_column_type(rows, row, col, &type, &err); - - switch (type) { - case LIBSQL_INT: - long long int_value; - status = libsql_get_int(row, col, &int_value, &err); - row_host_object.values.emplace_back(int_value); - break; - - case LIBSQL_FLOAT: - double float_value; - status = libsql_get_float(row, col, &float_value, &err); - row_host_object.values.emplace_back(float_value); - break; - - case LIBSQL_TEXT: - const char *text_value; - status = libsql_get_string(row, col, &text_value, &err); - row_host_object.values.emplace_back(text_value); - break; - - case LIBSQL_BLOB: { - blob value_blob; - libsql_get_blob(row, col, &value_blob, &err); - auto *data = new uint8_t[value_blob.len]; - // You cannot share raw memory between native and JS - // always copy the data - memcpy(data, value_blob.ptr, value_blob.len); - libsql_free_blob(value_blob); - row_host_object.values.emplace_back( - ArrayBuffer{.data = std::shared_ptr{data}, - .size = static_cast(value_blob.len)}); - break; - } - - case LIBSQL_NULL: - // intentional fall-through - default: - row_host_object.values.emplace_back(nullptr); - break; - } - - if (status != 0) { - fprintf(stderr, "%s\n", err); - throw std::runtime_error(err); - } - - // On the first interation through the columns, set the metadata - if (!metadata_set && metadatas != nullptr) { - const char *col_name; - status = libsql_column_name(rows, col, &col_name, &err); - - auto metadata = SmartHostObject(); - metadata.fields.emplace_back("name", col_name); - metadata.fields.emplace_back("index", col); - metadata.fields.emplace_back("type", "UNKNOWN"); - // metadata.fields.push_back( - // std::make_pair("type", type == -1 ? - // "UNKNOWN" : type)); - - metadatas->push_back(metadata); - } - } - - if (results != nullptr) { - results->push_back(row_host_object); - } - - metadata_set = true; - err = nullptr; + if (results != nullptr) { + results->push_back(row_host_object); } - if (status != 0) { - fprintf(stderr, "%s\n", err); - } + metadata_set = true; + err = nullptr; + } - libsql_free_rows(rows); - libsql_free_stmt(stmt); + if (status != 0) { + fprintf(stderr, "%s\n", err); + } - unsigned long long changes = libsql_changes(db.c); - long long insert_row_id = libsql_last_insert_rowid(db.c); + libsql_free_rows(rows); + libsql_free_stmt(stmt); - return {.affectedRows = static_cast(changes), - .insertId = static_cast(insert_row_id)}; + unsigned long long changes = libsql_changes(db.c); + long long insert_row_id = libsql_last_insert_rowid(db.c); + + return {.affectedRows = static_cast(changes), + .insertId = static_cast(insert_row_id)}; } /// Executes returning data in raw arrays, a small performance optimization @@ -633,139 +631,153 @@ opsqlite_libsql_execute_raw(DB const &db, std::string const &query, const std::vector *params, std::vector> *results) { - libsql_rows_t rows; - libsql_row_t row; - libsql_stmt_t stmt; - int status; - const char *err = nullptr; + libsql_rows_t rows; + libsql_row_t row; + libsql_stmt_t stmt; + int status; + const char *err = nullptr; - status = libsql_prepare(db.c, query.c_str(), &stmt, &err); + status = libsql_prepare(db.c, query.c_str(), &stmt, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - if (params != nullptr && !params->empty()) { - opsqlite_libsql_bind_statement(stmt, params); - } + if (params != nullptr && !params->empty()) { + opsqlite_libsql_bind_statement(stmt, params); + } - status = libsql_query_stmt(stmt, &rows, &err); + status = libsql_query_stmt(stmt, &rows, &err); - if (status != 0) { - throw std::runtime_error(err); - } + if (status != 0) { + throw std::runtime_error(err); + } - int num_cols = libsql_column_count(rows); - while ((status = libsql_next_row(rows, &row, &err)) == 0) { - - if (!err && !row) { - break; - } - - std::vector row_vector; - - for (int col = 0; col < num_cols; col++) { - int type; - - libsql_column_type(rows, row, col, &type, &err); - - switch (type) { - case LIBSQL_INT: - long long int_value; - status = libsql_get_int(row, col, &int_value, &err); - row_vector.emplace_back(int_value); - break; - - case LIBSQL_FLOAT: - double float_value; - status = libsql_get_float(row, col, &float_value, &err); - row_vector.emplace_back(float_value); - break; - - case LIBSQL_TEXT: - const char *text_value; - status = libsql_get_string(row, col, &text_value, &err); - row_vector.emplace_back(text_value); - break; - - case LIBSQL_BLOB: { - blob value_blob; - libsql_get_blob(row, col, &value_blob, &err); - auto *data = new uint8_t[value_blob.len]; - // You cannot share raw memory between native and JS - // always copy the data - memcpy(data, value_blob.ptr, value_blob.len); - libsql_free_blob(value_blob); - row_vector.emplace_back( - ArrayBuffer{.data = std::shared_ptr{data}, - .size = static_cast(value_blob.len)}); - break; - } - - case LIBSQL_NULL: - // intentional fall-through - default: - row_vector.emplace_back(nullptr); - break; - } - - if (status != 0) { - fprintf(stderr, "%s\n", err); - throw std::runtime_error("libsql error"); - } - } - - if (results != nullptr) { - results->push_back(row_vector); - } - - err = nullptr; - } + int num_cols = libsql_column_count(rows); + std::vector column_names; + column_names.reserve(num_cols); + for (int col = 0; col < num_cols; col++) { + const char *col_name; + status = libsql_column_name(rows, col, &col_name, &err); if (status != 0) { + fprintf(stderr, "%s\n", err); + throw std::runtime_error(err); + } + + column_names.emplace_back(col_name == nullptr ? "" : col_name); + } + + while ((status = libsql_next_row(rows, &row, &err)) == 0) { + + if (!err && !row) { + break; + } + + std::vector row_vector; + + for (int col = 0; col < num_cols; col++) { + int type; + + libsql_column_type(rows, row, col, &type, &err); + + switch (type) { + case LIBSQL_INT: + long long int_value; + status = libsql_get_int(row, col, &int_value, &err); + row_vector.emplace_back(int_value); + break; + + case LIBSQL_FLOAT: + double float_value; + status = libsql_get_float(row, col, &float_value, &err); + row_vector.emplace_back(float_value); + break; + + case LIBSQL_TEXT: + const char *text_value; + status = libsql_get_string(row, col, &text_value, &err); + row_vector.emplace_back(text_value); + break; + + case LIBSQL_BLOB: { + blob value_blob; + libsql_get_blob(row, col, &value_blob, &err); + auto *data = new uint8_t[value_blob.len]; + // You cannot share raw memory between native and JS + // always copy the data + memcpy(data, value_blob.ptr, value_blob.len); + libsql_free_blob(value_blob); + row_vector.emplace_back( + ArrayBuffer{.data = std::shared_ptr{data}, + .size = static_cast(value_blob.len)}); + break; + } + + case LIBSQL_NULL: + // intentional fall-through + default: + row_vector.emplace_back(nullptr); + break; + } + + if (status != 0) { fprintf(stderr, "%s\n", err); + throw std::runtime_error("libsql error"); + } + } + + if (results != nullptr) { + results->push_back(row_vector); } - libsql_free_rows(rows); - libsql_free_stmt(stmt); + err = nullptr; + } - unsigned long long changes = libsql_changes(db.c); - long long insert_row_id = libsql_last_insert_rowid(db.c); + if (status != 0) { + fprintf(stderr, "%s\n", err); + } - return {.affectedRows = static_cast(changes), - .insertId = static_cast(insert_row_id)}; + libsql_free_rows(rows); + libsql_free_stmt(stmt); + + unsigned long long changes = libsql_changes(db.c); + long long insert_row_id = libsql_last_insert_rowid(db.c); + + return {.affectedRows = static_cast(changes), + .insertId = static_cast(insert_row_id), + .column_names = std::move(column_names)}; } BatchResult opsqlite_libsql_execute_batch(DB const &db, const std::vector *commands) { - size_t commandCount = commands->size(); - if (commandCount <= 0) { - throw std::runtime_error("No SQL commands provided"); - } - - try { - int affectedRows = 0; - // opsqlite_libsql_execute(db, "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_libsql_execute(db, command.sql, &command.params); - affectedRows += result.affectedRows; - } - // opsqlite_libsql_execute(db, "COMMIT", nullptr); - return BatchResult{ - .affectedRows = affectedRows, - .commands = static_cast(commandCount), - }; - } catch (std::exception &exc) { - // opsqlite_libsql_execute(db, "ROLLBACK", nullptr); - return BatchResult{ - .message = exc.what(), - }; - } + size_t commandCount = commands->size(); + if (commandCount <= 0) { + throw std::runtime_error("No SQL commands provided"); + } + + try { + int affectedRows = 0; + // opsqlite_libsql_execute(db, "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_libsql_execute(db, command.sql, &command.params); + affectedRows += result.affectedRows; + } + // opsqlite_libsql_execute(db, "COMMIT", nullptr); + return BatchResult{ + .affectedRows = affectedRows, + .commands = static_cast(commandCount), + }; + } catch (std::exception &exc) { + // opsqlite_libsql_execute(db, "ROLLBACK", nullptr); + return BatchResult{ + .message = exc.what(), + }; + } } } // namespace opsqlite diff --git a/cpp/turso_bridge.cpp b/cpp/turso_bridge.cpp index 43e410cc..dbd3f15b 100644 --- a/cpp/turso_bridge.cpp +++ b/cpp/turso_bridge.cpp @@ -1,7 +1,7 @@ -#include "bridge.h" #include "DBHostObject.h" #include "DumbHostObject.h" #include "SmartHostObject.h" +#include "bridge.h" #include "utils.hpp" #ifdef __APPLE__ @@ -50,8 +50,8 @@ inline TursoStmtHandle *to_turso_stmt(sqlite3_stmt *statement) { return reinterpret_cast(statement); } -inline const turso_connection_t *require_turso_connection( - TursoDbHandle *handle, const std::string &context) { +inline const turso_connection_t * +require_turso_connection(TursoDbHandle *handle, const std::string &context) { if (handle == nullptr || handle->connection == nullptr) { throw std::runtime_error("[op-sqlite][turso] " + context + ": invalid database connection"); @@ -66,9 +66,8 @@ void throw_if_turso_error(turso_status_code_t code, const char *error, return; } - throw std::runtime_error( - "[op-sqlite][turso] " + context + - (error != nullptr ? ": " + std::string(error) : "")); + throw std::runtime_error("[op-sqlite][turso] " + context + + (error != nullptr ? ": " + std::string(error) : "")); } std::vector read_binary_file(const std::string &path) { @@ -89,8 +88,7 @@ std::vector read_binary_file(const std::string &path) { return content; } -void write_binary_file_atomic(const std::string &path, - const char *data, +void write_binary_file_atomic(const std::string &path, const char *data, size_t size) { std::filesystem::path target(path); std::filesystem::create_directories(target.parent_path()); @@ -100,8 +98,9 @@ void write_binary_file_atomic(const std::string &path, { std::ofstream file(temp, std::ios::binary | std::ios::trunc); if (!file.is_open()) { - throw std::runtime_error("[op-sqlite][turso] failed to open temp file for write: " + - temp.string()); + throw std::runtime_error( + "[op-sqlite][turso] failed to open temp file for write: " + + temp.string()); } if (size > 0) { @@ -123,8 +122,8 @@ void write_binary_file_atomic(const std::string &path, } if (ec) { - throw std::runtime_error("[op-sqlite][turso] failed to atomically replace file: " + - path); + throw std::runtime_error( + "[op-sqlite][turso] failed to atomically replace file: " + path); } } @@ -154,7 +153,8 @@ void setup_turso_temp_dir(const std::string &db_path) { return; } - // Keep temp files in app-writable storage instead of restricted emulator temp dirs. + // Keep temp files in app-writable storage instead of restricted emulator temp + // dirs. setenv("TMPDIR", temp_dir.c_str(), 1); setenv("SQLITE_TMPDIR", temp_dir.c_str(), 1); setenv("TMP", temp_dir.c_str(), 1); @@ -165,8 +165,8 @@ void process_sync_io_item(const turso_sync_io_item_t *item) { const auto kind = turso_sync_database_io_request_kind(item); if (kind == TURSO_SYNC_IO_HTTP) { - std::string message = - "[op-sqlite][turso] sync HTTP IO request is not supported by native op-sqlite Turso bridge yet"; + std::string message = "[op-sqlite][turso] sync HTTP IO request is not " + "supported by native op-sqlite Turso bridge yet"; turso_slice_ref_t error = {.ptr = message.c_str(), .len = message.size()}; turso_sync_database_io_poison(item, &error); return; @@ -181,13 +181,15 @@ void process_sync_io_item(const turso_sync_io_item_t *item) { return; } - std::string path(static_cast(request.path.ptr), request.path.len); + std::string path(static_cast(request.path.ptr), + request.path.len); auto buffer = read_binary_file(path); if (!buffer.empty()) { turso_slice_ref_t slice = {.ptr = buffer.data(), .len = buffer.size()}; if (turso_sync_database_io_push_buffer(item, &slice) != TURSO_OK) { std::string message = "failed to push FULL_READ data"; - turso_slice_ref_t error = {.ptr = message.c_str(), .len = message.size()}; + turso_slice_ref_t error = {.ptr = message.c_str(), + .len = message.size()}; turso_sync_database_io_poison(item, &error); return; } @@ -206,7 +208,8 @@ void process_sync_io_item(const turso_sync_io_item_t *item) { return; } - std::string path(static_cast(request.path.ptr), request.path.len); + std::string path(static_cast(request.path.ptr), + request.path.len); try { write_binary_file_atomic(path, @@ -270,8 +273,8 @@ void bind_value(turso_statement_t *statement, size_t position, turso_status_code_t code = TURSO_OK; if (std::holds_alternative(value)) { - code = turso_statement_bind_positional_int( - statement, position, std::get(value) ? 1 : 0); + code = turso_statement_bind_positional_int(statement, position, + std::get(value) ? 1 : 0); } else if (std::holds_alternative(value)) { code = turso_statement_bind_positional_int(statement, position, std::get(value)); @@ -375,7 +378,7 @@ sqlite3 *opsqlite_open(std::string const &name, std::string const &path, turso_database_config_t db_config = { .async_io = 0, - .path = handle->path.c_str(), + .path = handle->path.c_str(), .experimental_features = nullptr, .vfs = nullptr, .encryption_cipher = nullptr, @@ -386,16 +389,14 @@ sqlite3 *opsqlite_open(std::string const &name, std::string const &path, const turso_database_t *database = nullptr; try { - throw_if_turso_error( - turso_database_new(&db_config, &database, &error), error, - "create database at " + handle->path); + throw_if_turso_error(turso_database_new(&db_config, &database, &error), + error, "create database at " + handle->path); throw_if_turso_error(turso_database_open(database, &error), error, "open database at " + handle->path); turso_connection_t *connection = nullptr; - throw_if_turso_error( - turso_database_connect(database, &connection, &error), error, - "connect database at " + handle->path); + throw_if_turso_error(turso_database_connect(database, &connection, &error), + error, "connect database at " + handle->path); handle->database = database; handle->connection = connection; @@ -445,8 +446,9 @@ sqlite3 *opsqlite_open_sync(std::string const &name, std::string const &path, .partial_bootstrap_strategy_query = nullptr, .partial_bootstrap_segment_size = 0, .partial_bootstrap_prefetch = false, - .remote_encryption_key = - remote_encryption_key.empty() ? nullptr : remote_encryption_key.c_str(), + .remote_encryption_key = remote_encryption_key.empty() + ? nullptr + : remote_encryption_key.c_str(), .remote_encryption_cipher = nullptr, }; @@ -454,14 +456,14 @@ sqlite3 *opsqlite_open_sync(std::string const &name, std::string const &path, const turso_sync_database_t *sync_database = nullptr; try { - throw_if_turso_error( - turso_sync_database_new(&db_config, &sync_config, &sync_database, &error), - error, "create sync database at " + handle->path); + throw_if_turso_error(turso_sync_database_new(&db_config, &sync_config, + &sync_database, &error), + error, "create sync database at " + handle->path); const turso_sync_operation_t *open_operation = nullptr; throw_if_turso_error( - turso_sync_database_create(sync_database, &open_operation, &error), error, - "open/create sync database at " + handle->path); + turso_sync_database_create(sync_database, &open_operation, &error), + error, "open/create sync database at " + handle->path); run_sync_operation(sync_database, open_operation); turso_sync_operation_deinit(open_operation); @@ -474,7 +476,8 @@ sqlite3 *opsqlite_open_sync(std::string const &name, std::string const &path, if (turso_sync_operation_result_kind(connect_operation) != TURSO_ASYNC_RESULT_CONNECTION) { turso_sync_operation_deinit(connect_operation); - throw std::runtime_error("[op-sqlite][turso] sync connect did not return a connection"); + throw std::runtime_error( + "[op-sqlite][turso] sync connect did not return a connection"); } const turso_connection_t *connection = nullptr; @@ -507,9 +510,9 @@ sqlite3 *opsqlite_open_sync(std::string const &name, std::string const &path, sqlite3 *opsqlite_open_remote(std::string const &url, std::string const &auth_token, std::string const &base_path) { - std::string remote_name = - "turso_remote_" + std::to_string(std::hash{}(url)) + - ".sqlite"; + std::string remote_name = "turso_remote_" + + std::to_string(std::hash{}(url)) + + ".sqlite"; return opsqlite_open_sync(remote_name, base_path, url, auth_token, ""); } @@ -540,24 +543,23 @@ void opsqlite_close(sqlite3 *db) { void opsqlite_sync(sqlite3 *db) { auto *handle = to_turso_db(db); if (handle == nullptr || handle->sync_database == nullptr) { - throw std::runtime_error("[op-sqlite][turso] sync is only available for sync/remote databases"); + throw std::runtime_error( + "[op-sqlite][turso] sync is only available for sync/remote databases"); } const char *error = nullptr; const turso_sync_operation_t *push_operation = nullptr; - throw_if_turso_error( - turso_sync_database_push_changes(handle->sync_database, &push_operation, - &error), - error, "push sync changes"); + throw_if_turso_error(turso_sync_database_push_changes( + handle->sync_database, &push_operation, &error), + error, "push sync changes"); run_sync_operation(handle->sync_database, push_operation); turso_sync_operation_deinit(push_operation); const turso_sync_operation_t *wait_operation = nullptr; - throw_if_turso_error( - turso_sync_database_wait_changes(handle->sync_database, &wait_operation, - &error), - error, "wait sync changes"); + throw_if_turso_error(turso_sync_database_wait_changes( + handle->sync_database, &wait_operation, &error), + error, "wait sync changes"); run_sync_operation(handle->sync_database, wait_operation); const turso_sync_changes_t *changes = nullptr; @@ -608,10 +610,11 @@ sqlite3_stmt *opsqlite_prepare_statement(sqlite3 *db, turso_statement_t *statement = nullptr; const char *error = nullptr; - throw_if_turso_error(turso_connection_prepare_single( - require_turso_connection(handle, "prepare statement"), - query.c_str(), &statement, &error), - error, "prepare statement"); + throw_if_turso_error( + turso_connection_prepare_single( + require_turso_connection(handle, "prepare statement"), query.c_str(), + &statement, &error), + error, "prepare statement"); auto *stmt_handle = new TursoStmtHandle(); stmt_handle->statement = statement; @@ -646,7 +649,8 @@ BridgeResult opsqlite_execute_prepared_statement( return; } - int col_count = static_cast(turso_statement_column_count(stmt->statement)); + int col_count = + static_cast(turso_statement_column_count(stmt->statement)); DumbHostObject row = DumbHostObject(metadatas); for (int i = 0; i < col_count; i++) { @@ -654,11 +658,12 @@ BridgeResult opsqlite_execute_prepared_statement( switch (kind) { case TURSO_TYPE_INTEGER: - row.values.emplace_back( - static_cast(turso_statement_row_value_int(stmt->statement, i))); + row.values.emplace_back(static_cast( + turso_statement_row_value_int(stmt->statement, i))); break; case TURSO_TYPE_REAL: - row.values.emplace_back(turso_statement_row_value_double(stmt->statement, i)); + row.values.emplace_back( + turso_statement_row_value_double(stmt->statement, i)); break; case TURSO_TYPE_TEXT: { auto size = turso_statement_row_value_bytes_count(stmt->statement, i); @@ -671,8 +676,9 @@ BridgeResult opsqlite_execute_prepared_statement( auto ptr = turso_statement_row_value_bytes_ptr(stmt->statement, i); auto *data = new uint8_t[static_cast(size)]; memcpy(data, ptr, static_cast(size)); - row.values.emplace_back(ArrayBuffer{.data = std::shared_ptr{data}, - .size = static_cast(size)}); + row.values.emplace_back( + ArrayBuffer{.data = std::shared_ptr{data}, + .size = static_cast(size)}); break; } case TURSO_TYPE_NULL: @@ -686,7 +692,8 @@ BridgeResult opsqlite_execute_prepared_statement( }); if (metadatas != nullptr && metadatas->empty()) { - int col_count = static_cast(turso_statement_column_count(stmt->statement)); + int col_count = + static_cast(turso_statement_column_count(stmt->statement)); for (int i = 0; i < col_count; i++) { auto metadata = SmartHostObject(); @@ -714,8 +721,8 @@ BridgeResult opsqlite_execute_prepared_statement( reset_statement(stmt->statement); return {.affectedRows = changes, - .insertId = static_cast(turso_connection_last_insert_rowid( - require_turso_connection(db_handle, "last_insert_rowid")))}; + .insertId = static_cast(turso_connection_last_insert_rowid( + require_turso_connection(db_handle, "last_insert_rowid")))}; } BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, @@ -732,8 +739,9 @@ BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, size_t tail = 0; auto code = turso_connection_prepare_first( - require_turso_connection(db_handle, "prepare statement in batch execute"), - query.c_str() + offset, &statement, &tail, &error); + require_turso_connection(db_handle, + "prepare statement in batch execute"), + query.c_str() + offset, &statement, &tail, &error); throw_if_turso_error(code, error, "prepare statement in batch execute"); if (tail == 0) { @@ -772,7 +780,8 @@ BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, auto kind = turso_statement_row_value_kind(statement, i); switch (kind) { case TURSO_TYPE_INTEGER: - row.emplace_back(static_cast(turso_statement_row_value_int(statement, i))); + row.emplace_back( + static_cast(turso_statement_row_value_int(statement, i))); break; case TURSO_TYPE_REAL: row.emplace_back(turso_statement_row_value_double(statement, i)); @@ -809,8 +818,8 @@ BridgeResult opsqlite_execute(sqlite3 *db, std::string const &query, } return {.affectedRows = changes, - .insertId = static_cast(turso_connection_last_insert_rowid( - require_turso_connection(db_handle, "last_insert_rowid"))), + .insertId = static_cast(turso_connection_last_insert_rowid( + require_turso_connection(db_handle, "last_insert_rowid"))), .rows = std::move(rows), .column_names = std::move(column_names)}; } @@ -825,14 +834,16 @@ BridgeResult opsqlite_execute_host_objects( opsqlite_bind_statement(statement, params); } - auto res = opsqlite_execute_prepared_statement(db, statement, results, metadatas); + auto res = + opsqlite_execute_prepared_statement(db, statement, results, metadatas); opsqlite_finalize_statement(statement); return res; } -BridgeResult opsqlite_execute_raw( - sqlite3 *db, std::string const &query, const std::vector *params, - std::vector> *results) { +BridgeResult +opsqlite_execute_raw(sqlite3 *db, std::string const &query, + const std::vector *params, + std::vector> *results) { auto response = opsqlite_execute(db, query, params); if (results != nullptr) { @@ -840,7 +851,8 @@ BridgeResult opsqlite_execute_raw( } return {.affectedRows = response.affectedRows, - .insertId = response.insertId}; + .insertId = response.insertId, + .column_names = std::move(response.column_names)}; } void opsqlite_register_update_hook([[maybe_unused]] sqlite3 *db, @@ -853,20 +865,21 @@ void opsqlite_register_commit_hook([[maybe_unused]] sqlite3 *db, void opsqlite_deregister_commit_hook([[maybe_unused]] sqlite3 *db) {} -void opsqlite_register_rollback_hook([[maybe_unused]] sqlite3 *db, - [[maybe_unused]] void *db_host_object_ptr) {} +void opsqlite_register_rollback_hook( + [[maybe_unused]] sqlite3 *db, [[maybe_unused]] void *db_host_object_ptr) {} void opsqlite_deregister_rollback_hook([[maybe_unused]] sqlite3 *db) {} void opsqlite_load_extension([[maybe_unused]] sqlite3 *db, [[maybe_unused]] std::string &path, [[maybe_unused]] std::string &entry_point) { - throw std::runtime_error( - "[op-sqlite][turso] load_extension is not supported by Turso SDK kit backend"); + throw std::runtime_error("[op-sqlite][turso] load_extension is not supported " + "by Turso SDK kit backend"); } -BatchResult opsqlite_execute_batch(sqlite3 *db, - const std::vector *commands) { +BatchResult +opsqlite_execute_batch(sqlite3 *db, + const std::vector *commands) { size_t command_count = commands->size(); if (command_count == 0) { throw std::runtime_error("No SQL commands provided"); diff --git a/cpp/utils.cpp b/cpp/utils.cpp index 086bd3e0..990dd9e9 100644 --- a/cpp/utils.cpp +++ b/cpp/utils.cpp @@ -101,9 +101,9 @@ inline JSVariant to_variant(jsi::Runtime &rt, const jsi::Value &value) { size_t byteLength = 0; uint8_t *sourceData = nullptr; jsi::Function arrayBufferCtor = - rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); + rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); jsi::Function isViewFn = - arrayBufferCtor.getPropertyAsFunction(rt, "isView"); + arrayBufferCtor.getPropertyAsFunction(rt, "isView"); bool isArrayBufferView = isViewFn.call(rt, obj).getBool(); if (obj.isArrayBuffer(rt)) { @@ -244,15 +244,29 @@ jsi::Value create_raw_result(jsi::Runtime &rt, const BridgeResult &status, const std::vector> *results) { size_t row_count = results->size(); - jsi::Array res = jsi::Array(rt, row_count); + jsi::Object res(rt); + jsi::Array raw_rows = jsi::Array(rt, row_count); for (int i = 0; i < row_count; i++) { auto row = results->at(i); auto array = jsi::Array(rt, row.size()); for (int j = 0; j < row.size(); j++) { array.setValueAtIndex(rt, j, to_jsi(rt, row[j])); } - res.setValueAtIndex(rt, i, array); + raw_rows.setValueAtIndex(rt, i, array); + } + + size_t column_count = status.column_names.size(); + jsi::Array column_names = jsi::Array(rt, column_count); + for (int i = 0; i < column_count; i++) { + column_names.setValueAtIndex( + rt, i, jsi::String::createFromUtf8(rt, status.column_names.at(i))); } + + res.setProperty(rt, "rowsAffected", status.affectedRows); + res.setProperty(rt, "insertId", status.insertId); + res.setProperty(rt, "rawRows", std::move(raw_rows)); + res.setProperty(rt, "columnNames", std::move(column_names)); + return res; } @@ -266,7 +280,8 @@ void to_batch_arguments(jsi::Runtime &rt, jsi::Array const &tuples, continue; } - const std::string query = tuple.getValueAtIndex(rt, 0).asString(rt).utf8(rt); + const std::string query = + tuple.getValueAtIndex(rt, 0).asString(rt).utf8(rt); if (length == 1) { commands->push_back({query}); continue; diff --git a/docs/docs/api.md b/docs/docs/api.md index cd01ecb0..f764f16f 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -171,15 +171,43 @@ let results2 = await statement.execute(); You only pay the price of parsing the query once, and each subsequent execution should be faster. -## Raw execution +## Execute Raw -If you don't care about the keys you can use a simplified execution that will return an array of scalars. This should be a lot faster than the regular operation since objects with the same keys don’t need to be created. +If you don't care about object rows you can use a simplified execution that returns row values in arrays plus the corresponding `columnNames`. This avoids creating a JS object per row and is typically faster than `execute()` for large result sets. + +### Breaking change notice + +Starting on version `17.0.0`, `executeRaw()` and `executeRawSync()` return an object instead of a bare array of rows. + +Previous shape: + +```tsx +const rows = await db.executeRaw('SELECT * FROM Users;'); +// rows = [[123, 'Katie']] +``` + +Current shape: + +```tsx +const result = await db.executeRaw('SELECT * FROM Users;'); + +result.rawRows; +// raw data in array format: [[123, 'Katie']] + +result.columnNames; +// actual column names: ['id', 'name'] +``` + +`rawRows` contains the raw data in array format, and `columnNames` contains the actual names of the columns. This change also applies to `executeRawSync()`. ```tsx let result = await db.executeRaw('SELECT * FROM Users;'); -// result = [[123, 'Katie', ...]] +// result.rawRows = [[123, 'Katie', ...]] +// result.columnNames = ['id', 'name', ...] ``` +If you need the old bare-array behavior, read from `result.rawRows`. + ### Multiple Statements You can execute multiple statements in a single operation. The API however is not really thought for this use case and the results (and their metadata) will be mangled, so you can discard it. This is not supported in libsql, due to the library itself not supporting this use case. diff --git a/example/src/tests/queries.ts b/example/src/tests/queries.ts index 62332b87..040083d3 100644 --- a/example/src/tests/queries.ts +++ b/example/src/tests/queries.ts @@ -618,7 +618,7 @@ describe("Queries tests", () => { }); }); - it("Execute raw sync should return just an array of objects", async () => { + it("Execute raw sync should return raw rows and column names", async () => { const id = chance.integer(); const name = chance.name(); const age = chance.integer(); @@ -632,7 +632,8 @@ describe("Queries tests", () => { ]); const res = db.executeRawSync("SELECT id, name, age, networth FROM User"); - expect(res).toDeepEqual([[id, name, age, networth]]); + expect(res.rawRows).toDeepEqual([[id, name, age, networth]]); + expect(res.columnNames).toDeepEqual(["id", "name", "age", "networth"]); }); it("Transaction, rejects on callback error", async () => { @@ -816,7 +817,7 @@ describe("Queries tests", () => { expect(res.rows[0]!.myWeirdProp).toEqual("quack_changed"); }); - it("Execute raw should return just an array of objects", async () => { + it("Execute raw should return raw rows and column names", async () => { const id = chance.integer(); const name = chance.name(); const age = chance.integer(); @@ -829,7 +830,8 @@ describe("Queries tests", () => { ]); const res = await db.executeRaw("SELECT id, name, age, networth FROM User"); - expect(res).toDeepEqual([[id, name, age, networth]]); + expect(res.rawRows).toDeepEqual([[id, name, age, networth]]); + expect(res.columnNames).toDeepEqual(["id", "name", "age", "networth"]); }); it("Create fts5 virtual table", async () => { diff --git a/node/README.md b/node/README.md index 1d9e3826..5d11a19c 100644 --- a/node/README.md +++ b/node/README.md @@ -50,7 +50,7 @@ While the API surface is identical, there are some behavioral differences: - ✅ `open()` - Open database with name and location - ✅ `openV2()` - Open database with full path - ✅ `execute()` / `executeSync()` - Query execution -- ✅ `executeRaw()` / `executeRawSync()` - Raw array results +- ✅ `executeRaw()` / `executeRawSync()` - Raw row arrays plus column names - ✅ `executeBatch()` - Batch operations in transaction - ✅ `transaction()` - Transaction support - ✅ `prepareStatement()` - Prepared statements diff --git a/node/src/database.ts b/node/src/database.ts index 64f343dd..972f78d2 100644 --- a/node/src/database.ts +++ b/node/src/database.ts @@ -1,37 +1,40 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import Database from 'better-sqlite3'; +import * as fs from "node:fs"; +import * as path from "node:path"; +import Database from "better-sqlite3"; import type { - BatchQueryResult, - ColumnMetadata, - DB, - FileLoadResult, - PreparedStatement, - QueryResult, - Scalar, - SQLBatchTuple, - Transaction, - UpdateHookOperation, -} from './types'; + BatchQueryResult, + ColumnMetadata, + DB, + FileLoadResult, + PreparedStatement, + QueryResult, + RawQueryResult, + Scalar, + SQLBatchTuple, + Transaction, + UpdateHookOperation, +} from "./types"; export class NodeDatabase implements DB { private db: Database.Database; private dbPath: string; - private updateHookCallback?: ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) | null; + private updateHookCallback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null; private commitHookCallback?: (() => void) | null; private rollbackHookCallback?: (() => void) | null; constructor(name: string, location?: string) { - this.dbPath = ':memory:' - if(location !== ":memory:") { - const dbLocation = location || './'; + this.dbPath = ":memory:"; + if (location !== ":memory:") { + const dbLocation = location || "./"; this.dbPath = path.join(dbLocation, name); - + // Ensure directory exists const dir = path.dirname(this.dbPath); if (!fs.existsSync(dir)) { @@ -46,15 +49,15 @@ export class NodeDatabase implements DB { private setupHooks(): void { // Setup update hook if needed // if (this.db.function) { - // Note: better-sqlite3 doesn't have direct update hook support - // This is a limitation compared to the native implementation + // Note: better-sqlite3 doesn't have direct update hook support + // This is a limitation compared to the native implementation // } } private convertParams(params?: Scalar[]): any[] | undefined { if (!params) return undefined; - return params.map(param => { + return params.map((param) => { if (param instanceof ArrayBuffer) { return Buffer.from(param); } else if (ArrayBuffer.isView(param)) { @@ -65,10 +68,10 @@ export class NodeDatabase implements DB { } private convertRows(stmt: Database.Statement, rows: any[]): QueryResult { - const columnNames = stmt.columns().map(col => col.name); + const columnNames = stmt.columns().map((col) => col.name); const metadata: ColumnMetadata[] = stmt.columns().map((col, index) => ({ name: col.name, - type: col.type || 'UNKNOWN', + type: col.type || "UNKNOWN", index, })); @@ -86,17 +89,13 @@ export class NodeDatabase implements DB { const stmt = this.db.prepare(query); // Check if it's a SELECT query - const isSelect = query.trim().toUpperCase().startsWith('SELECT'); + const isSelect = query.trim().toUpperCase().startsWith("SELECT"); if (isSelect) { - const rows = convertedParams - ? stmt.all(...convertedParams) - : stmt.all(); + const rows = convertedParams ? stmt.all(...convertedParams) : stmt.all(); return this.convertRows(stmt, rows); } else { - const info = convertedParams - ? stmt.run(...convertedParams) - : stmt.run(); + const info = convertedParams ? stmt.run(...convertedParams) : stmt.run(); return { rowsAffected: info.changes, @@ -118,20 +117,23 @@ export class NodeDatabase implements DB { return this.execute(query, params); } - executeRawSync(query: string, params?: Scalar[]): any[] { + executeRawSync(query: string, params?: Scalar[]): RawQueryResult { try { const convertedParams = this.convertParams(params); const stmt = this.db.prepare(query); - const rows = convertedParams - ? stmt.raw().all(...convertedParams) - : stmt.raw().all(); - return rows; + const rawRows = convertedParams ? stmt.raw().all(...convertedParams) : stmt.raw().all(); + const columnNames = stmt.columns().map((col) => col.name); + return { + rowsAffected: 0, + rawRows: rawRows as Scalar[][], + columnNames, + }; } catch (error: any) { throw new Error(`SQL Error: ${error.message}`); } } - async executeRaw(query: string, params?: Scalar[]): Promise { + async executeRaw(query: string, params?: Scalar[]): Promise { return Promise.resolve(this.executeRawSync(query, params)); } @@ -166,11 +168,11 @@ export class NodeDatabase implements DB { } async loadFile(location: string): Promise { - const fileContent = fs.readFileSync(location, 'utf-8'); + const fileContent = fs.readFileSync(location, "utf-8"); const statements = fileContent - .split(';') - .map(s => s.trim()) - .filter(s => s.length > 0); + .split(";") + .map((s) => s.trim()) + .filter((s) => s.length > 0); let totalRowsAffected = 0; let commandCount = 0; @@ -200,23 +202,23 @@ export class NodeDatabase implements DB { return { rowsAffected: 0, rows: [] }; }, rollback: () => { - throw new Error('ROLLBACK'); + throw new Error("ROLLBACK"); }, }; // Manually control transaction with BEGIN/COMMIT/ROLLBACK to support async operations - this.executeSync('BEGIN TRANSACTION'); + this.executeSync("BEGIN TRANSACTION"); try { await fn(transaction); - this.executeSync('COMMIT'); + this.executeSync("COMMIT"); if (this.commitHookCallback) { this.commitHookCallback(); } } catch (error: any) { - this.executeSync('ROLLBACK'); + this.executeSync("ROLLBACK"); if (this.rollbackHookCallback) { this.rollbackHookCallback(); @@ -238,17 +240,13 @@ export class NodeDatabase implements DB { boundParams = this.convertParams(params) || []; }, execute: async () => { - const isSelect = query.trim().toUpperCase().startsWith('SELECT'); + const isSelect = query.trim().toUpperCase().startsWith("SELECT"); if (isSelect) { - const rows = boundParams.length > 0 - ? stmt.all(...boundParams) - : stmt.all(); + const rows = boundParams.length > 0 ? stmt.all(...boundParams) : stmt.all(); return this.convertRows(stmt, rows); } else { - const info = boundParams.length > 0 - ? stmt.run(...boundParams) - : stmt.run(); + const info = boundParams.length > 0 ? stmt.run(...boundParams) : stmt.run(); return { rowsAffected: info.changes, @@ -260,12 +258,8 @@ export class NodeDatabase implements DB { }; } - attach(params: { - secondaryDbFileName: string; - alias: string; - location?: string; - }): void { - const dbLocation = params.location || './'; + attach(params: { secondaryDbFileName: string; alias: string; location?: string }): void { + const dbLocation = params.location || "./"; const dbPath = path.join(dbLocation, params.secondaryDbFileName); this.db.prepare(`ATTACH DATABASE ? AS ?`).run(dbPath, params.alias); } @@ -282,11 +276,11 @@ export class NodeDatabase implements DB { row?: any; rowId: number; }) => void) - | null + | null, ): void { this.updateHookCallback = callback; // Note: better-sqlite3 doesn't support update hooks directly - console.warn('Update hooks are not fully supported in the Node.js implementation'); + console.warn("Update hooks are not fully supported in the Node.js implementation"); } commitHook(callback?: (() => void) | null): void { @@ -321,18 +315,18 @@ export class NodeDatabase implements DB { callback: (response: any) => void; }): () => void { // Reactive queries are not supported in Node.js implementation - console.warn('Reactive queries are not supported in the Node.js implementation'); + console.warn("Reactive queries are not supported in the Node.js implementation"); return () => {}; } sync(): void { // LibSQL sync is not supported in the Node.js implementation - throw new Error('sync() is only available with libsql'); + throw new Error("sync() is only available with libsql"); } setReservedBytes(reservedBytes: number): void { // SQLCipher specific, not supported in standard SQLite - console.warn('setReservedBytes is not supported in the Node.js implementation'); + console.warn("setReservedBytes is not supported in the Node.js implementation"); } getReservedBytes(): number { diff --git a/node/src/test.spec.ts b/node/src/test.spec.ts index c7e3128f..fa406d9a 100644 --- a/node/src/test.spec.ts +++ b/node/src/test.spec.ts @@ -4,223 +4,194 @@ import * as os from "node:os"; import { isIOSEmbedded, isLibsql, isSQLCipher, open } from "./index"; describe("op-sqlite Node.js tests", () => { - let db: ReturnType; - - beforeAll(() => { - db = open({ name: "test.sqlite", location: "./" }); - }); - - afterAll(() => { - db.close(); - - const cleanupDb = open({ name: "test.sqlite", location: "./" }); - cleanupDb.delete(); - - const cleanupDb2 = open({ name: "test2.sqlite", location: "./" }); - cleanupDb2.delete(); - }); - - test("Database opens successfully", () => { - const path = db.getDbPath(); - expect(path).toContain("test.sqlite"); - }); - - test("Create table", () => { - db.executeSync( - "CREATE TABLE IF NOT EXISTS test_users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)", - ); - }); - - test("Insert data", () => { - const result = db.executeSync( - "INSERT INTO test_users (name, age) VALUES (?, ?)", - ["Alice", 30], - ); - expect(result.rowsAffected).toBe(1); - expect(result.insertId).toBeDefined(); - }); - - test("Query data", async () => { - const result = await db.execute("SELECT * FROM test_users WHERE name = ?", [ - "Alice", - ]); - expect(result.rows.length).toBe(1); - expect(result.rows[0].name).toBe("Alice"); - expect(result.rows[0].age).toBe(30); - }); - - test("Query without parameters", () => { - const result = db.executeSync("SELECT COUNT(*) as count FROM test_users"); - const row: number = result.rows.length; - expect(row).toBeGreaterThanOrEqual(1); - }); - - test("Update data", () => { - const result = db.executeSync( - "UPDATE test_users SET age = ? WHERE name = ?", - [31, "Alice"], - ); - expect(result.rowsAffected).toBe(1); - }); - - test("Verify update", () => { - const result = db.executeSync("SELECT age FROM test_users WHERE name = ?", [ - "Alice", - ]); - expect(result.rows[0].age).toBe(31); - }); - - test("Execute raw query", async () => { - const result = await db.executeRaw( - "SELECT name, age FROM test_users WHERE name = ?", - ["Alice"], - ); - expect(Array.isArray(result)).toBe(true); - expect(Array.isArray(result[0])).toBe(true); - expect(result[0][0]).toBe("Alice"); - expect(result[0][1]).toBe(31); - }); - - test("Execute batch", async () => { - const result = await db.executeBatch([ - ["INSERT INTO test_users (name, age) VALUES (?, ?)", ["Bob", 25]], - ["INSERT INTO test_users (name, age) VALUES (?, ?)", ["Charlie", 35]], - ]); - expect(result.rowsAffected ?? 0).toBeGreaterThanOrEqual(2); - }); - - test("Transaction commit", async () => { - await db.transaction(async (tx) => { - await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", [ - "David", - 40, - ]); - await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", [ - "Emma", - 28, - ]); - }); - - const result = db.executeSync("SELECT COUNT(*) as count FROM test_users"); - expect(result.rows[0].count).toBeGreaterThanOrEqual(5); - }); - - test("Transaction rollback", async () => { - const beforeCount = db.executeSync( - "SELECT COUNT(*) as count FROM test_users", - ).rows[0].count; - - let caught: Error | undefined; - try { - await db.transaction(async (tx) => { - await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", [ - "Temporary", - 99, - ]); - throw new Error("Rollback test"); - }); - } catch (error: any) { - caught = error; - } - - expect(caught).toBeDefined(); - expect(caught?.message).toBe("Rollback test"); - - const afterCount = db.executeSync( - "SELECT COUNT(*) as count FROM test_users", - ).rows[0].count; - expect(beforeCount).toBe(afterCount); - - const tempRow = db.executeSync( - "SELECT * FROM test_users WHERE name = ?", - ["Temporary"], - ); - expect(tempRow.rows.length).toBe(0); - }); - - test("Prepared statement", async () => { - const stmt = db.prepareStatement("SELECT * FROM test_users WHERE age > ?"); - - stmt.bindSync([30]); - const result1 = await stmt.execute(); - expect(result1.rows.length).toBeGreaterThanOrEqual(2); - - stmt.bindSync([25]); - const result2 = await stmt.execute(); - expect(result2.rows.length).toBeGreaterThanOrEqual(result1.rows.length); - }); - - test("Query metadata", () => { - const result = db.executeSync("SELECT * FROM test_users LIMIT 1"); - expect(result.metadata).toBeDefined(); - expect(result.metadata!.length).toBeGreaterThanOrEqual(3); - expect(result.columnNames).toBeDefined(); - expect(result.columnNames).toContain("name"); - }); - - test("Delete data", () => { - const result = db.executeSync("DELETE FROM test_users WHERE name = ?", [ - "Bob", - ]); - expect(result.rowsAffected).toBe(1); - }); - - test("Attach database", () => { - const db2 = open({ name: "test2.sqlite", location: "./" }); - db2.executeSync( - "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT)", - ); - db2.executeSync("INSERT INTO products (name) VALUES (?)", ["Laptop"]); - db2.close(); - - db.attach({ - secondaryDbFileName: "test2.sqlite", - alias: "secondary", - location: "./", - }); - const result = db.executeSync("SELECT * FROM secondary.products"); - expect(result.rows.length).toBe(1); - expect(result.rows[0].name).toBe("Laptop"); - }); - - test("Detach database", () => { - db.detach("secondary"); - expect(() => { - db.executeSync("SELECT * FROM secondary.products"); - }).toThrow(/no such table|secondary/); - }); - - test("Load SQL file", async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opsqlite-loadfile-")); - const sqlFilePath = path.join(tempDir, "seed.sql"); - fs.writeFileSync( - sqlFilePath, - [ - "CREATE TABLE loadfile_products (id INTEGER PRIMARY KEY, name TEXT)", - "INSERT INTO loadfile_products (name) VALUES ('Widget')", - "INSERT INTO loadfile_products (name) VALUES ('Gadget')", - ].join(";\n") + ";\n", - ); - - try { - const loadResult = await db.loadFile(sqlFilePath); - expect(loadResult.commands).toBe(3); - - const result = db.executeSync( - "SELECT name FROM loadfile_products ORDER BY id", - ); - expect(result.rows.length).toBe(2); - expect(result.rows[0].name).toBe("Widget"); - expect(result.rows[1].name).toBe("Gadget"); - } finally { - db.executeSync("DROP TABLE IF EXISTS loadfile_products"); - fs.rmSync(tempDir, { recursive: true, force: true }); - } - }); - - test("Feature checks", () => { - expect(isSQLCipher()).toBe(false); - expect(isLibsql()).toBe(false); - expect(isIOSEmbedded()).toBe(false); - }); + let db: ReturnType; + + beforeAll(() => { + db = open({ name: "test.sqlite", location: "./" }); + }); + + afterAll(() => { + db.close(); + + const cleanupDb = open({ name: "test.sqlite", location: "./" }); + cleanupDb.delete(); + + const cleanupDb2 = open({ name: "test2.sqlite", location: "./" }); + cleanupDb2.delete(); + }); + + test("Database opens successfully", () => { + const path = db.getDbPath(); + expect(path).toContain("test.sqlite"); + }); + + test("Create table", () => { + db.executeSync( + "CREATE TABLE IF NOT EXISTS test_users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)", + ); + }); + + test("Insert data", () => { + const result = db.executeSync("INSERT INTO test_users (name, age) VALUES (?, ?)", [ + "Alice", + 30, + ]); + expect(result.rowsAffected).toBe(1); + expect(result.insertId).toBeDefined(); + }); + + test("Query data", async () => { + const result = await db.execute("SELECT * FROM test_users WHERE name = ?", ["Alice"]); + expect(result.rows.length).toBe(1); + expect(result.rows[0].name).toBe("Alice"); + expect(result.rows[0].age).toBe(30); + }); + + test("Query without parameters", () => { + const result = db.executeSync("SELECT COUNT(*) as count FROM test_users"); + const row: number = result.rows.length; + expect(row).toBeGreaterThanOrEqual(1); + }); + + test("Update data", () => { + const result = db.executeSync("UPDATE test_users SET age = ? WHERE name = ?", [31, "Alice"]); + expect(result.rowsAffected).toBe(1); + }); + + test("Verify update", () => { + const result = db.executeSync("SELECT age FROM test_users WHERE name = ?", ["Alice"]); + expect(result.rows[0].age).toBe(31); + }); + + test("Execute raw query", async () => { + const result = await db.executeRaw("SELECT name, age FROM test_users WHERE name = ?", [ + "Alice", + ]); + expect(Array.isArray(result.rawRows)).toBe(true); + expect(Array.isArray(result.rawRows[0])).toBe(true); + expect(result.rawRows[0][0]).toBe("Alice"); + expect(result.rawRows[0][1]).toBe(31); + expect(result.columnNames).toEqual(["name", "age"]); + }); + + test("Execute batch", async () => { + const result = await db.executeBatch([ + ["INSERT INTO test_users (name, age) VALUES (?, ?)", ["Bob", 25]], + ["INSERT INTO test_users (name, age) VALUES (?, ?)", ["Charlie", 35]], + ]); + expect(result.rowsAffected ?? 0).toBeGreaterThanOrEqual(2); + }); + + test("Transaction commit", async () => { + await db.transaction(async (tx) => { + await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", ["David", 40]); + await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", ["Emma", 28]); + }); + + const result = db.executeSync("SELECT COUNT(*) as count FROM test_users"); + expect(result.rows[0].count).toBeGreaterThanOrEqual(5); + }); + + test("Transaction rollback", async () => { + const beforeCount = db.executeSync("SELECT COUNT(*) as count FROM test_users").rows[0].count; + + let caught: Error | undefined; + try { + await db.transaction(async (tx) => { + await tx.execute("INSERT INTO test_users (name, age) VALUES (?, ?)", ["Temporary", 99]); + throw new Error("Rollback test"); + }); + } catch (error: any) { + caught = error; + } + + expect(caught).toBeDefined(); + expect(caught?.message).toBe("Rollback test"); + + const afterCount = db.executeSync("SELECT COUNT(*) as count FROM test_users").rows[0].count; + expect(beforeCount).toBe(afterCount); + + const tempRow = db.executeSync("SELECT * FROM test_users WHERE name = ?", ["Temporary"]); + expect(tempRow.rows.length).toBe(0); + }); + + test("Prepared statement", async () => { + const stmt = db.prepareStatement("SELECT * FROM test_users WHERE age > ?"); + + stmt.bindSync([30]); + const result1 = await stmt.execute(); + expect(result1.rows.length).toBeGreaterThanOrEqual(2); + + stmt.bindSync([25]); + const result2 = await stmt.execute(); + expect(result2.rows.length).toBeGreaterThanOrEqual(result1.rows.length); + }); + + test("Query metadata", () => { + const result = db.executeSync("SELECT * FROM test_users LIMIT 1"); + expect(result.metadata).toBeDefined(); + expect(result.metadata!.length).toBeGreaterThanOrEqual(3); + expect(result.columnNames).toBeDefined(); + expect(result.columnNames).toContain("name"); + }); + + test("Delete data", () => { + const result = db.executeSync("DELETE FROM test_users WHERE name = ?", ["Bob"]); + expect(result.rowsAffected).toBe(1); + }); + + test("Attach database", () => { + const db2 = open({ name: "test2.sqlite", location: "./" }); + db2.executeSync("CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT)"); + db2.executeSync("INSERT INTO products (name) VALUES (?)", ["Laptop"]); + db2.close(); + + db.attach({ + secondaryDbFileName: "test2.sqlite", + alias: "secondary", + location: "./", + }); + const result = db.executeSync("SELECT * FROM secondary.products"); + expect(result.rows.length).toBe(1); + expect(result.rows[0].name).toBe("Laptop"); + }); + + test("Detach database", () => { + db.detach("secondary"); + expect(() => { + db.executeSync("SELECT * FROM secondary.products"); + }).toThrow(/no such table|secondary/); + }); + + test("Load SQL file", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "opsqlite-loadfile-")); + const sqlFilePath = path.join(tempDir, "seed.sql"); + fs.writeFileSync( + sqlFilePath, + [ + "CREATE TABLE loadfile_products (id INTEGER PRIMARY KEY, name TEXT)", + "INSERT INTO loadfile_products (name) VALUES ('Widget')", + "INSERT INTO loadfile_products (name) VALUES ('Gadget')", + ].join(";\n") + ";\n", + ); + + try { + const loadResult = await db.loadFile(sqlFilePath); + expect(loadResult.commands).toBe(3); + + const result = db.executeSync("SELECT name FROM loadfile_products ORDER BY id"); + expect(result.rows.length).toBe(2); + expect(result.rows[0].name).toBe("Widget"); + expect(result.rows[1].name).toBe("Gadget"); + } finally { + db.executeSync("DROP TABLE IF EXISTS loadfile_products"); + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test("Feature checks", () => { + expect(isSQLCipher()).toBe(false); + expect(isLibsql()).toBe(false); + expect(isIOSEmbedded()).toBe(false); + }); }); diff --git a/node/src/types.ts b/node/src/types.ts index a4975c9c..536fea98 100644 --- a/node/src/types.ts +++ b/node/src/types.ts @@ -1,124 +1,111 @@ // Re-export all types from the main package -export type Scalar = - | string - | number - | boolean - | null - | ArrayBuffer - | ArrayBufferView; +export type Scalar = string | number | boolean | null | ArrayBuffer | ArrayBufferView; export type QueryResult = { - insertId?: number; - rowsAffected: number; - res?: any[]; - rows: Array>; - rawRows?: Scalar[][]; - columnNames?: string[]; - metadata?: ColumnMetadata[]; + insertId?: number; + rowsAffected: number; + res?: any[]; + rows: Array>; + rawRows?: Scalar[][]; + columnNames?: string[]; + metadata?: ColumnMetadata[]; +}; + +export type RawQueryResult = { + insertId?: number; + rowsAffected: number; + rawRows: Scalar[][]; + columnNames: string[]; }; export type ColumnMetadata = { - name: string; - type: string; - index: number; + name: string; + type: string; + index: number; }; -export type SQLBatchTuple = - | [string] - | [string, Scalar[]] - | [string, Scalar[][]]; +export type SQLBatchTuple = [string] | [string, Scalar[]] | [string, Scalar[][]]; export type UpdateHookOperation = "INSERT" | "DELETE" | "UPDATE"; export type BatchQueryResult = { - rowsAffected?: number; + rowsAffected?: number; }; export type FileLoadResult = BatchQueryResult & { - commands?: number; + commands?: number; }; export type Transaction = { - commit: () => Promise; - execute: (query: string, params?: Scalar[]) => Promise; - rollback: () => QueryResult; + commit: () => Promise; + execute: (query: string, params?: Scalar[]) => Promise; + rollback: () => QueryResult; }; export type PreparedStatement = { - bind: (params: any[]) => Promise; - bindSync: (params: any[]) => void; - execute: () => Promise; + bind: (params: any[]) => Promise; + bindSync: (params: any[]) => void; + execute: () => Promise; }; export type DB = { - close: () => void; - delete: () => void; - attach: (params: { - secondaryDbFileName: string; - alias: string; - location?: string; - }) => void; - detach: (alias: string) => void; - transaction: (fn: (tx: Transaction) => Promise) => Promise; - executeSync: (query: string, params?: Scalar[]) => QueryResult; - execute: (query: string, params?: Scalar[]) => Promise; - executeWithHostObjects: ( - query: string, - params?: Scalar[], - ) => Promise; - executeBatch: (commands: SQLBatchTuple[]) => Promise; - loadFile: (location: string) => Promise; - updateHook: ( - callback?: - | ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) - | null, - ) => void; - commitHook: (callback?: (() => void) | null) => void; - rollbackHook: (callback?: (() => void) | null) => void; - prepareStatement: (query: string) => PreparedStatement; - loadExtension: (path: string, entryPoint?: string) => void; - executeRaw: (query: string, params?: Scalar[]) => Promise; - executeRawSync: (query: string, params?: Scalar[]) => any[]; - getDbPath: (location?: string) => string; - reactiveExecute: (params: { - query: string; - arguments: any[]; - fireOn: { - table: string; - ids?: number[]; - }[]; - callback: (response: any) => void; - }) => () => void; - sync: () => void; - setReservedBytes: (reservedBytes: number) => void; - getReservedBytes: () => number; - flushPendingReactiveQueries: () => Promise; + close: () => void; + delete: () => void; + attach: (params: { secondaryDbFileName: string; alias: string; location?: string }) => void; + detach: (alias: string) => void; + transaction: (fn: (tx: Transaction) => Promise) => Promise; + executeSync: (query: string, params?: Scalar[]) => QueryResult; + execute: (query: string, params?: Scalar[]) => Promise; + executeWithHostObjects: (query: string, params?: Scalar[]) => Promise; + executeBatch: (commands: SQLBatchTuple[]) => Promise; + loadFile: (location: string) => Promise; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null, + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + prepareStatement: (query: string) => PreparedStatement; + loadExtension: (path: string, entryPoint?: string) => void; + executeRaw: (query: string, params?: Scalar[]) => Promise; + executeRawSync: (query: string, params?: Scalar[]) => RawQueryResult; + getDbPath: (location?: string) => string; + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + sync: () => void; + setReservedBytes: (reservedBytes: number) => void; + getReservedBytes: () => number; + flushPendingReactiveQueries: () => Promise; }; export type DBParams = { - url?: string; - authToken?: string; - name?: string; - location?: string; - syncInterval?: number; + url?: string; + authToken?: string; + name?: string; + location?: string; + syncInterval?: number; }; export type OPSQLiteProxy = { - open: (options: { - name: string; - location?: string; - encryptionKey?: string; - }) => DB; - openV2: (options: { path: string; encryptionKey?: string }) => DB; - openRemote: (options: { url: string; authToken: string }) => DB; - openSync: (options: DBParams) => DB; - isSQLCipher: () => boolean; - isLibsql: () => boolean; - isTurso: () => boolean; - isIOSEmbedded: () => boolean; + open: (options: { name: string; location?: string; encryptionKey?: string }) => DB; + openV2: (options: { path: string; encryptionKey?: string }) => DB; + openRemote: (options: { url: string; authToken: string }) => DB; + openSync: (options: DBParams) => DB; + isSQLCipher: () => boolean; + isLibsql: () => boolean; + isTurso: () => boolean; + isIOSEmbedded: () => boolean; }; diff --git a/src/functions.web.ts b/src/functions.web.ts index 2bd71608..90a648ef 100644 --- a/src/functions.web.ts +++ b/src/functions.web.ts @@ -1,445 +1,443 @@ import type { - _InternalDB, - _PendingTransaction, - BatchQueryResult, - DB, - DBParams, - FileLoadResult, - OPSQLiteProxy, - PreparedStatement, - QueryResult, - Scalar, - SQLBatchTuple, - Transaction, + _InternalDB, + _PendingTransaction, + BatchQueryResult, + DB, + DBParams, + FileLoadResult, + OPSQLiteProxy, + PreparedStatement, + QueryResult, + RawQueryResult, + Scalar, + SQLBatchTuple, + Transaction, } from "./types"; -type WorkerPromiser = ( - type: string, - args?: Record, -) => Promise; +type WorkerPromiser = (type: string, args?: Record) => Promise; const WEB_ONLY_SYNC_ERROR = - "[op-sqlite] Web backend is async-only. Use openAsync() and async methods like execute()."; + "[op-sqlite] Web backend is async-only. Use openAsync() and async methods like execute()."; function throwSyncApiError(method: string): never { - throw new Error(`${WEB_ONLY_SYNC_ERROR} Called sync method: ${method}().`); + throw new Error(`${WEB_ONLY_SYNC_ERROR} Called sync method: ${method}().`); } function toNumber(value: unknown): number | undefined { - if (value == null) { - return undefined; - } + if (value == null) { + return undefined; + } - if (typeof value === "bigint") { - const asNumber = Number(value); - return Number.isFinite(asNumber) ? asNumber : undefined; - } + if (typeof value === "bigint") { + const asNumber = Number(value); + return Number.isFinite(asNumber) ? asNumber : undefined; + } - if (typeof value === "number") { - return Number.isFinite(value) ? value : undefined; - } + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } - return undefined; + return undefined; } function ensureSingleStatement(sql: string): void { - // Web worker executes the full SQL string while native executes only the first prepared statement. - // We warn here so callers can keep behavior consistent across platforms when needed. - if (sql.includes(";")) { - const trimmed = sql.trim(); - if (!trimmed.endsWith(";") || trimmed.slice(0, -1).includes(";")) { - console.warn( - "[op-sqlite] Web execute() runs full SQL strings. Avoid multi-statement SQL for parity with native first-statement behavior.", - ); - } - } + // Web worker executes the full SQL string while native executes only the first prepared statement. + // We warn here so callers can keep behavior consistent across platforms when needed. + if (sql.includes(";")) { + const trimmed = sql.trim(); + if (!trimmed.endsWith(";") || trimmed.slice(0, -1).includes(";")) { + console.warn( + "[op-sqlite] Web execute() runs full SQL strings. Avoid multi-statement SQL for parity with native first-statement behavior.", + ); + } + } } let promiserPromise: Promise | null = null; async function getPromiser(): Promise { - if (!promiserPromise) { - promiserPromise = (async () => { - let mod: any; + if (!promiserPromise) { + promiserPromise = (async () => { + let mod: any; - try { - mod = await import("@sqlite.org/sqlite-wasm"); - } catch (error) { - throw new Error( - `[op-sqlite] Web support requires optional dependency @sqlite.org/sqlite-wasm. Install it in your app: npm i @sqlite.org/sqlite-wasm (or yarn add @sqlite.org/sqlite-wasm). Original error: ${(error as Error).message}`, - ); - } + try { + mod = await import("@sqlite.org/sqlite-wasm"); + } catch (error) { + throw new Error( + `[op-sqlite] Web support requires optional dependency @sqlite.org/sqlite-wasm. Install it in your app: npm i @sqlite.org/sqlite-wasm (or yarn add @sqlite.org/sqlite-wasm). Original error: ${(error as Error).message}`, + ); + } - const makePromiser = mod.sqlite3Worker1Promiser as any; + const makePromiser = mod.sqlite3Worker1Promiser as any; - const maybePromiser = makePromiser(); + const maybePromiser = makePromiser(); - if (typeof maybePromiser === "function") { - return maybePromiser as WorkerPromiser; - } + if (typeof maybePromiser === "function") { + return maybePromiser as WorkerPromiser; + } - return (await maybePromiser) as WorkerPromiser; - })(); - } + return (await maybePromiser) as WorkerPromiser; + })(); + } - return promiserPromise; + return promiserPromise; } async function ensureOpfs(promiser: WorkerPromiser): Promise { - const config = await promiser("config-get", {}); - const vfsList = config?.result?.vfsList; - - if (!Array.isArray(vfsList) || !vfsList.includes("opfs")) { - throw new Error( - "[op-sqlite] OPFS is required on web for persistence. Ensure COOP/COEP headers are set and OPFS is available in this browser.", - ); - } + const config = await promiser("config-get", {}); + const vfsList = config?.result?.vfsList; + + if (!Array.isArray(vfsList) || !vfsList.includes("opfs")) { + throw new Error( + "[op-sqlite] OPFS is required on web for persistence. Ensure COOP/COEP headers are set and OPFS is available in this browser.", + ); + } } async function executeWorker( - promiser: WorkerPromiser, - dbId: string, - query: string, - params?: Scalar[], + promiser: WorkerPromiser, + dbId: string, + query: string, + params?: Scalar[], ): Promise { - ensureSingleStatement(query); - - const response = await promiser("exec", { - dbId, - sql: query, - bind: params, - rowMode: "object", - resultRows: [], - columnNames: [], - returnValue: "resultRows", - }); - - const result = response?.result; - const rows = Array.isArray(result?.resultRows) - ? (result.resultRows as Array>) - : Array.isArray(result) - ? (result as Array>) - : []; - const columnNames = Array.isArray(result?.columnNames) - ? (result.columnNames as string[]) - : rows.length > 0 - ? Object.keys(rows[0] ?? {}) - : []; - - const rowsAffected = toNumber(result?.changeCount) ?? 0; - const insertId = toNumber(result?.lastInsertRowId); - - return { - rowsAffected, - insertId, - rows, - columnNames, - }; + ensureSingleStatement(query); + + const response = await promiser("exec", { + dbId, + sql: query, + bind: params, + rowMode: "object", + resultRows: [], + columnNames: [], + returnValue: "resultRows", + }); + + const result = response?.result; + const rows = Array.isArray(result?.resultRows) + ? (result.resultRows as Array>) + : Array.isArray(result) + ? (result as Array>) + : []; + const columnNames = Array.isArray(result?.columnNames) + ? (result.columnNames as string[]) + : rows.length > 0 + ? Object.keys(rows[0] ?? {}) + : []; + + const rowsAffected = toNumber(result?.changeCount) ?? 0; + const insertId = toNumber(result?.lastInsertRowId); + + return { + rowsAffected, + insertId, + rows, + columnNames, + }; } -function enhanceWebDb( - db: _InternalDB, - options: { name?: string; location?: string }, -): DB { - const lock = { - queue: [] as _PendingTransaction[], - inProgress: false, - }; - - const startNextTransaction = () => { - if (lock.inProgress || lock.queue.length === 0) { - return; - } - - lock.inProgress = true; - const tx = lock.queue.shift(); - if (!tx) { - throw new Error("Could not get an operation on database"); - } - - setTimeout(() => { - tx.start(); - }, 0); - }; - - const withTransactionLock = async (work: () => Promise): Promise => { - return new Promise((resolve, reject) => { - const tx: _PendingTransaction = { - start: () => { - work() - .then(resolve) - .catch(reject) - .finally(() => { - lock.inProgress = false; - startNextTransaction(); - }); - }, - }; - - lock.queue.push(tx); - startNextTransaction(); - }); - }; - - const unsupported = (method: string) => () => throwSyncApiError(method); - - const enhancedDb: DB = { - close: unsupported("close"), - closeAsync: async () => { - await db.closeAsync?.(); - }, - interrupt: unsupported("interrupt"), - delete: unsupported("delete"), - attach: unsupported("attach"), - detach: unsupported("detach"), - transaction: async ( - fn: (tx: Transaction) => Promise, - ): Promise => { - return withTransactionLock(async () => { - let finalized = false; - - const commit = async (): Promise => { - if (finalized) { - throw new Error( - `OP-Sqlite Error: Database: ${options.name}. Cannot execute query on finalized transaction`, - ); - } - - const res = await enhancedDb.execute("COMMIT;"); - finalized = true; - return res; - }; - - const rollback = (): QueryResult => { - throwSyncApiError("rollback"); - }; - - const execute = async (query: string, params?: Scalar[]) => { - if (finalized) { - throw new Error( - `OP-Sqlite Error: Database: ${options.name}. Cannot execute query on finalized transaction`, - ); - } - - return enhancedDb.execute(query, params); - }; - - await enhancedDb.execute("BEGIN TRANSACTION;"); - - try { - await fn({ - execute, - commit, - rollback, - }); - - if (!finalized) { - await commit(); - } - } catch (error) { - if (!finalized) { - await enhancedDb.execute("ROLLBACK;"); - } - - throw error; - } - }); - }, - executeSync: unsupported("executeSync"), - execute: db.execute, - executeWithHostObjects: db.execute, - executeBatch: async ( - commands: SQLBatchTuple[], - ): Promise => { - await withTransactionLock(async () => { - await db.execute("BEGIN TRANSACTION;"); - - try { - for (const command of commands) { - const [sql, bind] = command; - - if (!bind) { - await db.execute(sql); - continue; - } - - if (Array.isArray(bind[0])) { - for (const rowBind of bind as Scalar[][]) { - await db.execute(sql, rowBind); - } - } else { - await db.execute(sql, bind as Scalar[]); - } - } - - await db.execute("COMMIT;"); - } catch (error) { - await db.execute("ROLLBACK;"); - throw error; - } - }); - - return { - rowsAffected: 0, - }; - }, - loadFile: async (_location: string): Promise => { - throw new Error("[op-sqlite] loadFile() is not supported on web."); - }, - updateHook: () => { - throw new Error("[op-sqlite] updateHook() is not supported on web."); - }, - commitHook: () => { - throw new Error("[op-sqlite] commitHook() is not supported on web."); - }, - rollbackHook: () => { - throw new Error("[op-sqlite] rollbackHook() is not supported on web."); - }, - prepareStatement: (query: string): PreparedStatement => { - let currentParams: Scalar[] = []; - - return { - bind: async (params: Scalar[]) => { - currentParams = params; - }, - bindSync: unsupported("bindSync"), - execute: async () => { - return db.execute(query, currentParams); - }, - }; - }, - loadExtension: unsupported("loadExtension"), - executeRaw: db.executeRaw, - executeRawSync: unsupported("executeRawSync"), - getDbPath: unsupported("getDbPath"), - reactiveExecute: unsupported("reactiveExecute"), - sync: unsupported("sync"), - setReservedBytes: unsupported("setReservedBytes"), - getReservedBytes: unsupported("getReservedBytes"), - flushPendingReactiveQueries: async () => {}, - }; - - return enhancedDb; +function enhanceWebDb(db: _InternalDB, options: { name?: string; location?: string }): DB { + const lock = { + queue: [] as _PendingTransaction[], + inProgress: false, + }; + + const startNextTransaction = () => { + if (lock.inProgress || lock.queue.length === 0) { + return; + } + + lock.inProgress = true; + const tx = lock.queue.shift(); + if (!tx) { + throw new Error("Could not get an operation on database"); + } + + setTimeout(() => { + tx.start(); + }, 0); + }; + + const withTransactionLock = async (work: () => Promise): Promise => { + return new Promise((resolve, reject) => { + const tx: _PendingTransaction = { + start: () => { + work() + .then(resolve) + .catch(reject) + .finally(() => { + lock.inProgress = false; + startNextTransaction(); + }); + }, + }; + + lock.queue.push(tx); + startNextTransaction(); + }); + }; + + const unsupported = (method: string) => () => throwSyncApiError(method); + + const enhancedDb: DB = { + close: unsupported("close"), + closeAsync: async () => { + await db.closeAsync?.(); + }, + interrupt: unsupported("interrupt"), + delete: unsupported("delete"), + attach: unsupported("attach"), + detach: unsupported("detach"), + transaction: async (fn: (tx: Transaction) => Promise): Promise => { + return withTransactionLock(async () => { + let finalized = false; + + const commit = async (): Promise => { + if (finalized) { + throw new Error( + `OP-Sqlite Error: Database: ${options.name}. Cannot execute query on finalized transaction`, + ); + } + + const res = await enhancedDb.execute("COMMIT;"); + finalized = true; + return res; + }; + + const rollback = (): QueryResult => { + throwSyncApiError("rollback"); + }; + + const execute = async (query: string, params?: Scalar[]) => { + if (finalized) { + throw new Error( + `OP-Sqlite Error: Database: ${options.name}. Cannot execute query on finalized transaction`, + ); + } + + return enhancedDb.execute(query, params); + }; + + await enhancedDb.execute("BEGIN TRANSACTION;"); + + try { + await fn({ + execute, + commit, + rollback, + }); + + if (!finalized) { + await commit(); + } + } catch (error) { + if (!finalized) { + await enhancedDb.execute("ROLLBACK;"); + } + + throw error; + } + }); + }, + executeSync: unsupported("executeSync"), + execute: db.execute, + executeWithHostObjects: db.execute, + executeBatch: async (commands: SQLBatchTuple[]): Promise => { + await withTransactionLock(async () => { + await db.execute("BEGIN TRANSACTION;"); + + try { + for (const command of commands) { + const [sql, bind] = command; + + if (!bind) { + await db.execute(sql); + continue; + } + + if (Array.isArray(bind[0])) { + for (const rowBind of bind as Scalar[][]) { + await db.execute(sql, rowBind); + } + } else { + await db.execute(sql, bind as Scalar[]); + } + } + + await db.execute("COMMIT;"); + } catch (error) { + await db.execute("ROLLBACK;"); + throw error; + } + }); + + return { + rowsAffected: 0, + }; + }, + loadFile: async (_location: string): Promise => { + throw new Error("[op-sqlite] loadFile() is not supported on web."); + }, + updateHook: () => { + throw new Error("[op-sqlite] updateHook() is not supported on web."); + }, + commitHook: () => { + throw new Error("[op-sqlite] commitHook() is not supported on web."); + }, + rollbackHook: () => { + throw new Error("[op-sqlite] rollbackHook() is not supported on web."); + }, + prepareStatement: (query: string): PreparedStatement => { + let currentParams: Scalar[] = []; + + return { + bind: async (params: Scalar[]) => { + currentParams = params; + }, + bindSync: unsupported("bindSync"), + execute: async () => { + return db.execute(query, currentParams); + }, + }; + }, + loadExtension: unsupported("loadExtension"), + executeRaw: db.executeRaw, + executeRawSync: unsupported("executeRawSync"), + getDbPath: unsupported("getDbPath"), + reactiveExecute: unsupported("reactiveExecute"), + sync: unsupported("sync"), + setReservedBytes: unsupported("setReservedBytes"), + getReservedBytes: unsupported("getReservedBytes"), + flushPendingReactiveQueries: async () => {}, + }; + + return enhancedDb; } async function createWebDb(params: { - name: string; - location?: string; - encryptionKey?: string; + name: string; + location?: string; + encryptionKey?: string; }): Promise<_InternalDB> { - if (params.encryptionKey) { - throw new Error("[op-sqlite] SQLCipher is not supported on web."); - } - - const promiser = await getPromiser(); - await ensureOpfs(promiser); - - const filename = `file:${params.name}?vfs=opfs`; - const opened = await promiser("open", { - filename, - }); - - const dbId = opened?.dbId || opened?.result?.dbId; - if (!dbId || typeof dbId !== "string") { - throw new Error("[op-sqlite] Failed to open web sqlite database."); - } - - return { - close: () => { - throwSyncApiError("close"); - }, - closeAsync: async () => { - await promiser("close", { - dbId, - }); - }, - interrupt: () => { - throwSyncApiError("interrupt"); - }, - delete: () => { - throwSyncApiError("delete"); - }, - attach: () => { - throw new Error("[op-sqlite] attach() is not supported on web."); - }, - detach: () => { - throw new Error("[op-sqlite] detach() is not supported on web."); - }, - transaction: async () => { - throw new Error( - "[op-sqlite] transaction() must be called on an opened DB object.", - ); - }, - executeSync: () => { - throwSyncApiError("executeSync"); - }, - execute: async (query: string, bind?: Scalar[]) => { - return executeWorker(promiser, dbId, query, bind); - }, - executeWithHostObjects: async (query: string, bind?: Scalar[]) => { - return executeWorker(promiser, dbId, query, bind); - }, - executeBatch: async (_commands: SQLBatchTuple[]) => { - throw new Error( - "[op-sqlite] executeBatch() must be called on an opened DB object.", - ); - }, - loadFile: async (_location: string) => { - throw new Error("[op-sqlite] loadFile() is not supported on web."); - }, - updateHook: () => { - throw new Error("[op-sqlite] updateHook() is not supported on web."); - }, - commitHook: () => { - throw new Error("[op-sqlite] commitHook() is not supported on web."); - }, - rollbackHook: () => { - throw new Error("[op-sqlite] rollbackHook() is not supported on web."); - }, - prepareStatement: (_query: string) => { - throw new Error( - "[op-sqlite] prepareStatement() must be called on an opened DB object.", - ); - }, - loadExtension: () => { - throw new Error("[op-sqlite] loadExtension() is not supported on web."); - }, - executeRaw: async (query: string, bind?: Scalar[]) => { - ensureSingleStatement(query); - - const response = await promiser("exec", { - dbId, - sql: query, - bind, - rowMode: "array", - resultRows: [], - returnValue: "resultRows", - }); - - const result = response?.result; - const rows = result?.resultRows ?? result; - return Array.isArray(rows) ? rows : []; - }, - executeRawSync: () => { - throwSyncApiError("executeRawSync"); - }, - getDbPath: () => { - throwSyncApiError("getDbPath"); - }, - reactiveExecute: () => { - throw new Error("[op-sqlite] reactiveExecute() is not supported on web."); - }, - sync: () => { - throwSyncApiError("sync"); - }, - setReservedBytes: () => { - throwSyncApiError("setReservedBytes"); - }, - getReservedBytes: () => { - throwSyncApiError("getReservedBytes"); - }, - flushPendingReactiveQueries: async () => {}, - }; + if (params.encryptionKey) { + throw new Error("[op-sqlite] SQLCipher is not supported on web."); + } + + const promiser = await getPromiser(); + await ensureOpfs(promiser); + + const filename = `file:${params.name}?vfs=opfs`; + const opened = await promiser("open", { + filename, + }); + + const dbId = opened?.dbId || opened?.result?.dbId; + if (!dbId || typeof dbId !== "string") { + throw new Error("[op-sqlite] Failed to open web sqlite database."); + } + + return { + close: () => { + throwSyncApiError("close"); + }, + closeAsync: async () => { + await promiser("close", { + dbId, + }); + }, + interrupt: () => { + throwSyncApiError("interrupt"); + }, + delete: () => { + throwSyncApiError("delete"); + }, + attach: () => { + throw new Error("[op-sqlite] attach() is not supported on web."); + }, + detach: () => { + throw new Error("[op-sqlite] detach() is not supported on web."); + }, + transaction: async () => { + throw new Error("[op-sqlite] transaction() must be called on an opened DB object."); + }, + executeSync: () => { + throwSyncApiError("executeSync"); + }, + execute: async (query: string, bind?: Scalar[]) => { + return executeWorker(promiser, dbId, query, bind); + }, + executeWithHostObjects: async (query: string, bind?: Scalar[]) => { + return executeWorker(promiser, dbId, query, bind); + }, + executeBatch: async (_commands: SQLBatchTuple[]) => { + throw new Error("[op-sqlite] executeBatch() must be called on an opened DB object."); + }, + loadFile: async (_location: string) => { + throw new Error("[op-sqlite] loadFile() is not supported on web."); + }, + updateHook: () => { + throw new Error("[op-sqlite] updateHook() is not supported on web."); + }, + commitHook: () => { + throw new Error("[op-sqlite] commitHook() is not supported on web."); + }, + rollbackHook: () => { + throw new Error("[op-sqlite] rollbackHook() is not supported on web."); + }, + prepareStatement: (_query: string) => { + throw new Error("[op-sqlite] prepareStatement() must be called on an opened DB object."); + }, + loadExtension: () => { + throw new Error("[op-sqlite] loadExtension() is not supported on web."); + }, + executeRaw: async (query: string, bind?: Scalar[]): Promise => { + ensureSingleStatement(query); + + const response = await promiser("exec", { + dbId, + sql: query, + bind, + rowMode: "array", + resultRows: [], + returnValue: "resultRows", + }); + + const result = response?.result; + const rawRows = Array.isArray(result?.resultRows) + ? (result.resultRows as Scalar[][]) + : Array.isArray(result) + ? (result as Scalar[][]) + : []; + const columnNames = Array.isArray(result?.columnNames) + ? (result.columnNames as string[]) + : []; + + return { + rowsAffected: toNumber(result?.changeCount) ?? 0, + insertId: toNumber(result?.lastInsertRowId), + rawRows, + columnNames, + }; + }, + executeRawSync: () => { + throwSyncApiError("executeRawSync"); + }, + getDbPath: () => { + throwSyncApiError("getDbPath"); + }, + reactiveExecute: () => { + throw new Error("[op-sqlite] reactiveExecute() is not supported on web."); + }, + sync: () => { + throwSyncApiError("sync"); + }, + setReservedBytes: () => { + throwSyncApiError("setReservedBytes"); + }, + getReservedBytes: () => { + throwSyncApiError("getReservedBytes"); + }, + flushPendingReactiveQueries: async () => {}, + }; } /** @@ -447,65 +445,61 @@ async function createWebDb(params: { * Web is async-only: use openAsync() and async methods like execute(). */ export const openAsync = async (params: { - name: string; - location?: string; - encryptionKey?: string; + name: string; + location?: string; + encryptionKey?: string; }): Promise => { - const db = await createWebDb(params); - return enhanceWebDb(db, params); + const db = await createWebDb(params); + return enhanceWebDb(db, params); }; -export const open = (_params: { - name: string; - location?: string; - encryptionKey?: string; -}): DB => { - throwSyncApiError("open"); +export const open = (_params: { name: string; location?: string; encryptionKey?: string }): DB => { + throwSyncApiError("open"); }; export const openSync = (_params: { - url: string; - authToken: string; - name: string; - location?: string; - libsqlSyncInterval?: number; - libsqlOffline?: boolean; - encryptionKey?: string; - remoteEncryptionKey?: string; + url: string; + authToken: string; + name: string; + location?: string; + libsqlSyncInterval?: number; + libsqlOffline?: boolean; + encryptionKey?: string; + remoteEncryptionKey?: string; }): DB => { - throwSyncApiError("openSync"); + throwSyncApiError("openSync"); }; export const openRemote = (_params: { url: string; authToken: string }): DB => { - throw new Error("[op-sqlite] openRemote() is not supported on web."); + throw new Error("[op-sqlite] openRemote() is not supported on web."); }; export const moveAssetsDatabase = async (_args: { - filename: string; - path?: string; - overwrite?: boolean; + filename: string; + path?: string; + overwrite?: boolean; }): Promise => { - throw new Error("[op-sqlite] moveAssetsDatabase() is not supported on web."); + throw new Error("[op-sqlite] moveAssetsDatabase() is not supported on web."); }; export const getDylibPath = (_bundle: string, _name: string): string => { - throw new Error("[op-sqlite] getDylibPath() is not supported on web."); + throw new Error("[op-sqlite] getDylibPath() is not supported on web."); }; export const isSQLCipher = (): boolean => { - return false; + return false; }; export const isLibsql = (): boolean => { - return false; + return false; }; export const isTurso = (): boolean => { - return false; + return false; }; export const isIOSEmbedded = (): boolean => { - return false; + return false; }; /** diff --git a/src/types.ts b/src/types.ts index 2174e21c..0e8abccb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,4 @@ -export type Scalar = - | string - | number - | boolean - | null - | ArrayBuffer - | ArrayBufferView; +export type Scalar = string | number | boolean | null | ArrayBuffer | ArrayBufferView; /** * Object returned by SQL Query executions { @@ -17,17 +11,24 @@ export type Scalar = * @interface QueryResult */ export type QueryResult = { - insertId?: number; - rowsAffected: number; - res?: any[]; - rows: Array>; - // An array of intermediate results, just values without column names - rawRows?: Scalar[][]; - columnNames?: string[]; - /** - * Query metadata, available only for select query results - */ - metadata?: ColumnMetadata[]; + insertId?: number; + rowsAffected: number; + res?: any[]; + rows: Array>; + // An array of intermediate results, just values without column names + rawRows?: Scalar[][]; + columnNames?: string[]; + /** + * Query metadata, available only for select query results + */ + metadata?: ColumnMetadata[]; +}; + +export type RawQueryResult = { + insertId?: number; + rowsAffected: number; + rawRows: Scalar[][]; + columnNames: string[]; }; /** @@ -35,13 +36,13 @@ export type QueryResult = { * Describes some information about columns fetched by the query */ export type ColumnMetadata = { - /** The name used for this column for this result set */ - name: string; - /** The declared column type for this column, when fetched directly from a table or a View resulting from a table column. "UNKNOWN" for dynamic values, like function returned ones. */ - type: string; - /** - * The index for this column for this result set*/ - index: number; + /** The name used for this column for this result set */ + name: string; + /** The declared column type for this column, when fetched directly from a table or a View resulting from a table column. "UNKNOWN" for dynamic values, like function returned ones. */ + type: string; + /** + * The index for this column for this result set*/ + index: number; }; /** @@ -50,10 +51,7 @@ export type ColumnMetadata = { * If a single query must be executed many times with different arguments, its preferred * to declare it a single time, and use an array of array parameters. */ -export type SQLBatchTuple = - | [string] - | [string, Scalar[]] - | [string, Scalar[][]]; +export type SQLBatchTuple = [string] | [string, Scalar[]] | [string, Scalar[][]]; export type UpdateHookOperation = "INSERT" | "DELETE" | "UPDATE"; @@ -63,7 +61,7 @@ export type UpdateHookOperation = "INSERT" | "DELETE" | "UPDATE"; * rowsAffected: Number of affected rows if status == 0 */ export type BatchQueryResult = { - rowsAffected?: number; + rowsAffected?: number; }; /** @@ -71,267 +69,248 @@ export type BatchQueryResult = { * Similar to BatchQueryResult */ export type FileLoadResult = BatchQueryResult & { - commands?: number; + commands?: number; }; export type Transaction = { - commit: () => Promise; - execute: (query: string, params?: Scalar[]) => Promise; - rollback: () => QueryResult; + commit: () => Promise; + execute: (query: string, params?: Scalar[]) => Promise; + rollback: () => QueryResult; }; export type _PendingTransaction = { - /* - * The start function should not throw or return a promise because the - * queue just calls it and does not monitor for failures or completions. - * - * It should catch any errors and call the resolve or reject of the wrapping - * promise when complete. - * - * It should also automatically commit or rollback the transaction if needed - */ - start: () => void; + /* + * The start function should not throw or return a promise because the + * queue just calls it and does not monitor for failures or completions. + * + * It should catch any errors and call the resolve or reject of the wrapping + * promise when complete. + * + * It should also automatically commit or rollback the transaction if needed + */ + start: () => void; }; export type PreparedStatement = { - bind: (params: any[]) => Promise; - bindSync: (params: any[]) => void; - execute: () => Promise; + bind: (params: any[]) => Promise; + bindSync: (params: any[]) => void; + execute: () => Promise; }; export type _InternalDB = { - close: () => void; - closeAsync?: () => Promise; - interrupt: () => void; - delete: () => void; - attach: (params: { - secondaryDbFileName: string; - alias: string; - location?: string; - }) => void; - detach: (alias: string) => void; - transaction: (fn: (tx: Transaction) => Promise) => Promise; - executeSync: (query: string, params?: Scalar[]) => QueryResult; - execute: (query: string, params?: Scalar[]) => Promise; - executeWithHostObjects: ( - query: string, - params?: Scalar[], - ) => Promise; - executeBatch: (commands: SQLBatchTuple[]) => Promise; - loadFile: (location: string) => Promise; - updateHook: ( - callback?: - | ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) - | null, - ) => void; - commitHook: (callback?: (() => void) | null) => void; - rollbackHook: (callback?: (() => void) | null) => void; - prepareStatement: (query: string) => PreparedStatement; - loadExtension: (path: string, entryPoint?: string) => void; - executeRaw: (query: string, params?: Scalar[]) => Promise; - executeRawSync: (query: string, params?: Scalar[]) => any[]; - getDbPath: (location?: string) => string; - reactiveExecute: (params: { - query: string; - arguments: any[]; - fireOn: { - table: string; - ids?: number[]; - }[]; - callback: (response: any) => void; - }) => () => void; - sync: () => void; - setReservedBytes: (reservedBytes: number) => void; - getReservedBytes: () => number; - flushPendingReactiveQueries: () => Promise; + close: () => void; + closeAsync?: () => Promise; + interrupt: () => void; + delete: () => void; + attach: (params: { secondaryDbFileName: string; alias: string; location?: string }) => void; + detach: (alias: string) => void; + transaction: (fn: (tx: Transaction) => Promise) => Promise; + executeSync: (query: string, params?: Scalar[]) => QueryResult; + execute: (query: string, params?: Scalar[]) => Promise; + executeWithHostObjects: (query: string, params?: Scalar[]) => Promise; + executeBatch: (commands: SQLBatchTuple[]) => Promise; + loadFile: (location: string) => Promise; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null, + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + prepareStatement: (query: string) => PreparedStatement; + loadExtension: (path: string, entryPoint?: string) => void; + executeRaw: (query: string, params?: Scalar[]) => Promise; + executeRawSync: (query: string, params?: Scalar[]) => RawQueryResult; + getDbPath: (location?: string) => string; + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + sync: () => void; + setReservedBytes: (reservedBytes: number) => void; + getReservedBytes: () => number; + flushPendingReactiveQueries: () => Promise; }; export type DB = { - close: () => void; - closeAsync: () => Promise; - /** - * Aborts any pending database operation on this connection. - * - * Calls SQLite's native sqlite3_interrupt(). Safe to call from a thread - * different from the one running the operation. An interrupted operation - * returns SQLITE_INTERRUPT and any in-flight transaction is rolled back. - */ - interrupt: () => void; - delete: () => void; - attach: (params: { - secondaryDbFileName: string; - alias: string; - location?: string; - }) => void; - detach: (alias: string) => void; - /** - * Wraps all the executions into a transaction. If an error is thrown it will rollback all of the changes - * - * You need to use this if you are using reactive queries for the queries to fire after the transaction is done - */ - transaction: (fn: (tx: Transaction) => Promise) => Promise; - /** - * Sync version of the execute function - * It will block the JS thread and therefore your UI and should be used with caution - * - * When writing your queries, you can use the ? character as a placeholder for parameters - * The parameters will be automatically escaped and sanitized - * - * Example: - * db.executeSync('SELECT * FROM table WHERE id = ?', [1]); - * - * If you are writing a query that doesn't require parameters, you can omit the second argument - * - * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! - * Transactions protect you from partial writes and ensure that your data is always in a consistent state - * - * @param query - * @param params - * @returns QueryResult - */ - executeSync: (query: string, params?: Scalar[]) => QueryResult; - /** - * Basic query execution function, it is async don't forget to await it - * - * When writing your queries, you can use the ? character as a placeholder for parameters - * The parameters will be automatically escaped and sanitized - * - * Example: - * await db.execute('SELECT * FROM table WHERE id = ?', [1]); - * - * If you are writing a query that doesn't require parameters, you can omit the second argument - * - * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! - * Transactions protect you from partial writes and ensure that your data is always in a consistent state - * - * If you need a large amount of queries ran as fast as possible you should be using `executeBatch`, `executeRaw`, `loadFile` or `executeWithHostObjects` - * - * @param query string of your SQL query - * @param params a list of parameters to bind to the query, if any - * @returns Promise with the result of the query - */ - execute: (query: string, params?: Scalar[]) => Promise; - /** - * Similar to the execute function but returns the response in HostObjects - * Read more about HostObjects in the documentation and their pitfalls - * - * Will be a lot faster than the normal execute functions when returning data but you will pay when accessing the fields - * as the conversion is done the moment you access any field - * @param query - * @param params - * @returns - */ - executeWithHostObjects: ( - query: string, - params?: Scalar[], - ) => Promise; - /** - * Executes all the queries in the params inside a single transaction - * - * It's faster than executing single queries as data is sent to the native side only once - * @param commands - * @returns Promise - */ - executeBatch: (commands: SQLBatchTuple[]) => Promise; - /** - * Loads a SQLite Dump from disk. It will be the fastest way to execute a large set of queries as no JS is involved - */ - loadFile: (location: string) => Promise; - updateHook: ( - callback?: - | ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) - | null, - ) => void; - commitHook: (callback?: (() => void) | null) => void; - rollbackHook: (callback?: (() => void) | null) => void; - /** - * Constructs a prepared statement from the query string - * The statement can be re-bound with parameters and executed - * The performance gain is significant when the same query is executed multiple times, NOT when the query is executed (once) - * The cost lies in the preparation of the statement as it is compiled and optimized by the sqlite engine, the params can then rebound - * but the query itself is already optimized - * - * @param query string of your SQL query - * @returns Prepared statement object - */ - prepareStatement: (query: string) => PreparedStatement; - /** - * Loads a runtime loadable sqlite extension. Libsql and iOS embedded version do not support loading extensions - */ - loadExtension: (path: string, entryPoint?: string) => void; - /** - * Same as `execute` except the results are not returned in objects but rather in arrays with just the values and not the keys - * It will be faster since a lot of repeated work is skipped and only the values you care about are returned - */ - executeRaw: (query: string, params?: Scalar[]) => Promise; - /** - * Same as `executeRaw` but it will block the JS thread and therefore your UI and should be used with caution - * It will return an array of arrays with just the values and not the keys - */ - executeRawSync: (query: string, params?: Scalar[]) => any[]; - /** - * Gets the absolute path to the db file. Useful for debugging on local builds and for attaching the DB from users devices - */ - getDbPath: (location?: string) => string; - /** - * Reactive execution of queries when data is written to the database. Check the docs for how to use them. - */ - reactiveExecute: (params: { - query: string; - arguments: any[]; - fireOn: { - table: string; - ids?: number[]; - }[]; - callback: (response: any) => void; - }) => () => void; - /** This function is only available for libsql. - * Allows to trigger a sync the database with it's remote replica - * In order for this function to work you need to use openSync or openRemote functions - * with libsql: true in the package.json - * - * The database is hosted in turso - **/ - sync: () => void; - setReservedBytes: (reservedBytes: number) => void; - getReservedBytes: () => number; - /** - * If you have changed any of the tables outside of a transaction then the reactive queries will not fire on their own - * This method allows to flush the pending queue of changes. Useful when using Drizzle or other ORM that do not - * use the db.transaction method internally - * @returns void - */ - flushPendingReactiveQueries: () => Promise; + close: () => void; + closeAsync: () => Promise; + /** + * Aborts any pending database operation on this connection. + * + * Calls SQLite's native sqlite3_interrupt(). Safe to call from a thread + * different from the one running the operation. An interrupted operation + * returns SQLITE_INTERRUPT and any in-flight transaction is rolled back. + */ + interrupt: () => void; + delete: () => void; + attach: (params: { secondaryDbFileName: string; alias: string; location?: string }) => void; + detach: (alias: string) => void; + /** + * Wraps all the executions into a transaction. If an error is thrown it will rollback all of the changes + * + * You need to use this if you are using reactive queries for the queries to fire after the transaction is done + */ + transaction: (fn: (tx: Transaction) => Promise) => Promise; + /** + * Sync version of the execute function + * It will block the JS thread and therefore your UI and should be used with caution + * + * When writing your queries, you can use the ? character as a placeholder for parameters + * The parameters will be automatically escaped and sanitized + * + * Example: + * db.executeSync('SELECT * FROM table WHERE id = ?', [1]); + * + * If you are writing a query that doesn't require parameters, you can omit the second argument + * + * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! + * Transactions protect you from partial writes and ensure that your data is always in a consistent state + * + * @param query + * @param params + * @returns QueryResult + */ + executeSync: (query: string, params?: Scalar[]) => QueryResult; + /** + * Basic query execution function, it is async don't forget to await it + * + * When writing your queries, you can use the ? character as a placeholder for parameters + * The parameters will be automatically escaped and sanitized + * + * Example: + * await db.execute('SELECT * FROM table WHERE id = ?', [1]); + * + * If you are writing a query that doesn't require parameters, you can omit the second argument + * + * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! + * Transactions protect you from partial writes and ensure that your data is always in a consistent state + * + * If you need a large amount of queries ran as fast as possible you should be using `executeBatch`, `executeRaw`, `loadFile` or `executeWithHostObjects` + * + * @param query string of your SQL query + * @param params a list of parameters to bind to the query, if any + * @returns Promise with the result of the query + */ + execute: (query: string, params?: Scalar[]) => Promise; + /** + * Similar to the execute function but returns the response in HostObjects + * Read more about HostObjects in the documentation and their pitfalls + * + * Will be a lot faster than the normal execute functions when returning data but you will pay when accessing the fields + * as the conversion is done the moment you access any field + * @param query + * @param params + * @returns + */ + executeWithHostObjects: (query: string, params?: Scalar[]) => Promise; + /** + * Executes all the queries in the params inside a single transaction + * + * It's faster than executing single queries as data is sent to the native side only once + * @param commands + * @returns Promise + */ + executeBatch: (commands: SQLBatchTuple[]) => Promise; + /** + * Loads a SQLite Dump from disk. It will be the fastest way to execute a large set of queries as no JS is involved + */ + loadFile: (location: string) => Promise; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null, + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + /** + * Constructs a prepared statement from the query string + * The statement can be re-bound with parameters and executed + * The performance gain is significant when the same query is executed multiple times, NOT when the query is executed (once) + * The cost lies in the preparation of the statement as it is compiled and optimized by the sqlite engine, the params can then rebound + * but the query itself is already optimized + * + * @param query string of your SQL query + * @returns Prepared statement object + */ + prepareStatement: (query: string) => PreparedStatement; + /** + * Loads a runtime loadable sqlite extension. Libsql and iOS embedded version do not support loading extensions + */ + loadExtension: (path: string, entryPoint?: string) => void; + /** + * Same as `execute` except the rows are returned in arrays with just the values and not the keys. + * The result includes `rawRows` plus `columnNames` so callers can map them when needed. + */ + executeRaw: (query: string, params?: Scalar[]) => Promise; + /** + * Same as `executeRaw` but it will block the JS thread and therefore your UI and should be used with caution + */ + executeRawSync: (query: string, params?: Scalar[]) => RawQueryResult; + /** + * Gets the absolute path to the db file. Useful for debugging on local builds and for attaching the DB from users devices + */ + getDbPath: (location?: string) => string; + /** + * Reactive execution of queries when data is written to the database. Check the docs for how to use them. + */ + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + /** This function is only available for libsql. + * Allows to trigger a sync the database with it's remote replica + * In order for this function to work you need to use openSync or openRemote functions + * with libsql: true in the package.json + * + * The database is hosted in turso + **/ + sync: () => void; + setReservedBytes: (reservedBytes: number) => void; + getReservedBytes: () => number; + /** + * If you have changed any of the tables outside of a transaction then the reactive queries will not fire on their own + * This method allows to flush the pending queue of changes. Useful when using Drizzle or other ORM that do not + * use the db.transaction method internally + * @returns void + */ + flushPendingReactiveQueries: () => Promise; }; export type DBParams = { - url?: string; - authToken?: string; - name?: string; - location?: string; - syncInterval?: number; + url?: string; + authToken?: string; + name?: string; + location?: string; + syncInterval?: number; }; export type OPSQLiteProxy = { - open: (options: { - name: string; - location?: string; - encryptionKey?: string; - }) => _InternalDB; - openRemote: (options: { url: string; authToken: string }) => _InternalDB; - openSync: (options: DBParams) => _InternalDB; - isSQLCipher: () => boolean; - isLibsql: () => boolean; - isTurso: () => boolean; - isIOSEmbedded: () => boolean; + open: (options: { name: string; location?: string; encryptionKey?: string }) => _InternalDB; + openRemote: (options: { url: string; authToken: string }) => _InternalDB; + openSync: (options: DBParams) => _InternalDB; + isSQLCipher: () => boolean; + isLibsql: () => boolean; + isTurso: () => boolean; + isIOSEmbedded: () => boolean; };