From 577ea9f273bd2ece92b597bff2d67250a80a0bd9 Mon Sep 17 00:00:00 2001 From: Edy Silva Date: Wed, 24 Sep 2025 14:44:37 -0300 Subject: [PATCH] wip: sqlite async api --- src/env_properties.h | 1 + src/node_sqlite.cc | 966 ++++++++++++++----- src/node_sqlite.h | 157 +-- test/parallel/test-sqlite-database-async.mjs | 71 ++ test/parallel/test-sqlite-statement-async.js | 124 +++ 5 files changed, 1025 insertions(+), 294 deletions(-) create mode 100644 test/parallel/test-sqlite-database-async.mjs create mode 100644 test/parallel/test-sqlite-statement-async.js diff --git a/src/env_properties.h b/src/env_properties.h index f889c2120f29b0..5c231dc6c2653e 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -462,6 +462,7 @@ V(space_stats_template, v8::DictionaryTemplate) \ V(sqlite_column_template, v8::DictionaryTemplate) \ V(sqlite_statement_sync_constructor_template, v8::FunctionTemplate) \ + V(sqlite_statement_async_constructor_template, v8::FunctionTemplate) \ V(sqlite_statement_sync_iterator_constructor_template, v8::FunctionTemplate) \ V(sqlite_session_constructor_template, v8::FunctionTemplate) \ V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \ diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index af0e5c21bb8cd6..f0215724ae176f 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -219,9 +219,9 @@ void JSValueToSQLiteResult(Isolate* isolate, } } -class DatabaseSync; +class Database; -inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, DatabaseSync* db) { +inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, Database* db) { if (db->ShouldIgnoreSQLiteError()) { db->SetIgnoreNextSQLiteError(false); return; @@ -268,7 +268,7 @@ inline MaybeLocal NullableSQLiteStringToValue(Isolate* isolate, class CustomAggregate { public: explicit CustomAggregate(Environment* env, - DatabaseSync* db, + Database* db, bool use_bigint_args, Local start, Local step_fn, @@ -308,7 +308,7 @@ class CustomAggregate { static inline void xStepBase(sqlite3_context* ctx, int argc, sqlite3_value** argv, - Global CustomAggregate::*mptr) { + Global CustomAggregate::* mptr) { CustomAggregate* self = static_cast(sqlite3_user_data(ctx)); Environment* env = self->env_; @@ -432,7 +432,7 @@ class CustomAggregate { } Environment* env_; - DatabaseSync* db_; + Database* db_; bool use_bigint_args_; Global start_; Global step_fn_; @@ -440,10 +440,56 @@ class CustomAggregate { Global result_fn_; }; +template +class SQLiteAsyncTask : public ThreadPoolWork { + public: + explicit SQLiteAsyncTask( + Environment* env, + Database* db, + Local resolver, + std::function work, + std::function)> after) + : ThreadPoolWork(env, "node_sqlite_async_task"), + env_(env), + db_(db), + work_(work), + after_(after) { + resolver_.Reset(env->isolate(), resolver); + } + + void DoThreadPoolWork() override { + if (work_) { + result_ = work_(); + } + } + + void AfterThreadPoolWork(int status) override { + Isolate* isolate = env_->isolate(); + HandleScope handle_scope(isolate); + Local resolver = + Local::New(isolate, resolver_); + + if (after_) { + after_(result_, resolver); + Finalize(); + } + } + + void Finalize() { db_->RemoveAsyncTask(this); } + + private: + Environment* env_; + Database* db_; + Global resolver_; + std::function work_ = nullptr; + std::function)> after_ = nullptr; + T result_; +}; + class BackupJob : public ThreadPoolWork { public: explicit BackupJob(Environment* env, - DatabaseSync* source, + Database* source, Local resolver, std::string source_db, std::string destination_name, @@ -591,7 +637,7 @@ class BackupJob : public ThreadPoolWork { Environment* env() const { return env_; } Environment* env_; - DatabaseSync* source_; + Database* source_; Global resolver_; Global progressFunc_; sqlite3* dest_ = nullptr; @@ -605,7 +651,7 @@ class BackupJob : public ThreadPoolWork { UserDefinedFunction::UserDefinedFunction(Environment* env, Local fn, - DatabaseSync* db, + Database* db, bool use_bigint_args) : env_(env), fn_(env->isolate(), fn), @@ -664,11 +710,11 @@ void UserDefinedFunction::xDestroy(void* self) { delete static_cast(self); } -DatabaseSync::DatabaseSync(Environment* env, - Local object, - DatabaseOpenConfiguration&& open_config, - bool open, - bool allow_load_extension) +Database::Database(Environment* env, + Local object, + DatabaseOpenConfiguration&& open_config, + bool open, + bool allow_load_extension) : BaseObject(env, object), open_config_(std::move(open_config)) { MakeWeak(); connection_ = nullptr; @@ -681,15 +727,23 @@ DatabaseSync::DatabaseSync(Environment* env, } } -void DatabaseSync::AddBackup(BackupJob* job) { +void Database::AddBackup(BackupJob* job) { backups_.insert(job); } -void DatabaseSync::RemoveBackup(BackupJob* job) { +void Database::RemoveBackup(BackupJob* job) { backups_.erase(job); } -void DatabaseSync::DeleteSessions() { +void Database::AddAsyncTask(ThreadPoolWork* async_task) { + async_tasks_.insert(async_task); +} + +void Database::RemoveAsyncTask(ThreadPoolWork* async_task) { + async_tasks_.erase(async_task); +} + +void Database::DeleteSessions() { // all attached sessions need to be deleted before the database is closed // https://www.sqlite.org/session/sqlite3session_create.html for (auto* session : sessions_) { @@ -698,8 +752,9 @@ void DatabaseSync::DeleteSessions() { sessions_.clear(); } -DatabaseSync::~DatabaseSync() { +Database::~Database() { FinalizeBackups(); + async_tasks_.clear(); if (IsOpen()) { FinalizeStatements(); @@ -709,13 +764,13 @@ DatabaseSync::~DatabaseSync() { } } -void DatabaseSync::MemoryInfo(MemoryTracker* tracker) const { +void Database::MemoryInfo(MemoryTracker* tracker) const { // TODO(tniessen): more accurately track the size of all fields tracker->TrackFieldWithSize( "open_config", sizeof(open_config_), "DatabaseOpenConfiguration"); } -bool DatabaseSync::Open() { +bool Database::Open() { if (IsOpen()) { THROW_ERR_INVALID_STATE(env(), "database is already open"); return false; @@ -770,7 +825,7 @@ bool DatabaseSync::Open() { return true; } -void DatabaseSync::FinalizeBackups() { +void Database::FinalizeBackups() { for (auto backup : backups_) { backup->Cleanup(); } @@ -778,7 +833,7 @@ void DatabaseSync::FinalizeBackups() { backups_.clear(); } -void DatabaseSync::FinalizeStatements() { +void Database::FinalizeStatements() { for (auto stmt : statements_) { stmt->Finalize(); } @@ -786,31 +841,31 @@ void DatabaseSync::FinalizeStatements() { statements_.clear(); } -void DatabaseSync::UntrackStatement(StatementSync* statement) { +void Database::UntrackStatement(Statement* statement) { auto it = statements_.find(statement); if (it != statements_.end()) { statements_.erase(it); } } -inline bool DatabaseSync::IsOpen() { +inline bool Database::IsOpen() { return connection_ != nullptr; } -inline sqlite3* DatabaseSync::Connection() { +inline sqlite3* Database::Connection() { return connection_; } -void DatabaseSync::SetIgnoreNextSQLiteError(bool ignore) { +void Database::SetIgnoreNextSQLiteError(bool ignore) { ignore_next_sqlite_error_ = ignore; } -bool DatabaseSync::ShouldIgnoreSQLiteError() { +bool Database::ShouldIgnoreSQLiteError() { return ignore_next_sqlite_error_; } -void DatabaseSync::CreateTagStore(const FunctionCallbackInfo& args) { - DatabaseSync* db = BaseObject::Unwrap(args.This()); +void Database::CreateTagStore(const FunctionCallbackInfo& args) { + Database* db = BaseObject::Unwrap(args.This()); Environment* env = Environment::GetCurrent(args); if (!db->IsOpen()) { @@ -823,7 +878,7 @@ void DatabaseSync::CreateTagStore(const FunctionCallbackInfo& args) { } BaseObjectPtr session = - SQLTagStore::Create(env, BaseObjectWeakPtr(db), capacity); + SQLTagStore::Create(env, BaseObjectWeakPtr(db), capacity); if (!session) { // Handle error if creation failed THROW_ERR_SQLITE_ERROR(env->isolate(), "Failed to create SQLTagStore"); @@ -879,7 +934,8 @@ std::optional ValidateDatabasePath(Environment* env, return std::nullopt; } -void DatabaseSync::New(const FunctionCallbackInfo& args) { +inline void DatabaseNew(const FunctionCallbackInfo& args, + bool async = true) { Environment* env = Environment::GetCurrent(args); if (!args.IsConstructCall()) { THROW_ERR_CONSTRUCT_CALL_REQUIRED(env); @@ -1066,33 +1122,41 @@ void DatabaseSync::New(const FunctionCallbackInfo& args) { } } - new DatabaseSync( + open_config.set_async(async); + new Database( env, args.This(), std::move(open_config), open, allow_load_extension); } -void DatabaseSync::Open(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::New(const FunctionCallbackInfo& args) { + DatabaseNew(args, false); +} + +void Database::NewAsync(const FunctionCallbackInfo& args) { + DatabaseNew(args, true); +} + +void Database::Open(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); db->Open(); } -void DatabaseSync::IsOpenGetter(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::IsOpenGetter(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); args.GetReturnValue().Set(db->IsOpen()); } -void DatabaseSync::IsTransactionGetter( - const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::IsTransactionGetter(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); args.GetReturnValue().Set(sqlite3_get_autocommit(db->connection_) == 0); } -void DatabaseSync::Close(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::Close(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); @@ -1103,7 +1167,7 @@ void DatabaseSync::Close(const FunctionCallbackInfo& args) { db->connection_ = nullptr; } -void DatabaseSync::Dispose(const v8::FunctionCallbackInfo& args) { +void Database::Dispose(const v8::FunctionCallbackInfo& args) { v8::TryCatch try_catch(args.GetIsolate()); Close(args); if (try_catch.HasCaught()) { @@ -1111,8 +1175,8 @@ void DatabaseSync::Dispose(const v8::FunctionCallbackInfo& args) { } } -void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::Prepare(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); @@ -1127,16 +1191,34 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { sqlite3_stmt* s = nullptr; int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, 0); CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); - BaseObjectPtr stmt = - StatementSync::Create(env, BaseObjectPtr(db), s); + BaseObjectPtr stmt = + Statement::Create(env, BaseObjectPtr(db), s); db->statements_.insert(stmt.get()); args.GetReturnValue().Set(stmt->object()); } -void DatabaseSync::Exec(const FunctionCallbackInfo& args) { - DatabaseSync* db; +template +Local MakeSQLiteAsyncWork( + Environment* env, + Database* db, + std::function task, + std::function)> after) { + Local resolver; + if (!Promise::Resolver::New(env->context()).ToLocal(&resolver)) { + return Local(); + } + + auto* work = new SQLiteAsyncTask(env, db, resolver, task, after); + work->ScheduleWork(); + db->AddAsyncTask(work); + return resolver; +} + +void Database::Exec(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); if (!args[0]->IsString()) { @@ -1145,13 +1227,49 @@ void DatabaseSync::Exec(const FunctionCallbackInfo& args) { return; } - Utf8Value sql(env->isolate(), args[0].As()); - int r = sqlite3_exec(db->connection_, *sql, nullptr, nullptr, nullptr); - CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); + auto sql = Utf8Value(env->isolate(), args[0].As()).ToString(); + auto task = [sql, db]() -> int { + return sqlite3_exec( + db->connection_, sql.c_str(), nullptr, nullptr, nullptr); + }; + + if (!db->open_config_.get_async()) { + int r = task(); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); + return; + } + + auto after = [db, env, isolate](int exec_result, + Local resolver) { + if (exec_result != SQLITE_OK) { + if (db->ShouldIgnoreSQLiteError()) { + db->SetIgnoreNextSQLiteError(false); + return; + } + + Local e; + if (!CreateSQLiteError(isolate, db->Connection()).ToLocal(&e)) { + return; + } + + resolver->Reject(env->context(), e).FromJust(); + return; + } + + resolver->Resolve(env->context(), Undefined(env->isolate())).FromJust(); + }; + + Local resolver = + MakeSQLiteAsyncWork(env, db, task, after); + if (resolver.IsEmpty()) { + return; + } + + args.GetReturnValue().Set(resolver->GetPromise()); } -void DatabaseSync::CustomFunction(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::CustomFunction(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); @@ -1293,8 +1411,8 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo& args) { CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); } -void DatabaseSync::Location(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::Location(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); @@ -1323,8 +1441,8 @@ void DatabaseSync::Location(const FunctionCallbackInfo& args) { } } -void DatabaseSync::AggregateFunction(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::AggregateFunction(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); @@ -1480,7 +1598,7 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo& args) { CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); } -void DatabaseSync::CreateSession(const FunctionCallbackInfo& args) { +void Database::CreateSession(const FunctionCallbackInfo& args) { std::string table; std::string db_name = "main"; @@ -1537,7 +1655,7 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo& args) { } } - DatabaseSync* db; + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); @@ -1550,7 +1668,7 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo& args) { CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); BaseObjectPtr session = - Session::Create(env, BaseObjectWeakPtr(db), pSession); + Session::Create(env, BaseObjectWeakPtr(db), pSession); args.GetReturnValue().Set(session->object()); } @@ -1562,7 +1680,7 @@ void Backup(const FunctionCallbackInfo& args) { return; } - DatabaseSync* db; + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args[0].As()); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); std::optional dest_path = @@ -1685,11 +1803,11 @@ static int xFilter(void* pCtx, const char* zTab) { return filterCallback(zTab) ? 1 : 0; } -void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo& args) { +void Database::ApplyChangeset(const FunctionCallbackInfo& args) { conflictCallback = nullptr; filterCallback = nullptr; - DatabaseSync* db; + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); @@ -1798,9 +1916,8 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo& args) { THROW_ERR_SQLITE_ERROR(env->isolate(), r); } -void DatabaseSync::EnableLoadExtension( - const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::EnableLoadExtension(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); auto isolate = args.GetIsolate(); if (!args[0]->IsBoolean()) { @@ -1824,8 +1941,8 @@ void DatabaseSync::EnableLoadExtension( CHECK_ERROR_OR_THROW(isolate, db, load_extension_ret, SQLITE_OK, void()); } -void DatabaseSync::LoadExtension(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::LoadExtension(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -1860,10 +1977,10 @@ void DatabaseSync::LoadExtension(const FunctionCallbackInfo& args) { } } -StatementSync::StatementSync(Environment* env, - Local object, - BaseObjectPtr db, - sqlite3_stmt* stmt) +Statement::Statement(Environment* env, + Local object, + BaseObjectPtr db, + sqlite3_stmt* stmt) : BaseObject(env, object), db_(std::move(db)) { MakeWeak(); statement_ = stmt; @@ -1875,23 +1992,23 @@ StatementSync::StatementSync(Environment* env, bare_named_params_ = std::nullopt; } -StatementSync::~StatementSync() { +Statement::~Statement() { if (!IsFinalized()) { db_->UntrackStatement(this); Finalize(); } } -void StatementSync::Finalize() { +void Statement::Finalize() { sqlite3_finalize(statement_); statement_ = nullptr; } -inline bool StatementSync::IsFinalized() { +inline bool Statement::IsFinalized() { return statement_ == nullptr; } -bool StatementSync::BindParams(const FunctionCallbackInfo& args) { +bool Statement::BindParams(const FunctionCallbackInfo& args) { int r = sqlite3_clear_bindings(statement_); CHECK_ERROR_OR_THROW(env()->isolate(), db_.get(), r, SQLITE_OK, false); @@ -1993,7 +2110,7 @@ bool StatementSync::BindParams(const FunctionCallbackInfo& args) { return true; } -bool StatementSync::BindValue(const Local& value, const int index) { +bool Statement::BindValue(const Local& value, const int index) { // SQLite only supports a subset of JavaScript types. Some JS types such as // functions don't make sense to support. Other JS types such as booleans and // Dates could be supported by converting them to numbers. However, there @@ -2033,12 +2150,12 @@ bool StatementSync::BindValue(const Local& value, const int index) { return true; } -MaybeLocal StatementSync::ColumnToValue(const int column) { +MaybeLocal Statement::ColumnToValue(const int column) { return StatementExecutionHelper::ColumnToValue( env(), statement_, column, use_big_ints_); } -MaybeLocal StatementSync::ColumnNameToName(const int column) { +MaybeLocal Statement::ColumnNameToName(const int column) { const char* col_name = sqlite3_column_name(statement_, column); if (col_name == nullptr) { THROW_ERR_INVALID_STATE(env(), "Cannot get name of column %d", column); @@ -2070,7 +2187,7 @@ MaybeLocal StatementExecutionHelper::ColumnNameToName(Environment* env, return String::NewFromUtf8(env->isolate(), col_name).As(); } -void StatementSync::MemoryInfo(MemoryTracker* tracker) const {} +void Statement::MemoryInfo(MemoryTracker* tracker) const {} Maybe ExtractRowValues(Environment* env, sqlite3_stmt* stmt, @@ -2091,7 +2208,7 @@ Maybe ExtractRowValues(Environment* env, } Local StatementExecutionHelper::All(Environment* env, - DatabaseSync* db, + Database* db, sqlite3_stmt* stmt, bool return_arrays, bool use_big_ints) { @@ -2133,26 +2250,27 @@ Local StatementExecutionHelper::All(Environment* env, return Array::New(isolate, rows.data(), rows.size()); } -Local StatementExecutionHelper::Run(Environment* env, - DatabaseSync* db, - sqlite3_stmt* stmt, - bool use_big_ints) { - Isolate* isolate = env->isolate(); +int StatementRun(sqlite3_stmt* stmt) { sqlite3_step(stmt); - int r = sqlite3_reset(stmt); - CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, Object::New(isolate)); - Local result = Object::New(isolate); - sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(db->Connection()); - sqlite3_int64 changes = sqlite3_changes64(db->Connection()); + return sqlite3_reset(stmt); +} + +MaybeLocal StatementSQLiteToJSConverter::ConvertStatementRun( + Environment* env, + bool use_big_ints, + sqlite3_int64 changes, + sqlite3_int64 last_insert_rowid) { + Local result = Object::New(env->isolate()); Local last_insert_rowid_val; Local changes_val; if (use_big_ints) { - last_insert_rowid_val = BigInt::New(isolate, last_insert_rowid); - changes_val = BigInt::New(isolate, changes); + last_insert_rowid_val = BigInt::New(env->isolate(), last_insert_rowid); + changes_val = BigInt::New(env->isolate(), changes); } else { - last_insert_rowid_val = Number::New(isolate, last_insert_rowid); - changes_val = Number::New(isolate, changes); + last_insert_rowid_val = + Number::New(env->isolate(), static_cast(last_insert_rowid)); + changes_val = Number::New(env->isolate(), static_cast(changes)); } if (result @@ -2162,33 +2280,108 @@ Local StatementExecutionHelper::Run(Environment* env, .IsNothing() || result->Set(env->context(), env->changes_string(), changes_val) .IsNothing()) { + return MaybeLocal(); + } + + return result; +} + +MaybeLocal StatementAsyncExecutionHelper::Run( + Environment* env, Statement* stmt) { + Isolate* isolate = env->isolate(); + Database* db = stmt->db_.get(); + sqlite3* conn = db->Connection(); + auto task = + [stmt, + conn]() -> std::variant> { + sqlite3_step(stmt->statement_); + int r = sqlite3_reset(stmt->statement_); + if (r != SQLITE_OK) { + return r; + } + + sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(conn); + sqlite3_int64 changes = sqlite3_changes64(conn); + + return std::make_tuple(last_insert_rowid, changes); + }; + + auto after = + [env, stmt, conn]( + std::variant> result, + Local resolver) { + if (std::holds_alternative(result)) { + Local e; + if (!CreateSQLiteError(env->isolate(), conn).ToLocal(&e)) { + return; + } + resolver->Reject(env->context(), e).FromJust(); + return; + } + + auto [last_insert_rowid, changes] = + std::get>(result); + + Local promise_result; + if (!StatementSQLiteToJSConverter::ConvertStatementRun( + env, stmt->use_big_ints_, changes, last_insert_rowid) + .ToLocal(&promise_result)) { + return; + } + + resolver->Resolve(env->context(), promise_result).FromJust(); + }; + + Local resolver = MakeSQLiteAsyncWork< + std::variant>>( + env, db, task, after); + if (resolver.IsEmpty()) { + return MaybeLocal(); + } + + return resolver; +} + +Local StatementExecutionHelper::Run(Environment* env, + Database* db, + sqlite3_stmt* stmt, + bool use_big_ints) { + Isolate* isolate = env->isolate(); + sqlite3_step(stmt); + int r = sqlite3_reset(stmt); + CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, Object::New(isolate)); + sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(db->Connection()); + sqlite3_int64 changes = sqlite3_changes64(db->Connection()); + Local result; + if (!StatementSQLiteToJSConverter::ConvertStatementRun( + env, use_big_ints, changes, last_insert_rowid) + .ToLocal(&result)) { return Object::New(isolate); } return result; } -BaseObjectPtr StatementExecutionHelper::Iterate( - Environment* env, BaseObjectPtr stmt) { +BaseObjectPtr StatementExecutionHelper::Iterate( + Environment* env, BaseObjectPtr stmt) { Local context = env->context(); Local global = context->Global(); Local js_iterator; Local js_iterator_prototype; if (!global->Get(context, env->iterator_string()).ToLocal(&js_iterator)) { - return BaseObjectPtr(); + return BaseObjectPtr(); } if (!js_iterator.As() ->Get(context, env->prototype_string()) .ToLocal(&js_iterator_prototype)) { - return BaseObjectPtr(); + return BaseObjectPtr(); } - BaseObjectPtr iter = - StatementSyncIterator::Create(env, stmt); + BaseObjectPtr iter = StatementIterator::Create(env, stmt); if (!iter) { // Error in iterator creation, likely already threw in Create - return BaseObjectPtr(); + return BaseObjectPtr(); } if (iter->object() @@ -2196,14 +2389,47 @@ BaseObjectPtr StatementExecutionHelper::Iterate( .As() ->SetPrototypeV2(context, js_iterator_prototype) .IsNothing()) { - return BaseObjectPtr(); + return BaseObjectPtr(); } return iter; } +// maybe this function can receive a callback so the client decides what to do +// with the result +// instead of returning a structure +std::optional SQLiteStatementExecutor::ExecuteGet( + sqlite3_stmt* stmt) { + auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt); }); + SQLiteResult result; + int r = sqlite3_step(stmt); + if (r == SQLITE_DONE) { + return std::nullopt; + } + if (r == SQLITE_ROW) { + int num_cols = sqlite3_column_count(stmt); + if (num_cols == 0) { + return std::nullopt; + } + + result.rows.reserve(1); + std::vector array_values; + for (int i = 0; i < num_cols; ++i) { + sqlite3_value* val = sqlite3_value_dup(sqlite3_column_value(stmt, i)); + array_values.emplace_back(val); + } + + result.code = r; + result.rows.emplace_back(std::move(array_values)); + return result; + }; + + result.code = r; + return result; +} + Local StatementExecutionHelper::Get(Environment* env, - DatabaseSync* db, + Database* db, sqlite3_stmt* stmt, bool return_arrays, bool use_big_ints) { @@ -2245,20 +2471,136 @@ Local StatementExecutionHelper::Get(Environment* env, } } -void StatementSync::All(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::All(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); Isolate* isolate = env->isolate(); + Database* db = stmt->db_.get(); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; } + if (db->is_async()) { + auto task = [stmt]() -> std::vector { + int num_cols = sqlite3_column_count(stmt->statement_); + int r = 0; + std::vector rows; + if (stmt->return_arrays_) { + while ((r = sqlite3_step(stmt->statement_)) == SQLITE_ROW) { + std::vector array_values; + for (int i = 0; i < num_cols; ++i) { + sqlite3_value* val = + sqlite3_value_dup(sqlite3_column_value(stmt->statement_, i)); + array_values.emplace_back(val); + } + rows.emplace_back(std::move(array_values)); + } + } else { + while ((r = sqlite3_step(stmt->statement_)) == SQLITE_ROW) { + RowObject object_values; + for (int i = 0; i < num_cols; ++i) { + const char* col_name = sqlite3_column_name(stmt->statement_, i); + sqlite3_value* val = + sqlite3_value_dup(sqlite3_column_value(stmt->statement_, i)); + object_values.emplace_back(std::string(col_name), val); + } + rows.emplace_back(std::move(object_values)); + } + } + + return rows; + }; + + auto after = [env, isolate, stmt](std::vector rows, + Local resolver) { + LocalVector js_rows(isolate); + int i = 0; + + for (auto& row : rows) { + if (std::holds_alternative(row)) { + auto& arr = std::get(row); + int num_cols = arr.size(); + LocalVector array_values(isolate); + array_values.reserve(num_cols); + for (sqlite3_value* sqlite_val : arr) { + MaybeLocal js_val; + SQLITE_VALUE_TO_JS( + value, isolate, stmt->use_big_ints_, js_val, sqlite_val); + if (js_val.IsEmpty()) { + return; + } + + Local v8Value; + if (!js_val.ToLocal(&v8Value)) { + return; + } + + array_values.emplace_back(v8Value); + } + + Local row_array = + Array::New(isolate, array_values.data(), array_values.size()); + js_rows.emplace_back(row_array); + } else { + auto& object = std::get(row); + int num_cols = object.size(); + LocalVector row_keys(isolate); + row_keys.reserve(num_cols); + LocalVector row_values(isolate); + row_values.reserve(num_cols); + for (auto& [key, sqlite_val] : object) { + Local key_name; + if (!String::NewFromUtf8(isolate, key.c_str()).ToLocal(&key_name)) { + return; + } + + row_keys.emplace_back(key_name); + + MaybeLocal js_val; + SQLITE_VALUE_TO_JS( + value, isolate, stmt->use_big_ints_, js_val, sqlite_val); + if (js_val.IsEmpty()) { + return; + } + + Local v8Value; + if (!js_val.ToLocal(&v8Value)) { + return; + } + + row_values.emplace_back(v8Value); + } + + DCHECK_EQ(row_keys.size(), row_values.size()); + Local row_obj = Object::New(isolate, + Null(isolate), + row_keys.data(), + row_values.data(), + num_cols); + js_rows.emplace_back(row_obj); + } + } + + resolver->Resolve(env->context(), + Array::New(isolate, js_rows.data(), js_rows.size())); + }; + + Local resolver = + MakeSQLiteAsyncWork>(env, db, task, after); + if (resolver.IsEmpty()) { + return; + } + + args.GetReturnValue().Set(resolver->GetPromise()); + return; + } + auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); args.GetReturnValue().Set(StatementExecutionHelper::All(env, stmt->db_.get(), @@ -2267,8 +2609,8 @@ void StatementSync::All(const FunctionCallbackInfo& args) { stmt->use_big_ints_)); } -void StatementSync::Iterate(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::Iterate(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2280,8 +2622,8 @@ void StatementSync::Iterate(const FunctionCallbackInfo& args) { return; } - BaseObjectPtr iter = StatementExecutionHelper::Iterate( - env, BaseObjectPtr(stmt)); + BaseObjectPtr iter = + StatementExecutionHelper::Iterate(env, BaseObjectPtr(stmt)); if (!iter) { return; @@ -2290,19 +2632,84 @@ void StatementSync::Iterate(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(iter->object()); } -void StatementSync::Get(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::Get(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); + Database* db = stmt->db_.get(); THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; } + if (db->is_async()) { + auto task = [stmt]() -> int { return sqlite3_step(stmt->statement_); }; + auto after = [env, stmt, db](int sqlite_status, + Local resolver) { + Isolate* isolate = env->isolate(); + if (sqlite_status == SQLITE_DONE) { + resolver->Resolve(env->context(), Undefined(isolate)).FromJust(); + return; + } + + if (sqlite_status != SQLITE_ROW) { + Local e; + if (!CreateSQLiteError(isolate, db->Connection()).ToLocal(&e)) { + return; + } + resolver->Reject(env->context(), e).FromJust(); + return; + } + + int num_cols = sqlite3_column_count(stmt->statement_); + if (num_cols == 0) { + resolver->Resolve(env->context(), Undefined(isolate)).FromJust(); + return; + } + + LocalVector row_values(isolate); + if (ExtractRowValues( + env, stmt->statement_, num_cols, db->use_big_ints(), &row_values) + .IsNothing()) { + resolver->Resolve(env->context(), Undefined(isolate)).FromJust(); + return; + } + + Local result; + if (stmt->return_arrays_) { + result = Array::New(isolate, row_values.data(), row_values.size()); + } else { + LocalVector keys(isolate); + keys.reserve(num_cols); + for (int i = 0; i < num_cols; ++i) { + MaybeLocal key = StatementExecutionHelper::ColumnNameToName( + env, stmt->statement_, i); + if (key.IsEmpty()) return; + keys.emplace_back(key.ToLocalChecked()); + } + + DCHECK_EQ(keys.size(), row_values.size()); + result = Object::New( + isolate, Null(isolate), keys.data(), row_values.data(), num_cols); + } + + resolver->Resolve(env->context(), result).FromJust(); + }; + + Local resolver = + MakeSQLiteAsyncWork(env, db, task, after); + if (resolver.IsEmpty()) { + return; + } + + args.GetReturnValue().Set(resolver->GetPromise()); + return; + } + args.GetReturnValue().Set(StatementExecutionHelper::Get(env, stmt->db_.get(), stmt->statement_, @@ -2310,25 +2717,34 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { stmt->use_big_ints_)); } -void StatementSync::Run(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::Run(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); + Database* db = stmt->db_.get(); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; } - args.GetReturnValue().Set(StatementExecutionHelper::Run( - env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_)); + if (!db->is_async()) { + args.GetReturnValue().Set(StatementExecutionHelper::Run( + env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_)); + return; + } + + Local resolver; + if (StatementAsyncExecutionHelper::Run(env, stmt).ToLocal(&resolver)) { + args.GetReturnValue().Set(resolver->GetPromise()); + } } -void StatementSync::Columns(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::Columns(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2370,8 +2786,8 @@ void StatementSync::Columns(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(Array::New(isolate, cols.data(), cols.size())); } -void StatementSync::SourceSQLGetter(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::SourceSQLGetter(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2384,8 +2800,8 @@ void StatementSync::SourceSQLGetter(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(sql); } -void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::ExpandedSQLGetter(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2406,9 +2822,9 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } -void StatementSync::SetAllowBareNamedParameters( +void Statement::SetAllowBareNamedParameters( const FunctionCallbackInfo& args) { - StatementSync* stmt; + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2424,9 +2840,9 @@ void StatementSync::SetAllowBareNamedParameters( stmt->allow_bare_named_params_ = args[0]->IsTrue(); } -void StatementSync::SetAllowUnknownNamedParameters( +void Statement::SetAllowUnknownNamedParameters( const FunctionCallbackInfo& args) { - StatementSync* stmt; + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2441,8 +2857,8 @@ void StatementSync::SetAllowUnknownNamedParameters( stmt->allow_unknown_named_params_ = args[0]->IsTrue(); } -void StatementSync::SetReadBigInts(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::SetReadBigInts(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2457,8 +2873,8 @@ void StatementSync::SetReadBigInts(const FunctionCallbackInfo& args) { stmt->use_big_ints_ = args[0]->IsTrue(); } -void StatementSync::SetReturnArrays(const FunctionCallbackInfo& args) { - StatementSync* stmt; +void Statement::SetReturnArrays(const FunctionCallbackInfo& args) { + Statement* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2479,7 +2895,7 @@ void IllegalConstructor(const FunctionCallbackInfo& args) { SQLTagStore::SQLTagStore(Environment* env, Local object, - BaseObjectWeakPtr database, + BaseObjectWeakPtr database, int capacity) : BaseObject(env, object), database_(std::move(database)), @@ -2528,7 +2944,7 @@ Local SQLTagStore::GetConstructorTemplate(Environment* env) { } BaseObjectPtr SQLTagStore::Create( - Environment* env, BaseObjectWeakPtr database, int capacity) { + Environment* env, BaseObjectWeakPtr database, int capacity) { Local obj; if (!GetConstructorTemplate(env) ->InstanceTemplate() @@ -2553,7 +2969,7 @@ void SQLTagStore::Run(const FunctionCallbackInfo& info) { THROW_AND_RETURN_ON_BAD_STATE( env, !session->database_->IsOpen(), "database is not open"); - BaseObjectPtr stmt = PrepareStatement(info); + BaseObjectPtr stmt = PrepareStatement(info); if (!stmt) { return; @@ -2582,7 +2998,7 @@ void SQLTagStore::Iterate(const FunctionCallbackInfo& args) { THROW_AND_RETURN_ON_BAD_STATE( env, !session->database_->IsOpen(), "database is not open"); - BaseObjectPtr stmt = PrepareStatement(args); + BaseObjectPtr stmt = PrepareStatement(args); if (!stmt) { return; @@ -2599,8 +3015,8 @@ void SQLTagStore::Iterate(const FunctionCallbackInfo& args) { } } - BaseObjectPtr iter = StatementExecutionHelper::Iterate( - env, BaseObjectPtr(stmt)); + BaseObjectPtr iter = + StatementExecutionHelper::Iterate(env, BaseObjectPtr(stmt)); if (!iter) { return; @@ -2617,7 +3033,7 @@ void SQLTagStore::Get(const FunctionCallbackInfo& args) { THROW_AND_RETURN_ON_BAD_STATE( env, !session->database_->IsOpen(), "database is not open"); - BaseObjectPtr stmt = PrepareStatement(args); + BaseObjectPtr stmt = PrepareStatement(args); if (!stmt) { return; @@ -2652,7 +3068,7 @@ void SQLTagStore::All(const FunctionCallbackInfo& args) { THROW_AND_RETURN_ON_BAD_STATE( env, !session->database_->IsOpen(), "database is not open"); - BaseObjectPtr stmt = PrepareStatement(args); + BaseObjectPtr stmt = PrepareStatement(args); if (!stmt) { return; @@ -2700,14 +3116,14 @@ void SQLTagStore::Clear(const FunctionCallbackInfo& info) { store->sql_tags_.Clear(); } -BaseObjectPtr SQLTagStore::PrepareStatement( +BaseObjectPtr SQLTagStore::PrepareStatement( const FunctionCallbackInfo& args) { SQLTagStore* session = BaseObject::FromJSObject(args.This()); if (!session) { THROW_ERR_INVALID_ARG_TYPE( Environment::GetCurrent(args)->isolate(), "This method can only be called on SQLTagStore instances."); - return BaseObjectPtr(); + return BaseObjectPtr(); } Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); @@ -2717,7 +3133,7 @@ BaseObjectPtr SQLTagStore::PrepareStatement( THROW_ERR_INVALID_ARG_TYPE( isolate, "First argument must be an array of strings (template literal)."); - return BaseObjectPtr(); + return BaseObjectPtr(); } Local strings = args[0].As(); @@ -2730,7 +3146,7 @@ BaseObjectPtr SQLTagStore::PrepareStatement( if (!strings->Get(context, i).ToLocal(&str_val) || !str_val->IsString()) { THROW_ERR_INVALID_ARG_TYPE(isolate, "Template literal parts must be strings."); - return BaseObjectPtr(); + return BaseObjectPtr(); } Utf8Value part(isolate, str_val); sql += *part; @@ -2739,7 +3155,7 @@ BaseObjectPtr SQLTagStore::PrepareStatement( } } - BaseObjectPtr stmt = nullptr; + BaseObjectPtr stmt = nullptr; if (session->sql_tags_.Exists(sql)) { stmt = session->sql_tags_.Get(sql); if (stmt->IsFinalized()) { @@ -2760,16 +3176,16 @@ BaseObjectPtr SQLTagStore::PrepareStatement( if (r != SQLITE_OK) { THROW_ERR_SQLITE_ERROR(isolate, "Failed to prepare statement"); sqlite3_finalize(s); - return BaseObjectPtr(); + return BaseObjectPtr(); } - BaseObjectPtr stmt_obj = StatementSync::Create( - env, BaseObjectPtr(session->database_), s); + BaseObjectPtr stmt_obj = + Statement::Create(env, BaseObjectPtr(session->database_), s); if (!stmt_obj) { - THROW_ERR_SQLITE_ERROR(isolate, "Failed to create StatementSync"); + THROW_ERR_SQLITE_ERROR(isolate, "Failed to create Statement"); sqlite3_finalize(s); - return BaseObjectPtr(); + return BaseObjectPtr(); } session->sql_tags_.Put(sql, stmt_obj); @@ -2790,103 +3206,112 @@ void SQLTagStore::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("sql_tags_cache", cache_content_size); } -Local StatementSync::GetConstructorTemplate( - Environment* env) { +inline Local GetStatementConstructorTemplate( + Isolate* isolate) { Local tmpl = - env->sqlite_statement_sync_constructor_template(); - if (tmpl.IsEmpty()) { - Isolate* isolate = env->isolate(); - tmpl = NewFunctionTemplate(isolate, IllegalConstructor); - tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "StatementSync")); - tmpl->InstanceTemplate()->SetInternalFieldCount( - StatementSync::kInternalFieldCount); - SetProtoMethod(isolate, tmpl, "iterate", StatementSync::Iterate); - SetProtoMethod(isolate, tmpl, "all", StatementSync::All); - SetProtoMethod(isolate, tmpl, "get", StatementSync::Get); - SetProtoMethod(isolate, tmpl, "run", StatementSync::Run); - SetProtoMethodNoSideEffect( - isolate, tmpl, "columns", StatementSync::Columns); - SetSideEffectFreeGetter(isolate, - tmpl, - FIXED_ONE_BYTE_STRING(isolate, "sourceSQL"), - StatementSync::SourceSQLGetter); - SetSideEffectFreeGetter(isolate, - tmpl, - FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"), - StatementSync::ExpandedSQLGetter); - SetProtoMethod(isolate, - tmpl, - "setAllowBareNamedParameters", - StatementSync::SetAllowBareNamedParameters); - SetProtoMethod(isolate, - tmpl, - "setAllowUnknownNamedParameters", - StatementSync::SetAllowUnknownNamedParameters); - SetProtoMethod( - isolate, tmpl, "setReadBigInts", StatementSync::SetReadBigInts); - SetProtoMethod( - isolate, tmpl, "setReturnArrays", StatementSync::SetReturnArrays); - env->set_sqlite_statement_sync_constructor_template(tmpl); + NewFunctionTemplate(isolate, IllegalConstructor); + tmpl->InstanceTemplate()->SetInternalFieldCount( + Statement::kInternalFieldCount); + SetProtoMethod(isolate, tmpl, "iterate", Statement::Iterate); + SetProtoMethod(isolate, tmpl, "all", Statement::All); + SetProtoMethod(isolate, tmpl, "get", Statement::Get); + SetProtoMethod(isolate, tmpl, "run", Statement::Run); + SetProtoMethodNoSideEffect(isolate, tmpl, "columns", Statement::Columns); + SetSideEffectFreeGetter(isolate, + tmpl, + FIXED_ONE_BYTE_STRING(isolate, "sourceSQL"), + Statement::SourceSQLGetter); + SetSideEffectFreeGetter(isolate, + tmpl, + FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"), + Statement::ExpandedSQLGetter); + SetProtoMethod(isolate, + tmpl, + "setAllowBareNamedParameters", + Statement::SetAllowBareNamedParameters); + SetProtoMethod(isolate, + tmpl, + "setAllowUnknownNamedParameters", + Statement::SetAllowUnknownNamedParameters); + SetProtoMethod(isolate, tmpl, "setReadBigInts", Statement::SetReadBigInts); + SetProtoMethod(isolate, tmpl, "setReturnArrays", Statement::SetReturnArrays); + + return tmpl; +} + +Local Statement::GetConstructorTemplate( + Environment* env, std::string_view name = "Statement") { + Isolate* isolate = env->isolate(); + Local tmpl = GetStatementConstructorTemplate(isolate); + Local class_name; + if (!String::NewFromUtf8(isolate, name.data()).ToLocal(&class_name)) { + return Local(); } + tmpl->SetClassName(class_name); return tmpl; } -BaseObjectPtr StatementSync::Create( - Environment* env, BaseObjectPtr db, sqlite3_stmt* stmt) { +BaseObjectPtr Statement::Create(Environment* env, + BaseObjectPtr db, + sqlite3_stmt* stmt) { + Local constructor_template = + db.get()->is_async() ? env->sqlite_statement_async_constructor_template() + : env->sqlite_statement_sync_constructor_template(); + Local obj; - if (!GetConstructorTemplate(env) - ->InstanceTemplate() + if (constructor_template.IsEmpty() || + !constructor_template->InstanceTemplate() ->NewInstance(env->context()) .ToLocal(&obj)) { return nullptr; } - return MakeBaseObject(env, obj, std::move(db), stmt); + return MakeBaseObject(env, obj, std::move(db), stmt); } -StatementSyncIterator::StatementSyncIterator(Environment* env, - Local object, - BaseObjectPtr stmt) +StatementIterator::StatementIterator(Environment* env, + Local object, + BaseObjectPtr stmt) : BaseObject(env, object), stmt_(std::move(stmt)) { MakeWeak(); done_ = false; } -StatementSyncIterator::~StatementSyncIterator() {} -void StatementSyncIterator::MemoryInfo(MemoryTracker* tracker) const {} +StatementIterator::~StatementIterator() {} +void StatementIterator::MemoryInfo(MemoryTracker* tracker) const {} -Local StatementSyncIterator::GetConstructorTemplate( +Local StatementIterator::GetConstructorTemplate( Environment* env) { Local tmpl = env->sqlite_statement_sync_iterator_constructor_template(); if (tmpl.IsEmpty()) { Isolate* isolate = env->isolate(); tmpl = NewFunctionTemplate(isolate, IllegalConstructor); - tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "StatementSyncIterator")); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "StatementIterator")); tmpl->InstanceTemplate()->SetInternalFieldCount( - StatementSync::kInternalFieldCount); - SetProtoMethod(isolate, tmpl, "next", StatementSyncIterator::Next); - SetProtoMethod(isolate, tmpl, "return", StatementSyncIterator::Return); + Statement::kInternalFieldCount); + SetProtoMethod(isolate, tmpl, "next", StatementIterator::Next); + SetProtoMethod(isolate, tmpl, "return", StatementIterator::Return); env->set_sqlite_statement_sync_iterator_constructor_template(tmpl); } return tmpl; } -BaseObjectPtr StatementSyncIterator::Create( - Environment* env, BaseObjectPtr stmt) { +BaseObjectPtr StatementIterator::Create( + Environment* env, BaseObjectPtr stmt) { Local obj; if (!GetConstructorTemplate(env) ->InstanceTemplate() ->NewInstance(env->context()) .ToLocal(&obj)) { - return BaseObjectPtr(); + return BaseObjectPtr(); } - return MakeBaseObject(env, obj, std::move(stmt)); + return MakeBaseObject(env, obj, std::move(stmt)); } -void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { - StatementSyncIterator* iter; +void StatementIterator::Next(const FunctionCallbackInfo& args) { + StatementIterator* iter; ASSIGN_OR_RETURN_UNWRAP(&iter, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2959,8 +3384,8 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { } } -void StatementSyncIterator::Return(const FunctionCallbackInfo& args) { - StatementSyncIterator* iter; +void StatementIterator::Return(const FunctionCallbackInfo& args) { + StatementIterator* iter; ASSIGN_OR_RETURN_UNWRAP(&iter, args.This()); Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( @@ -2982,7 +3407,7 @@ void StatementSyncIterator::Return(const FunctionCallbackInfo& args) { Session::Session(Environment* env, Local object, - BaseObjectWeakPtr database, + BaseObjectWeakPtr database, sqlite3_session* session) : BaseObject(env, object), session_(session), @@ -2995,7 +3420,7 @@ Session::~Session() { } BaseObjectPtr Session::Create(Environment* env, - BaseObjectWeakPtr database, + BaseObjectWeakPtr database, sqlite3_session* session) { Local obj; if (!GetConstructorTemplate(env) @@ -3095,62 +3520,123 @@ void DefineConstants(Local target) { NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_FOREIGN_KEY); } +void DefineStatementMethods(Isolate* isolate, Local tmpl) { + SetProtoMethod(isolate, tmpl, "iterate", Statement::Iterate); + SetProtoMethod(isolate, tmpl, "all", Statement::All); + SetProtoMethod(isolate, tmpl, "get", Statement::Get); + SetProtoMethod(isolate, tmpl, "run", Statement::Run); + SetProtoMethodNoSideEffect(isolate, tmpl, "columns", Statement::Columns); + SetSideEffectFreeGetter(isolate, + tmpl, + FIXED_ONE_BYTE_STRING(isolate, "sourceSQL"), + Statement::SourceSQLGetter); + SetSideEffectFreeGetter(isolate, + tmpl, + FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"), + Statement::ExpandedSQLGetter); + SetProtoMethod(isolate, + tmpl, + "setAllowBareNamedParameters", + Statement::SetAllowBareNamedParameters); + SetProtoMethod(isolate, + tmpl, + "setAllowUnknownNamedParameters", + Statement::SetAllowUnknownNamedParameters); + SetProtoMethod(isolate, tmpl, "setReadBigInts", Statement::SetReadBigInts); + SetProtoMethod(isolate, tmpl, "setReturnArrays", Statement::SetReturnArrays); +} + +inline void DefineStatementFunctionTemplates(Environment* env) { + Isolate* isolate = env->isolate(); + Local sync_tmpl = + env->sqlite_statement_sync_constructor_template(); + if (sync_tmpl.IsEmpty()) { + sync_tmpl = NewFunctionTemplate(isolate, IllegalConstructor); + sync_tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "StatementSync")); + sync_tmpl->InstanceTemplate()->SetInternalFieldCount( + Statement::kInternalFieldCount); + DefineStatementMethods(isolate, sync_tmpl); + env->set_sqlite_statement_sync_constructor_template(sync_tmpl); + } + + Local async_tmpl = + env->sqlite_statement_async_constructor_template(); + if (async_tmpl.IsEmpty()) { + async_tmpl = NewFunctionTemplate(isolate, IllegalConstructor); + async_tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Statement")); + async_tmpl->InstanceTemplate()->SetInternalFieldCount( + Statement::kInternalFieldCount); + DefineStatementMethods(isolate, async_tmpl); + env->set_sqlite_statement_async_constructor_template(async_tmpl); + } +} + +inline void DefineAsyncInterface(Isolate* isolate, + Local target, + Local context) { + Local db_async_tmpl = + NewFunctionTemplate(isolate, Database::NewAsync); + db_async_tmpl->InstanceTemplate()->SetInternalFieldCount( + Database::kInternalFieldCount); + + SetProtoMethod(isolate, db_async_tmpl, "close", Database::Close); + SetProtoMethod(isolate, db_async_tmpl, "prepare", Database::Prepare); + SetProtoMethod(isolate, db_async_tmpl, "exec", Database::Exec); + SetConstructorFunction(context, target, "Database", db_async_tmpl); +} + static void Initialize(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); Isolate* isolate = env->isolate(); - Local db_tmpl = - NewFunctionTemplate(isolate, DatabaseSync::New); + Local db_tmpl = NewFunctionTemplate(isolate, Database::New); db_tmpl->InstanceTemplate()->SetInternalFieldCount( - DatabaseSync::kInternalFieldCount); + Database::kInternalFieldCount); Local constants = Object::New(isolate); DefineConstants(constants); - - SetProtoMethod(isolate, db_tmpl, "open", DatabaseSync::Open); - SetProtoMethod(isolate, db_tmpl, "close", DatabaseSync::Close); - SetProtoDispose(isolate, db_tmpl, DatabaseSync::Dispose); - SetProtoMethod(isolate, db_tmpl, "prepare", DatabaseSync::Prepare); - SetProtoMethod(isolate, db_tmpl, "exec", DatabaseSync::Exec); - SetProtoMethod(isolate, db_tmpl, "function", DatabaseSync::CustomFunction); - SetProtoMethod( - isolate, db_tmpl, "createTagStore", DatabaseSync::CreateTagStore); - SetProtoMethodNoSideEffect( - isolate, db_tmpl, "location", DatabaseSync::Location); + DefineAsyncInterface(isolate, target, context); + DefineStatementFunctionTemplates(env); + + SetProtoMethod(isolate, db_tmpl, "open", Database::Open); + SetProtoMethod(isolate, db_tmpl, "close", Database::Close); + SetProtoDispose(isolate, db_tmpl, Database::Dispose); + SetProtoMethod(isolate, db_tmpl, "prepare", Database::Prepare); + SetProtoMethod(isolate, db_tmpl, "exec", Database::Exec); + SetProtoMethod(isolate, db_tmpl, "function", Database::CustomFunction); + SetProtoMethod(isolate, db_tmpl, "createTagStore", Database::CreateTagStore); + SetProtoMethodNoSideEffect(isolate, db_tmpl, "location", Database::Location); + SetProtoMethod(isolate, db_tmpl, "aggregate", Database::AggregateFunction); + SetProtoMethod(isolate, db_tmpl, "createSession", Database::CreateSession); + SetProtoMethod(isolate, db_tmpl, "applyChangeset", Database::ApplyChangeset); SetProtoMethod( - isolate, db_tmpl, "aggregate", DatabaseSync::AggregateFunction); - SetProtoMethod( - isolate, db_tmpl, "createSession", DatabaseSync::CreateSession); - SetProtoMethod( - isolate, db_tmpl, "applyChangeset", DatabaseSync::ApplyChangeset); - SetProtoMethod(isolate, - db_tmpl, - "enableLoadExtension", - DatabaseSync::EnableLoadExtension); - SetProtoMethod( - isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension); + isolate, db_tmpl, "enableLoadExtension", Database::EnableLoadExtension); + SetProtoMethod(isolate, db_tmpl, "loadExtension", Database::LoadExtension); SetSideEffectFreeGetter(isolate, db_tmpl, FIXED_ONE_BYTE_STRING(isolate, "isOpen"), - DatabaseSync::IsOpenGetter); + Database::IsOpenGetter); SetSideEffectFreeGetter(isolate, db_tmpl, FIXED_ONE_BYTE_STRING(isolate, "isTransaction"), - DatabaseSync::IsTransactionGetter); + Database::IsTransactionGetter); Local sqlite_type_key = FIXED_ONE_BYTE_STRING(isolate, "sqlite-type"); Local sqlite_type_symbol = v8::Symbol::For(isolate, sqlite_type_key); Local database_sync_string = FIXED_ONE_BYTE_STRING(isolate, "node:sqlite"); db_tmpl->InstanceTemplate()->Set(sqlite_type_symbol, database_sync_string); - SetConstructorFunction(context, target, "DatabaseSync", db_tmpl); SetConstructorFunction(context, target, "StatementSync", - StatementSync::GetConstructorTemplate(env)); + env->sqlite_statement_sync_constructor_template()); + SetConstructorFunction(context, + target, + "Statement", + env->sqlite_statement_async_constructor_template()); SetConstructorFunction( context, target, "Session", Session::GetConstructorTemplate(env)); diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 2fb29053ead743..c11d7b684424dd 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -7,6 +7,7 @@ #include "lru_cache-inl.h" #include "node_mem.h" #include "sqlite3.h" +#include "threadpoolwork-inl.h" #include "util.h" #include @@ -23,6 +24,10 @@ class DatabaseOpenConfiguration { inline const std::string& location() const { return location_; } + inline bool get_async() const { return async_; } + + inline void set_async(bool flag) { async_ = flag; } + inline bool get_read_only() const { return read_only_; } inline void set_read_only(bool flag) { read_only_ = flag; } @@ -67,6 +72,7 @@ class DatabaseOpenConfiguration { private: std::string location_; + bool async_ = true; bool read_only_ = false; bool enable_foreign_keys_ = true; bool enable_dqs_ = false; @@ -77,24 +83,59 @@ class DatabaseOpenConfiguration { bool allow_unknown_named_params_ = false; }; -class DatabaseSync; -class StatementSyncIterator; -class StatementSync; +class Database; +class StatementIterator; +class Statement; class BackupJob; +using RowArray = std::vector; +using RowObject = std::vector>; +using Row = std::variant; + +struct SQLiteResult { + int code; + std::vector rows; +}; + +class StatementSQLiteToJSConverter { + public: + static v8::MaybeLocal ConvertStatementRun( + Environment* env, + bool use_big_ints, + sqlite3_int64 changes, + sqlite3_int64 last_insert_rowid); + // static v8::Local ConvertAll(Environment* env, + // const QueryResult& result, + // bool return_arrays); + // static v8::Local ConvertGet(Environment* env, + // sqlite3_stmt* stmt, + // bool use_big_ints); +}; + +class SQLiteStatementExecutor { + public: + static std::optional ExecuteGet(sqlite3_stmt* stmt); +}; + +class StatementAsyncExecutionHelper { + public: + static v8::MaybeLocal Run(Environment* env, + Statement* stmt); +}; + class StatementExecutionHelper { public: static v8::Local All(Environment* env, - DatabaseSync* db, + Database* db, sqlite3_stmt* stmt, bool return_arrays, bool use_big_ints); static v8::Local Run(Environment* env, - DatabaseSync* db, + Database* db, sqlite3_stmt* stmt, bool use_big_ints); - static BaseObjectPtr Iterate( - Environment* env, BaseObjectPtr stmt); + static BaseObjectPtr Iterate( + Environment* env, BaseObjectPtr stmt); static v8::MaybeLocal ColumnToValue(Environment* env, sqlite3_stmt* stmt, const int column, @@ -103,21 +144,22 @@ class StatementExecutionHelper { sqlite3_stmt* stmt, const int column); static v8::Local Get(Environment* env, - DatabaseSync* db, + Database* db, sqlite3_stmt* stmt, bool return_arrays, bool use_big_ints); }; -class DatabaseSync : public BaseObject { +class Database : public BaseObject { public: - DatabaseSync(Environment* env, - v8::Local object, - DatabaseOpenConfiguration&& open_config, - bool open, - bool allow_load_extension); + Database(Environment* env, + v8::Local object, + DatabaseOpenConfiguration&& open_config, + bool open, + bool allow_load_extension); void MemoryInfo(MemoryTracker* tracker) const override; static void New(const v8::FunctionCallbackInfo& args); + static void NewAsync(const v8::FunctionCallbackInfo& args); static void Open(const v8::FunctionCallbackInfo& args); static void IsOpenGetter(const v8::FunctionCallbackInfo& args); static void IsTransactionGetter( @@ -139,9 +181,12 @@ class DatabaseSync : public BaseObject { void FinalizeStatements(); void RemoveBackup(BackupJob* backup); void AddBackup(BackupJob* backup); + void AddAsyncTask(ThreadPoolWork* async_task); + void RemoveAsyncTask(ThreadPoolWork* async_task); void FinalizeBackups(); - void UntrackStatement(StatementSync* statement); + void UntrackStatement(Statement* statement); bool IsOpen(); + bool is_async() { return open_config_.get_async(); } bool use_big_ints() const { return open_config_.get_use_big_ints(); } bool return_arrays() const { return open_config_.get_return_arrays(); } bool allow_bare_named_params() const { @@ -159,41 +204,43 @@ class DatabaseSync : public BaseObject { void SetIgnoreNextSQLiteError(bool ignore); bool ShouldIgnoreSQLiteError(); - SET_MEMORY_INFO_NAME(DatabaseSync) - SET_SELF_SIZE(DatabaseSync) + SET_MEMORY_INFO_NAME(Database) + SET_SELF_SIZE(Database) private: bool Open(); void DeleteSessions(); - ~DatabaseSync() override; + ~Database() override; DatabaseOpenConfiguration open_config_; bool allow_load_extension_; bool enable_load_extension_; sqlite3* connection_; bool ignore_next_sqlite_error_; + std::set async_tasks_; std::set backups_; std::set sessions_; - std::unordered_set statements_; + std::unordered_set statements_; friend class Session; friend class SQLTagStore; friend class StatementExecutionHelper; + friend class StatementAsyncExecutionHelper; }; -class StatementSync : public BaseObject { +class Statement : public BaseObject { public: - StatementSync(Environment* env, - v8::Local object, - BaseObjectPtr db, - sqlite3_stmt* stmt); + Statement(Environment* env, + v8::Local object, + BaseObjectPtr db, + sqlite3_stmt* stmt); void MemoryInfo(MemoryTracker* tracker) const override; static v8::Local GetConstructorTemplate( - Environment* env); - static BaseObjectPtr Create(Environment* env, - BaseObjectPtr db, - sqlite3_stmt* stmt); + Environment* env, std::string_view class_name); + static BaseObjectPtr Create(Environment* env, + BaseObjectPtr db, + sqlite3_stmt* stmt); static void All(const v8::FunctionCallbackInfo& args); static void Iterate(const v8::FunctionCallbackInfo& args); static void Get(const v8::FunctionCallbackInfo& args); @@ -213,12 +260,12 @@ class StatementSync : public BaseObject { void Finalize(); bool IsFinalized(); - SET_MEMORY_INFO_NAME(StatementSync) - SET_SELF_SIZE(StatementSync) + SET_MEMORY_INFO_NAME(Statement) + SET_SELF_SIZE(Statement) private: - ~StatementSync() override; - BaseObjectPtr db_; + ~Statement() override; + BaseObjectPtr db_; sqlite3_stmt* statement_; bool return_arrays_ = false; bool use_big_ints_; @@ -228,30 +275,31 @@ class StatementSync : public BaseObject { bool BindParams(const v8::FunctionCallbackInfo& args); bool BindValue(const v8::Local& value, const int index); - friend class StatementSyncIterator; + friend class StatementIterator; friend class SQLTagStore; friend class StatementExecutionHelper; + friend class StatementAsyncExecutionHelper; }; -class StatementSyncIterator : public BaseObject { +class StatementIterator : public BaseObject { public: - StatementSyncIterator(Environment* env, - v8::Local object, - BaseObjectPtr stmt); + StatementIterator(Environment* env, + v8::Local object, + BaseObjectPtr stmt); void MemoryInfo(MemoryTracker* tracker) const override; static v8::Local GetConstructorTemplate( Environment* env); - static BaseObjectPtr Create( - Environment* env, BaseObjectPtr stmt); + static BaseObjectPtr Create(Environment* env, + BaseObjectPtr stmt); static void Next(const v8::FunctionCallbackInfo& args); static void Return(const v8::FunctionCallbackInfo& args); - SET_MEMORY_INFO_NAME(StatementSyncIterator) - SET_SELF_SIZE(StatementSyncIterator) + SET_MEMORY_INFO_NAME(StatementIterator) + SET_SELF_SIZE(StatementIterator) private: - ~StatementSyncIterator() override; - BaseObjectPtr stmt_; + ~StatementIterator() override; + BaseObjectPtr stmt_; bool done_; }; @@ -261,7 +309,7 @@ class Session : public BaseObject { public: Session(Environment* env, v8::Local object, - BaseObjectWeakPtr database, + BaseObjectWeakPtr database, sqlite3_session* session); ~Session() override; template @@ -271,7 +319,7 @@ class Session : public BaseObject { static v8::Local GetConstructorTemplate( Environment* env); static BaseObjectPtr Create(Environment* env, - BaseObjectWeakPtr database, + BaseObjectWeakPtr database, sqlite3_session* session); void MemoryInfo(MemoryTracker* tracker) const override; @@ -281,18 +329,19 @@ class Session : public BaseObject { private: void Delete(); sqlite3_session* session_; - BaseObjectWeakPtr database_; // The Parent Database + BaseObjectWeakPtr database_; // The Parent Database }; class SQLTagStore : public BaseObject { public: SQLTagStore(Environment* env, v8::Local object, - BaseObjectWeakPtr database, + BaseObjectWeakPtr database, int capacity); ~SQLTagStore() override; - static BaseObjectPtr Create( - Environment* env, BaseObjectWeakPtr database, int capacity); + static BaseObjectPtr Create(Environment* env, + BaseObjectWeakPtr database, + int capacity); static v8::Local GetConstructorTemplate( Environment* env); static void All(const v8::FunctionCallbackInfo& info); @@ -309,10 +358,10 @@ class SQLTagStore : public BaseObject { SET_SELF_SIZE(SQLTagStore) private: - static BaseObjectPtr PrepareStatement( + static BaseObjectPtr PrepareStatement( const v8::FunctionCallbackInfo& args); - BaseObjectWeakPtr database_; - LRUCache> sql_tags_; + BaseObjectWeakPtr database_; + LRUCache> sql_tags_; int capacity_; friend class StatementExecutionHelper; }; @@ -321,7 +370,7 @@ class UserDefinedFunction { public: UserDefinedFunction(Environment* env, v8::Local fn, - DatabaseSync* db, + Database* db, bool use_bigint_args); ~UserDefinedFunction(); static void xFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv); @@ -330,7 +379,7 @@ class UserDefinedFunction { private: Environment* env_; v8::Global fn_; - DatabaseSync* db_; + Database* db_; bool use_bigint_args_; }; diff --git a/test/parallel/test-sqlite-database-async.mjs b/test/parallel/test-sqlite-database-async.mjs new file mode 100644 index 00000000000000..20dc3a4587920e --- /dev/null +++ b/test/parallel/test-sqlite-database-async.mjs @@ -0,0 +1,71 @@ +import { skipIfSQLiteMissing } from '../common/index.mjs'; +import tmpdir from '../common/tmpdir.js'; +import { suite, test } from 'node:test'; +import { join } from 'node:path'; +import { Database } from 'node:sqlite'; +skipIfSQLiteMissing(); + +tmpdir.refresh(); + +let cnt = 0; +function nextDb() { + return join(tmpdir.path, `database-${cnt++}.db`); +} + +suite('Database.prototype.exec()', () => { + test('executes SQL', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const result = await db.exec(` + CREATE TABLE data( + key INTEGER PRIMARY KEY, + val INTEGER + ) STRICT; + INSERT INTO data (key, val) VALUES (1, 2); + INSERT INTO data (key, val) VALUES (8, 9); + `); + t.assert.strictEqual(result, undefined); + }); + + test('reports errors from SQLite', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + + await t.assert.rejects(db.exec('CREATE TABLEEEE'), { + code: 'ERR_SQLITE_ERROR', + message: /syntax error/, + }); + }); + + test('throws if the URL does not have the file: scheme', (t) => { + t.assert.throws(() => { + new Database(new URL('http://example.com')); + }, { + code: 'ERR_INVALID_URL_SCHEME', + message: 'The URL must be of scheme file:', + }); + }); + + test('throws if database is not open', (t) => { + const db = new Database(nextDb(), { open: false }); + + t.assert.throws(() => { + db.exec(); + }, { + code: 'ERR_INVALID_STATE', + message: /database is not open/, + }); + }); + + test('throws if sql is not a string', (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + + t.assert.throws(() => { + db.exec(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "sql" argument must be a string/, + }); + }); +}); diff --git a/test/parallel/test-sqlite-statement-async.js b/test/parallel/test-sqlite-statement-async.js new file mode 100644 index 00000000000000..d773a6f7612387 --- /dev/null +++ b/test/parallel/test-sqlite-statement-async.js @@ -0,0 +1,124 @@ +'use strict'; +const { skipIfSQLiteMissing } = require('../common'); +skipIfSQLiteMissing(); +const tmpdir = require('../common/tmpdir'); +const { join } = require('node:path'); +const { Database, Statement } = require('node:sqlite'); +const { suite, test } = require('node:test'); +let cnt = 0; + +tmpdir.refresh(); + +function nextDb() { + return join(tmpdir.path, `database-${cnt++}.db`); +} + +suite('Statement() constructor', () => { + test('Statement cannot be constructed directly', (t) => { + t.assert.throws(() => { + new Statement(); + }, { + code: 'ERR_ILLEGAL_CONSTRUCTOR', + message: /Illegal constructor/, + }); + }); +}); + +suite('Statement.prototype.run()', () => { + test('executes a query and returns change metadata', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec(` + CREATE TABLE storage(key TEXT, val TEXT); + INSERT INTO storage (key, val) VALUES ('foo', 'bar'); + `); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('SELECT * FROM storage'); + const r = await stmt.run(); + t.assert.deepStrictEqual(r, { changes: 1, lastInsertRowid: 1 }); + }); + + test('SQLite throws when trying to bind too many parameters', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)'); + t.assert.rejects(async () => { + await stmt.run(1, 2, 3); + }, { + code: 'ERR_SQLITE_ERROR', + message: 'column index out of range', + errcode: 25, + errstr: 'column index out of range', + }); + }); + + test('SQLite defaults to NULL for unbound parameters', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)'); + await t.assert.rejects(async () => { + await stmt.run(1); + }, { + code: 'ERR_SQLITE_ERROR', + message: 'NOT NULL constraint failed: data.val', + errcode: 1299, + errstr: 'constraint failed', + }); + }); + + test('returns correct metadata when using RETURNING', async (t) => { + const db = new Database(':memory:'); + const setup = await db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const sql = 'INSERT INTO data (key, val) VALUES ($k, $v) RETURNING key'; + const stmt = db.prepare(sql); + let r = await stmt.run({ k: 1, v: 10 }); + t.assert.deepStrictEqual(r, { changes: 1, lastInsertRowid: 1 }); + + r = await stmt.run({ k: 2, v: 20 }); + t.assert.deepStrictEqual(r, { changes: 1, lastInsertRowid: 2 }); + + r = await stmt.run({ k: 3, v: 30 }); + t.assert.deepStrictEqual(r, { changes: 1, lastInsertRowid: 3 }); + }); +}); + +suite('Statement.prototype.get()', () => { + test('executes a query and returns undefined on no results', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + t.assert.strictEqual(await stmt.get(), undefined); + stmt = db.prepare('SELECT * FROM storage'); + t.assert.strictEqual(await stmt.get(), undefined); + }); + + test('executes a query and returns the first result', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + t.assert.strictEqual(await stmt.get(), undefined); + stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); + t.assert.strictEqual(await stmt.get('key1', 'val1'), undefined); + t.assert.strictEqual(await stmt.get('key2', 'val2'), undefined); + stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + t.assert.deepStrictEqual(await stmt.get(), { __proto__: null, key: 'key1', val: 'val1' }); + }); + + test('executes a query that returns special columns', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const stmt = db.prepare('SELECT 1 as __proto__, 2 as constructor, 3 as toString'); + t.assert.deepStrictEqual(await stmt.get(), { __proto__: null, ['__proto__']: 1, constructor: 2, toString: 3 }); + }); +});