diff --git a/deps/sqlite/sqlite.gyp b/deps/sqlite/sqlite.gyp index 72b36fb0df760d..89541a9db9eaa7 100644 --- a/deps/sqlite/sqlite.gyp +++ b/deps/sqlite/sqlite.gyp @@ -25,6 +25,7 @@ 'SQLITE_ENABLE_RBU', 'SQLITE_ENABLE_RTREE', 'SQLITE_ENABLE_SESSION', + 'SQLITE_THREADSAFE=2', ], 'include_dirs': ['.'], 'sources': [ diff --git a/deps/sqlite/unofficial.gni b/deps/sqlite/unofficial.gni index dae3f36bde23db..2e1c66fe4c4814 100644 --- a/deps/sqlite/unofficial.gni +++ b/deps/sqlite/unofficial.gni @@ -19,6 +19,7 @@ template("sqlite_gn_build") { "SQLITE_ENABLE_RBU", "SQLITE_ENABLE_RTREE", "SQLITE_ENABLE_SESSION", + "SQLITE_THREADSAFE=2", ] } diff --git a/src/env_properties.h b/src/env_properties.h index 903158ebbdc2b7..fe6aade8b28a59 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -435,6 +435,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(srv_record_template, v8::DictionaryTemplate) \ diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 0b111ce1fb1d59..11764d0d461baf 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -220,9 +220,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; @@ -269,7 +269,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, @@ -433,7 +433,7 @@ class CustomAggregate { } Environment* env_; - DatabaseSync* db_; + Database* db_; bool use_bigint_args_; Global start_; Global step_fn_; @@ -441,10 +441,58 @@ 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(); + } + db_->ProcessNextAsyncTask(); + } + + void Finalize() { db_->RemoveAsyncTask(this); } + + private: + Environment* env_; + Database* db_; + Global resolver_; + std::function work_ = nullptr; + std::function)> after_ = nullptr; + T result_; +}; + +// TODO(geeksilva97): Replace BackupJob usage with SQLiteAsyncTask class BackupJob : public ThreadPoolWork { public: explicit BackupJob(Environment* env, - DatabaseSync* source, + Database* source, Local resolver, std::string source_db, std::string destination_name, @@ -592,7 +640,7 @@ class BackupJob : public ThreadPoolWork { Environment* env() const { return env_; } Environment* env_; - DatabaseSync* source_; + Database* source_; Global resolver_; Global progressFunc_; sqlite3* dest_ = nullptr; @@ -606,7 +654,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), @@ -665,11 +713,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; @@ -682,15 +730,42 @@ 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::ScheduleAsyncTask(ThreadPoolWork* work) { + if (has_running_task_) { + task_queue_.push(work); + } else { + has_running_task_ = true; + work->ScheduleWork(); + } +} + +void Database::ProcessNextAsyncTask() { + if (task_queue_.empty()) { + has_running_task_ = false; + } else { + ThreadPoolWork* next_work = task_queue_.front(); + task_queue_.pop(); + next_work->ScheduleWork(); + } +} + +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_) { @@ -699,7 +774,7 @@ void DatabaseSync::DeleteSessions() { sessions_.clear(); } -DatabaseSync::~DatabaseSync() { +Database::~Database() { FinalizeBackups(); if (IsOpen()) { @@ -710,13 +785,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; @@ -779,7 +854,7 @@ bool DatabaseSync::Open() { return true; } -void DatabaseSync::FinalizeBackups() { +void Database::FinalizeBackups() { for (auto backup : backups_) { backup->Cleanup(); } @@ -787,7 +862,7 @@ void DatabaseSync::FinalizeBackups() { backups_.clear(); } -void DatabaseSync::FinalizeStatements() { +void Database::FinalizeStatements() { for (auto stmt : statements_) { stmt->Finalize(); } @@ -795,31 +870,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()) { @@ -832,7 +907,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"); @@ -888,7 +963,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); @@ -1090,33 +1166,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"); @@ -1127,7 +1211,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()) { @@ -1135,8 +1219,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"); @@ -1152,16 +1236,34 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { 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); + db->AddAsyncTask(work); + db->ScheduleAsyncTask(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()) { @@ -1170,13 +1272,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"); @@ -1318,8 +1456,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"); @@ -1348,8 +1486,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"); @@ -1505,7 +1643,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"; @@ -1560,7 +1698,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"); @@ -1573,7 +1711,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()); } @@ -1585,7 +1723,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 = @@ -1711,10 +1849,10 @@ static int xFilter(void* pCtx, const char* zTab) { return ctx->filterCallback(zTab) ? 1 : 0; } -void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo& args) { +void Database::ApplyChangeset(const FunctionCallbackInfo& args) { ConflictCallbackContext context; - 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"); @@ -1832,9 +1970,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()) { @@ -1858,8 +1995,8 @@ void DatabaseSync::EnableLoadExtension( CHECK_ERROR_OR_THROW(isolate, db, load_extension_ret, SQLITE_OK, void()); } -void DatabaseSync::EnableDefensive(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::EnableDefensive(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"); @@ -1878,8 +2015,8 @@ void DatabaseSync::EnableDefensive(const FunctionCallbackInfo& args) { CHECK_ERROR_OR_THROW(isolate, db, defensive_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( @@ -1914,8 +2051,8 @@ void DatabaseSync::LoadExtension(const FunctionCallbackInfo& args) { } } -void DatabaseSync::SetAuthorizer(const FunctionCallbackInfo& args) { - DatabaseSync* db; +void Database::SetAuthorizer(const FunctionCallbackInfo& args) { + Database* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); @@ -1937,21 +2074,21 @@ void DatabaseSync::SetAuthorizer(const FunctionCallbackInfo& args) { db->object()->SetInternalField(kAuthorizerCallback, fn); - int r = sqlite3_set_authorizer( - db->connection_, DatabaseSync::AuthorizerCallback, db); + int r = + sqlite3_set_authorizer(db->connection_, Database::AuthorizerCallback, db); if (r != SQLITE_OK) { CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, void()); } } -int DatabaseSync::AuthorizerCallback(void* user_data, - int action_code, - const char* param1, - const char* param2, - const char* param3, - const char* param4) { - DatabaseSync* db = static_cast(user_data); +int Database::AuthorizerCallback(void* user_data, + int action_code, + const char* param1, + const char* param2, + const char* param3, + const char* param4) { + Database* db = static_cast(user_data); Environment* env = db->env(); Isolate* isolate = env->isolate(); HandleScope handle_scope(isolate); @@ -2021,10 +2158,10 @@ int DatabaseSync::AuthorizerCallback(void* user_data, return int_result; } -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; @@ -2036,23 +2173,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); @@ -2154,7 +2291,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 @@ -2194,12 +2331,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); @@ -2231,7 +2368,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, @@ -2252,7 +2389,7 @@ Maybe ExtractRowValues(Environment* env, } MaybeLocal StatementExecutionHelper::All(Environment* env, - DatabaseSync* db, + Database* db, sqlite3_stmt* stmt, bool return_arrays, bool use_big_ints) { @@ -2296,27 +2433,61 @@ MaybeLocal StatementExecutionHelper::All(Environment* env, return scope.Escape(Array::New(isolate, rows.data(), rows.size())); } -MaybeLocal StatementExecutionHelper::Run(Environment* env, - DatabaseSync* db, - sqlite3_stmt* stmt, - bool use_big_ints) { - Isolate* isolate = env->isolate(); - EscapableHandleScope scope(isolate); +int StatementRun(sqlite3_stmt* stmt) { sqlite3_step(stmt); - int r = sqlite3_reset(stmt); - CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, MaybeLocal()); - 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::ConvertStatementGet( + Environment* env, + sqlite3_stmt* stmt, + int num_cols, + bool use_big_ints, + bool return_arrays) { + Isolate* isolate = env->isolate(); + LocalVector row_values(isolate); + if (ExtractRowValues(env, stmt, num_cols, use_big_ints, &row_values) + .IsNothing()) { + return MaybeLocal(); + } + + if (return_arrays) { + return 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) { + Local key; + if (!StatementExecutionHelper::ColumnNameToName(env, stmt, i) + .ToLocal(&key)) { + return MaybeLocal(); + } + + keys.emplace_back(key); + } + + DCHECK_EQ(keys.size(), row_values.size()); + return Object::New( + isolate, Null(isolate), keys.data(), row_values.data(), num_cols); + } +} + +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 @@ -2329,30 +2500,285 @@ MaybeLocal StatementExecutionHelper::Run(Environment* env, return MaybeLocal(); } - return scope.Escape(result); + return result; +} + +MaybeLocal StatementAsyncExecutionHelper::All( + Environment* env, Statement* stmt) { + Isolate* isolate = env->isolate(); + Database* db = stmt->db_.get(); + auto task = [stmt]() -> std::vector { + int num_cols = sqlite3_column_count(stmt->statement_); + std::vector rows; + + auto dup_value = [&](int col) { + return sqlite3_value_dup(sqlite3_column_value(stmt->statement_, col)); + }; + + int r = 0; + while ((r = sqlite3_step(stmt->statement_)) == SQLITE_ROW) { + if (stmt->return_arrays_) { + std::vector array_values; + array_values.reserve(num_cols); + for (int i = 0; i < num_cols; ++i) { + array_values.emplace_back(dup_value(i)); + } + + rows.emplace_back(std::move(array_values)); + + } else { + RowObject object_values; + object_values.reserve(num_cols); + for (int i = 0; i < num_cols; ++i) { + const char* col_name = sqlite3_column_name(stmt->statement_, i); + object_values.emplace_back(std::string(col_name), dup_value(i)); + } + + rows.emplace_back(std::move(object_values)); + } + } + + return rows; + }; + + auto after = [env, isolate, stmt](std::vector rows, + Local resolver) { + LocalVector js_rows(isolate); + 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())) + .FromJust(); + }; + + Local resolver = + MakeSQLiteAsyncWork>(env, db, task, after); + if (resolver.IsEmpty()) { + return MaybeLocal(); + } + + return resolver; } -BaseObjectPtr StatementExecutionHelper::Iterate( - Environment* env, BaseObjectPtr stmt) { +MaybeLocal StatementAsyncExecutionHelper::Get( + Environment* env, Statement* stmt) { + Database* db = stmt->db_.get(); + auto task = [stmt]() -> std::tuple { + int r = sqlite3_step(stmt->statement_); + if (r != SQLITE_ROW && r != SQLITE_DONE) { + return std::make_tuple(r, 0); + } + return std::make_tuple(r, sqlite3_column_count(stmt->statement_)); + }; + + auto after = [db, env, stmt](std::tuple task_result, + Local resolver) { + Isolate* isolate = env->isolate(); + auto [r, num_cols] = task_result; + if (r == SQLITE_DONE) { + resolver->Resolve(env->context(), Undefined(isolate)).FromJust(); + return; + } + + if (r != SQLITE_ROW) { + Local e; + if (!CreateSQLiteError(isolate, db->Connection()).ToLocal(&e)) { + return; + } + resolver->Reject(env->context(), e).FromJust(); + return; + } + + if (num_cols == 0) { + resolver->Resolve(env->context(), Undefined(isolate)).FromJust(); + return; + } + + TryCatch try_catch(isolate); + Local result; + if (StatementSQLiteToJSConverter::ConvertStatementGet(env, + stmt->statement_, + num_cols, + stmt->use_big_ints_, + stmt->return_arrays_) + .ToLocal(&result)) { + resolver->Resolve(env->context(), result).FromJust(); + return; + } + + if (try_catch.HasCaught()) { + resolver->Reject(env->context(), try_catch.Exception()).FromJust(); + } + }; + + Local resolver = + MakeSQLiteAsyncWork>(env, db, task, after); + if (resolver.IsEmpty()) { + return MaybeLocal(); + } + + return resolver; +} + +MaybeLocal StatementAsyncExecutionHelper::Run( + Environment* env, Statement* stmt) { + 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; +} + +MaybeLocal 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) { 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() @@ -2360,14 +2786,14 @@ BaseObjectPtr StatementExecutionHelper::Iterate( .As() ->SetPrototypeV2(context, js_iterator_prototype) .IsNothing()) { - return BaseObjectPtr(); + return BaseObjectPtr(); } return iter; } MaybeLocal StatementExecutionHelper::Get(Environment* env, - DatabaseSync* db, + Database* db, sqlite3_stmt* stmt, bool return_arrays, bool use_big_ints) { @@ -2387,61 +2813,53 @@ MaybeLocal StatementExecutionHelper::Get(Environment* env, return Undefined(isolate); } - LocalVector row_values(isolate); - if (ExtractRowValues(env, stmt, num_cols, use_big_ints, &row_values) - .IsNothing()) { - return MaybeLocal(); + Local result; + if (StatementSQLiteToJSConverter::ConvertStatementGet( + env, stmt, num_cols, use_big_ints, return_arrays) + .ToLocal(&result)) { + return result; } - if (return_arrays) { - return scope.Escape( - 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) { - Local key; - if (!ColumnNameToName(env, stmt, i).ToLocal(&key)) { - return MaybeLocal(); - } - keys.emplace_back(key); - } - - DCHECK_EQ(keys.size(), row_values.size()); - return scope.Escape(Object::New( - isolate, Null(isolate), keys.data(), row_values.data(), num_cols)); - } + return Undefined(isolate); } -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; } - auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); + if (!db->is_async()) { + auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); + Local result; + if (StatementExecutionHelper::All(env, + stmt->db_.get(), + stmt->statement_, + stmt->return_arrays_, + stmt->use_big_ints_) + .ToLocal(&result)) { + args.GetReturnValue().Set(result); + } + return; + } - Local result; - if (StatementExecutionHelper::All(env, - stmt->db_.get(), - stmt->statement_, - stmt->return_arrays_, - stmt->use_big_ints_) - .ToLocal(&result)) { - args.GetReturnValue().Set(result); + Local resolver; + if (StatementAsyncExecutionHelper::All(env, stmt).ToLocal(&resolver)) { + args.GetReturnValue().Set(resolver->GetPromise()); } } -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( @@ -2453,8 +2871,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; @@ -2463,53 +2881,73 @@ 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; } - Local result; - if (StatementExecutionHelper::Get(env, - stmt->db_.get(), - stmt->statement_, - stmt->return_arrays_, - stmt->use_big_ints_) - .ToLocal(&result)) { - args.GetReturnValue().Set(result); + if (!db->is_async()) { + Local result; + if (StatementExecutionHelper::Get(env, + stmt->db_.get(), + stmt->statement_, + stmt->return_arrays_, + stmt->use_big_ints_) + .ToLocal(&result)) { + args.GetReturnValue().Set(result); + } + + return; + } + + Local resolver; + if (StatementAsyncExecutionHelper::Get(env, stmt).ToLocal(&resolver)) { + args.GetReturnValue().Set(resolver->GetPromise()); } } -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; } - Local result; - if (StatementExecutionHelper::Run( - env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_) - .ToLocal(&result)) { - args.GetReturnValue().Set(result); + if (!db->is_async()) { + Local result; + if (StatementExecutionHelper::Run( + env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_) + .ToLocal(&result)) { + args.GetReturnValue().Set(result); + } + + 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( @@ -2551,8 +2989,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( @@ -2565,8 +3003,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( @@ -2587,9 +3025,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( @@ -2605,9 +3043,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( @@ -2622,8 +3060,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( @@ -2638,8 +3076,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( @@ -2660,7 +3098,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)), @@ -2709,7 +3147,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() @@ -2734,7 +3172,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; @@ -2767,7 +3205,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; @@ -2784,8 +3222,8 @@ void SQLTagStore::Iterate(const FunctionCallbackInfo& args) { } } - BaseObjectPtr iter = StatementExecutionHelper::Iterate( - env, BaseObjectPtr(stmt)); + BaseObjectPtr iter = + StatementExecutionHelper::Iterate(env, BaseObjectPtr(stmt)); if (!iter) { return; @@ -2802,7 +3240,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; @@ -2841,7 +3279,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; @@ -2893,14 +3331,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(); @@ -2910,7 +3348,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(); @@ -2923,7 +3361,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.ToStringView(); @@ -2932,7 +3370,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()) { @@ -2949,16 +3387,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); @@ -2979,103 +3417,65 @@ void SQLTagStore::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("sql_tags_cache", cache_content_size); } -Local StatementSync::GetConstructorTemplate( - Environment* env) { +BaseObjectPtr Statement::Create(Environment* env, + BaseObjectPtr db, + sqlite3_stmt* stmt) { 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); + env->sqlite_statement_async_constructor_template(); + if (!db->is_async()) { + tmpl = env->sqlite_statement_sync_constructor_template(); } - return tmpl; -} - -BaseObjectPtr StatementSync::Create( - Environment* env, BaseObjectPtr db, sqlite3_stmt* stmt) { Local obj; - if (!GetConstructorTemplate(env) - ->InstanceTemplate() - ->NewInstance(env->context()) - .ToLocal(&obj)) { + if (!tmpl->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( @@ -3148,8 +3548,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( @@ -3171,7 +3571,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), @@ -3184,7 +3584,7 @@ Session::~Session() { } BaseObjectPtr Session::Create(Environment* env, - BaseObjectWeakPtr database, + BaseObjectWeakPtr database, sqlite3_session* session) { Local obj; if (!GetConstructorTemplate(env) @@ -3325,66 +3725,122 @@ void DefineConstants(Local target) { NODE_DEFINE_CONSTANT(target, SQLITE_RECURSIVE); } -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); - db_tmpl->InstanceTemplate()->SetInternalFieldCount( - DatabaseSync::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); - SetProtoMethod( - isolate, db_tmpl, "aggregate", DatabaseSync::AggregateFunction); - SetProtoMethod( - isolate, db_tmpl, "createSession", DatabaseSync::CreateSession); - SetProtoMethod( - isolate, db_tmpl, "applyChangeset", DatabaseSync::ApplyChangeset); +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, - db_tmpl, - "enableLoadExtension", - DatabaseSync::EnableLoadExtension); - SetProtoMethod( - isolate, db_tmpl, "enableDefensive", DatabaseSync::EnableDefensive); - SetProtoMethod( - isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension); + 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 DefineDatabaseMethods(Isolate* isolate, + Local tmpl) { + SetProtoMethod(isolate, tmpl, "open", Database::Open); + SetProtoMethod(isolate, tmpl, "close", Database::Close); + SetProtoDispose(isolate, tmpl, Database::Dispose); + SetProtoMethod(isolate, tmpl, "prepare", Database::Prepare); + SetProtoMethod(isolate, tmpl, "exec", Database::Exec); + SetProtoMethod(isolate, tmpl, "function", Database::CustomFunction); + SetProtoMethod(isolate, tmpl, "createTagStore", Database::CreateTagStore); + SetProtoMethodNoSideEffect(isolate, tmpl, "location", Database::Location); + SetProtoMethod(isolate, tmpl, "aggregate", Database::AggregateFunction); + SetProtoMethod(isolate, tmpl, "createSession", Database::CreateSession); + SetProtoMethod(isolate, tmpl, "applyChangeset", Database::ApplyChangeset); SetProtoMethod( - isolate, db_tmpl, "setAuthorizer", DatabaseSync::SetAuthorizer); + isolate, tmpl, "enableLoadExtension", Database::EnableLoadExtension); + SetProtoMethod(isolate, tmpl, "enableDefensive", Database::EnableDefensive); + SetProtoMethod(isolate, tmpl, "loadExtension", Database::LoadExtension); + SetProtoMethod(isolate, tmpl, "setAuthorizer", Database::SetAuthorizer); SetSideEffectFreeGetter(isolate, - db_tmpl, + tmpl, FIXED_ONE_BYTE_STRING(isolate, "isOpen"), - DatabaseSync::IsOpenGetter); + Database::IsOpenGetter); SetSideEffectFreeGetter(isolate, - db_tmpl, + 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); + tmpl->InstanceTemplate()->Set(sqlite_type_symbol, database_sync_string); +} + +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, Database::New); + db_tmpl->InstanceTemplate()->SetInternalFieldCount( + Database::kInternalFieldCount); + Local db_async_tmpl = + NewFunctionTemplate(isolate, Database::NewAsync); + db_async_tmpl->InstanceTemplate()->SetInternalFieldCount( + Database::kInternalFieldCount); + Local constants = Object::New(isolate); + DefineConstants(constants); + DefineStatementFunctionTemplates(env); + DefineDatabaseMethods(isolate, db_tmpl); + DefineDatabaseMethods(isolate, db_async_tmpl); SetConstructorFunction(context, target, "DatabaseSync", db_tmpl); SetConstructorFunction(context, target, "StatementSync", - StatementSync::GetConstructorTemplate(env)); + env->sqlite_statement_sync_constructor_template()); + + SetConstructorFunction(context, target, "Database", db_async_tmpl); + 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 8f0f9f15d621d5..1a4dfc67291dae 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -7,10 +7,12 @@ #include "lru_cache-inl.h" #include "node_mem.h" #include "sqlite3.h" +#include "threadpoolwork-inl.h" #include "util.h" #include #include +#include #include namespace node { @@ -23,6 +25,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; } @@ -71,6 +77,7 @@ class DatabaseOpenConfiguration { private: std::string location_; + bool async_ = false; bool read_only_ = false; bool enable_foreign_keys_ = true; bool enable_dqs_ = false; @@ -82,24 +89,52 @@ class DatabaseOpenConfiguration { bool defensive_ = 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; + +class StatementSQLiteToJSConverter { + public: + static v8::MaybeLocal ConvertStatementRun( + Environment* env, + bool use_big_ints, + sqlite3_int64 changes, + sqlite3_int64 last_insert_rowid); + static v8::MaybeLocal ConvertStatementGet(Environment* env, + sqlite3_stmt* stmt, + int num_cols, + bool use_big_ints, + bool return_arrays); +}; + +class StatementAsyncExecutionHelper { + public: + static v8::MaybeLocal Run(Environment* env, + Statement* stmt); + static v8::MaybeLocal Get(Environment* env, + Statement* stmt); + static v8::MaybeLocal All(Environment* env, + Statement* stmt); +}; + class StatementExecutionHelper { public: static v8::MaybeLocal All(Environment* env, - DatabaseSync* db, + Database* db, sqlite3_stmt* stmt, bool return_arrays, bool use_big_ints); static v8::MaybeLocal 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, @@ -108,26 +143,27 @@ class StatementExecutionHelper { sqlite3_stmt* stmt, const int column); static v8::MaybeLocal 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: enum InternalFields { kAuthorizerCallback = BaseObject::kInternalFieldCount, kInternalFieldCount }; - 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( @@ -157,9 +193,14 @@ 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 ScheduleAsyncTask(ThreadPoolWork* async_task); + void ProcessNextAsyncTask(); 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 { @@ -177,41 +218,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::queue task_queue_; + bool has_running_task_ = false; 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); + 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); @@ -231,12 +274,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_; @@ -246,30 +289,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_; }; @@ -279,7 +323,7 @@ class Session : public BaseObject { public: Session(Environment* env, v8::Local object, - BaseObjectWeakPtr database, + BaseObjectWeakPtr database, sqlite3_session* session); ~Session() override; template @@ -289,7 +333,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; @@ -299,18 +343,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); @@ -327,10 +372,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; }; @@ -339,7 +384,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); @@ -348,7 +393,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..53dc961a833e01 --- /dev/null +++ b/test/parallel/test-sqlite-database-async.mjs @@ -0,0 +1,526 @@ +import { skipIfSQLiteMissing } from '../common/index.mjs'; +import tmpdir from '../common/tmpdir.js'; +import { existsSync } from 'node:fs'; +import { suite, test } from 'node:test'; +import { join } from 'node:path'; +import { Database, Statement } from 'node:sqlite'; +skipIfSQLiteMissing(); + +tmpdir.refresh(); + +let cnt = 0; +function nextDb() { + return join(tmpdir.path, `database-${cnt++}.db`); +} + +suite('Database() constructor', () => { + test('throws if called without new', (t) => { + t.assert.throws(() => { + Database(); + }, { + code: 'ERR_CONSTRUCT_CALL_REQUIRED', + message: /Cannot call constructor without `new`/, + }); + }); + + test('throws if database path is not a string, Uint8Array, or URL', (t) => { + t.assert.throws(() => { + new Database(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "path" argument must be a string, Uint8Array, or URL without null bytes/, + }); + }); + + test('throws if the database location as Buffer contains null bytes', (t) => { + t.assert.throws(() => { + new Database(Buffer.from('l\0cation')); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.', + }); + }); + + test('throws if the database location as string contains null bytes', (t) => { + t.assert.throws(() => { + new Database('l\0cation'); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "path" argument must be a string, Uint8Array, or URL without null bytes.', + }); + }); + + test('throws if options is provided but is not an object', (t) => { + t.assert.throws(() => { + new Database('foo', null); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options" argument must be an object/, + }); + }); + + test('throws if options.open is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new Database('foo', { open: 5 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.open" argument must be a boolean/, + }); + }); + + test('throws if options.readOnly is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new Database('foo', { readOnly: 5 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.readOnly" argument must be a boolean/, + }); + }); + + test('throws if options.timeout is provided but is not an integer', (t) => { + t.assert.throws(() => { + new Database('foo', { timeout: .99 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.timeout" argument must be an integer/, + }); + }); + + test('is not read-only by default', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath); + await db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)'); + }); + + test('is read-only if readOnly is set', async (t) => { + const dbPath = nextDb(); + { + const db = new Database(dbPath); + await db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)'); + db.close(); + } + { + const db = new Database(dbPath, { readOnly: true }); + await t.assert.rejects(db.exec('CREATE TABLE bar (id INTEGER PRIMARY KEY)'), { + code: 'ERR_SQLITE_ERROR', + message: /attempt to write a readonly database/, + }); + } + }); + + test('throws if options.enableForeignKeyConstraints is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new Database('foo', { enableForeignKeyConstraints: 5 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.enableForeignKeyConstraints" argument must be a boolean/, + }); + }); + + test('enables foreign key constraints by default', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath); + await db.exec(` + CREATE TABLE foo (id INTEGER PRIMARY KEY); + CREATE TABLE bar (foo_id INTEGER REFERENCES foo(id)); + `); + t.after(() => { db.close(); }); + await t.assert.rejects(db.exec('INSERT INTO bar (foo_id) VALUES (1)'), + { + code: 'ERR_SQLITE_ERROR', + message: 'FOREIGN KEY constraint failed', + }); + }); + + test('allows disabling foreign key constraints', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath, { enableForeignKeyConstraints: false }); + await db.exec(` + CREATE TABLE foo (id INTEGER PRIMARY KEY); + CREATE TABLE bar (foo_id INTEGER REFERENCES foo(id)); + `); + t.after(() => { db.close(); }); + await db.exec('INSERT INTO bar (foo_id) VALUES (1)'); + }); + + test('throws if options.enableDoubleQuotedStringLiterals is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new Database('foo', { enableDoubleQuotedStringLiterals: 5 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.enableDoubleQuotedStringLiterals" argument must be a boolean/, + }); + }); + + test('disables double-quoted string literals by default', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath); + t.after(() => { db.close(); }); + await t.assert.rejects(db.exec('SELECT "foo";'), { + code: 'ERR_SQLITE_ERROR', + message: /no such column: "?foo"?/, + }); + }); + + test('allows enabling double-quoted string literals', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath, { enableDoubleQuotedStringLiterals: true }); + t.after(() => { db.close(); }); + await db.exec('SELECT "foo";'); + }); + + test('throws if options.readBigInts is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new Database('foo', { readBigInts: 42 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.readBigInts" argument must be a boolean.', + }); + }); + + test('allows reading big integers', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath, { readBigInts: true }); + t.after(() => { db.close(); }); + + const setup = await db.exec(` + CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT; + INSERT INTO data (key, val) VALUES (1, 42); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT val FROM data'); + t.assert.deepStrictEqual(await query.get(), { __proto__: null, val: 42n }); + + const insert = db.prepare('INSERT INTO data (key) VALUES (?)'); + t.assert.deepStrictEqual( + await insert.run(20), + { changes: 1n, lastInsertRowid: 20n }, + ); + }); + + test('throws if options.returnArrays is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new Database('foo', { returnArrays: 42 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.returnArrays" argument must be a boolean.', + }); + }); + + test('allows returning arrays', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath, { returnArrays: true }); + t.after(() => { db.close(); }); + const setup = await db.exec(` + CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; + INSERT INTO data (key, val) VALUES (1, 'one'); + INSERT INTO data (key, val) VALUES (2, 'two'); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT key, val FROM data WHERE key = 1'); + t.assert.deepStrictEqual(await query.get(), [1, 'one']); + }); + + test('throws if options.allowBareNamedParameters is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new Database('foo', { allowBareNamedParameters: 42 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.allowBareNamedParameters" argument must be a boolean.', + }); + }); + + test('throws if bare named parameters are used when option is false', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath, { allowBareNamedParameters: false }); + 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 ($k, $v)'); + t.assert.throws(() => { + stmt.run({ k: 2, v: 4 }); + }, { + code: 'ERR_INVALID_STATE', + message: /Unknown named parameter 'k'/, + }); + }); + + test('throws if options.allowUnknownNamedParameters is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new Database('foo', { allowUnknownNamedParameters: 42 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.allowUnknownNamedParameters" argument must be a boolean.', + }); + }); + + test('allows unknown named parameters', async (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath, { allowUnknownNamedParameters: true }); + t.after(() => { db.close(); }); + const setup = await db.exec( + 'CREATE TABLE data(key INTEGER, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + + const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); + const params = { $a: 1, $b: 2, $k: 42, $y: 25, $v: 84, $z: 99 }; + t.assert.deepStrictEqual( + await stmt.run(params), + { changes: 1, lastInsertRowid: 1 }, + ); + }); + + test('has sqlite-type symbol property', (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath); + t.after(() => { db.close(); }); + + const sqliteTypeSymbol = Symbol.for('sqlite-type'); + t.assert.strictEqual(db[sqliteTypeSymbol], 'node:sqlite'); + }); +}); + +suite('Database.prototype.open()', () => { + test('opens a database connection', (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath, { open: false }); + t.after(() => { db.close(); }); + + t.assert.strictEqual(db.isOpen, false); + t.assert.strictEqual(existsSync(dbPath), false); + t.assert.strictEqual(db.open(), undefined); + t.assert.strictEqual(db.isOpen, true); + t.assert.strictEqual(existsSync(dbPath), true); + }); + + test('throws if database is already open', (t) => { + const db = new Database(nextDb(), { open: false }); + t.after(() => { db.close(); }); + + t.assert.strictEqual(db.isOpen, false); + db.open(); + t.assert.strictEqual(db.isOpen, true); + t.assert.throws(() => { + db.open(); + }, { + code: 'ERR_INVALID_STATE', + message: /database is already open/, + }); + t.assert.strictEqual(db.isOpen, true); + }); +}); + +suite('Database.prototype.close()', () => { + test('closes an open database connection', (t) => { + const db = new Database(nextDb()); + + t.assert.strictEqual(db.isOpen, true); + t.assert.strictEqual(db.close(), undefined); + t.assert.strictEqual(db.isOpen, false); + }); + + test('throws if database is not open', (t) => { + const db = new Database(nextDb(), { open: false }); + + t.assert.strictEqual(db.isOpen, false); + t.assert.throws(() => { + db.close(); + }, { + code: 'ERR_INVALID_STATE', + message: /database is not open/, + }); + t.assert.strictEqual(db.isOpen, false); + }); +}); + +suite('Database.prototype.prepare()', () => { + test('returns a prepared statement', (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const stmt = db.prepare('CREATE TABLE webstorage(key TEXT)'); + t.assert.ok(stmt instanceof Statement); + }); + + test('throws if database is not open', (t) => { + const db = new Database(nextDb(), { open: false }); + + t.assert.throws(() => { + db.prepare(); + }, { + 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.prepare(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "sql" argument must be a string/, + }); + }); +}); + +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); + const stmt = db.prepare('SELECT * FROM data ORDER BY key'); + t.assert.deepStrictEqual(await stmt.all(), [ + { __proto__: null, key: 1, val: 2 }, + { __proto__: null, key: 8, val: 9 }, + ]); + }); + + 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/, + }); + }); +}); + +suite('Database.prototype.isTransaction', () => { + test('correctly detects a committed transaction', async (t) => { + const db = new Database(':memory:'); + + t.assert.strictEqual(db.isTransaction, false); + await db.exec('BEGIN'); + t.assert.strictEqual(db.isTransaction, true); + await db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)'); + t.assert.strictEqual(db.isTransaction, true); + await db.exec('COMMIT'); + t.assert.strictEqual(db.isTransaction, false); + }); + + test('correctly detects a rolled back transaction', async (t) => { + const db = new Database(':memory:'); + + t.assert.strictEqual(db.isTransaction, false); + await db.exec('BEGIN'); + t.assert.strictEqual(db.isTransaction, true); + await db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)'); + t.assert.strictEqual(db.isTransaction, true); + await db.exec('ROLLBACK'); + t.assert.strictEqual(db.isTransaction, false); + }); + + test('throws if database is not open', (t) => { + const db = new Database(nextDb(), { open: false }); + + t.assert.throws(() => { + return db.isTransaction; + }, { + code: 'ERR_INVALID_STATE', + message: /database is not open/, + }); + }); +}); + +suite('Database.prototype.location()', () => { + test('throws if database is not open', (t) => { + const db = new Database(nextDb(), { open: false }); + + t.assert.throws(() => { + db.location(); + }, { + code: 'ERR_INVALID_STATE', + message: /database is not open/, + }); + }); + + test('throws if provided dbName is not string', (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + + t.assert.throws(() => { + db.location(null); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "dbName" argument must be a string/, + }); + }); + + test('returns null when connected to in-memory database', (t) => { + const db = new Database(':memory:'); + t.assert.strictEqual(db.location(), null); + }); + + test('returns db path when connected to a persistent database', (t) => { + const dbPath = nextDb(); + const db = new Database(dbPath); + t.after(() => { db.close(); }); + t.assert.strictEqual(db.location(), dbPath); + }); + + test('returns that specific db path when attached', async (t) => { + const dbPath = nextDb(); + const otherPath = nextDb(); + const db = new Database(dbPath); + t.after(() => { db.close(); }); + const other = new Database(dbPath); + t.after(() => { other.close(); }); + + // Adding this escape because the test with unusual chars have a single quote which breaks the query + const escapedPath = otherPath.replace("'", "''"); + await db.exec(`ATTACH DATABASE '${escapedPath}' AS other`); + + t.assert.strictEqual(db.location('other'), otherPath); + }); +}); diff --git a/test/parallel/test-sqlite-statement-async.js b/test/parallel/test-sqlite-statement-async.js new file mode 100644 index 00000000000000..678bb01af7fcb4 --- /dev/null +++ b/test/parallel/test-sqlite-statement-async.js @@ -0,0 +1,611 @@ +// Flags: --expose-gc +'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.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 }); + }); +}); + +suite('Statement.prototype.all()', () => { + test('executes a query and returns an empty array on no results', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + t.assert.deepStrictEqual(await stmt.all(), []); + }); + + test('executes a query and returns all 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.deepStrictEqual(await stmt.run(), { changes: 0, lastInsertRowid: 0 }); + stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); + t.assert.deepStrictEqual( + await stmt.run('key1', 'val1'), + { changes: 1, lastInsertRowid: 1 }, + ); + t.assert.deepStrictEqual( + await stmt.run('key2', 'val2'), + { changes: 1, lastInsertRowid: 2 }, + ); + stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + t.assert.deepStrictEqual(await stmt.all(), [ + { __proto__: null, key: 'key1', val: 'val1' }, + { __proto__: null, key: 'key2', val: 'val2' }, + ]); + }); +}); + +suite('Statement.prototype.iterate()', () => { + test('executes a query and returns an empty iterator on no results', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + const iter = stmt.iterate(); + t.assert.strictEqual(iter instanceof globalThis.Iterator, true); + t.assert.ok(iter[Symbol.iterator]); + t.assert.deepStrictEqual(iter.toArray(), []); + }); + + test('executes a query and returns all 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.deepStrictEqual(await stmt.run(), { changes: 0, lastInsertRowid: 0 }); + stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); + t.assert.deepStrictEqual( + await stmt.run('key1', 'val1'), + { changes: 1, lastInsertRowid: 1 }, + ); + t.assert.deepStrictEqual( + await stmt.run('key2', 'val2'), + { changes: 1, lastInsertRowid: 2 }, + ); + + const items = [ + { __proto__: null, key: 'key1', val: 'val1' }, + { __proto__: null, key: 'key2', val: 'val2' }, + ]; + + stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + t.assert.deepStrictEqual(stmt.iterate().toArray(), items); + + const itemsLoop = items.slice(); + for (const item of stmt.iterate()) { + t.assert.deepStrictEqual(item, itemsLoop.shift()); + } + }); + + test('iterator keeps the prepared statement from being collected', async (t) => { + const db = new Database(':memory:'); + await db.exec(` + CREATE TABLE test(key TEXT, val TEXT); + INSERT INTO test (key, val) VALUES ('key1', 'val1'); + INSERT INTO test (key, val) VALUES ('key2', 'val2'); + `); + // Do not keep an explicit reference to the prepared statement. + const iterator = db.prepare('SELECT * FROM test').iterate(); + const results = []; + + global.gc(); + + for (const item of iterator) { + results.push(item); + } + + t.assert.deepStrictEqual(results, [ + { __proto__: null, key: 'key1', val: 'val1' }, + { __proto__: null, key: 'key2', val: 'val2' }, + ]); + }); + + test('iterator can be exited early', async (t) => { + const db = new Database(':memory:'); + await db.exec(` + CREATE TABLE test(key TEXT, val TEXT); + INSERT INTO test (key, val) VALUES ('key1', 'val1'); + INSERT INTO test (key, val) VALUES ('key2', 'val2'); + `); + const iterator = db.prepare('SELECT * FROM test').iterate(); + const results = []; + + for (const item of iterator) { + results.push(item); + break; + } + + t.assert.deepStrictEqual(results, [ + { __proto__: null, key: 'key1', val: 'val1' }, + ]); + t.assert.deepStrictEqual( + iterator.next(), + { __proto__: null, done: true, value: null }, + ); + }); +}); + +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'); + t.assert.deepStrictEqual(await stmt.run(), { 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.throws(() => { + 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( + 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); + t.assert.deepStrictEqual( + await stmt.run({ k: 1, v: 10 }), { changes: 1, lastInsertRowid: 1 } + ); + t.assert.deepStrictEqual( + await stmt.run({ k: 2, v: 20 }), { changes: 1, lastInsertRowid: 2 } + ); + t.assert.deepStrictEqual( + await stmt.run({ k: 3, v: 30 }), { changes: 1, lastInsertRowid: 3 } + ); + }); + + test('SQLite defaults unbound ?NNN 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 (?1, ?3)'); + + await t.assert.rejects( + stmt.run(1), + { + code: 'ERR_SQLITE_ERROR', + message: 'NOT NULL constraint failed: data.val', + errcode: 1299, + errstr: 'constraint failed', + }); + }); + + test('binds ?NNN params by position', 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 (?1, ?2)'); + t.assert.deepStrictEqual(await stmt.run(1, 2), { changes: 1, lastInsertRowid: 1 }); + }); +}); + +suite('Statement.prototype.sourceSQL', () => { + test('equals input SQL', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const sql = 'INSERT INTO types (key, val) VALUES ($k, $v)'; + const stmt = db.prepare(sql); + t.assert.strictEqual(stmt.sourceSQL, sql); + }); +}); + +suite('Statement.prototype.expandedSQL', async () => { + test('equals expanded SQL', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const sql = 'INSERT INTO types (key, val) VALUES ($k, ?)'; + const expanded = 'INSERT INTO types (key, val) VALUES (\'33\', \'42\')'; + const stmt = db.prepare(sql); + t.assert.deepStrictEqual( + await stmt.run({ $k: '33' }, '42'), + { changes: 1, lastInsertRowid: 33 }, + ); + t.assert.strictEqual(stmt.expandedSQL, expanded); + }); +}); + +suite('Statement.prototype.setReadBigInts()', () => { + test('BigInts support can be toggled', 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; + INSERT INTO data (key, val) VALUES (1, 42); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT val FROM data'); + t.assert.deepStrictEqual(await query.get(), { __proto__: null, val: 42 }); + t.assert.strictEqual(query.setReadBigInts(true), undefined); + t.assert.deepStrictEqual(await query.get(), { __proto__: null, val: 42n }); + t.assert.strictEqual(query.setReadBigInts(false), undefined); + t.assert.deepStrictEqual(await query.get(), { __proto__: null, val: 42 }); + + const insert = db.prepare('INSERT INTO data (key) VALUES (?)'); + t.assert.deepStrictEqual( + await insert.run(10), + { changes: 1, lastInsertRowid: 10 }, + ); + t.assert.strictEqual(insert.setReadBigInts(true), undefined); + t.assert.deepStrictEqual( + await insert.run(20), + { changes: 1n, lastInsertRowid: 20n }, + ); + t.assert.strictEqual(insert.setReadBigInts(false), undefined); + t.assert.deepStrictEqual( + await insert.run(30), + { changes: 1, lastInsertRowid: 30 }, + ); + }); + + test('throws when input is not a boolean', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)'); + t.assert.throws(() => { + stmt.setReadBigInts(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "readBigInts" argument must be a boolean/, + }); + }); + + test('BigInt is required for reading large integers', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const bad = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`); + await t.assert.rejects( + bad.get(), + { + code: 'ERR_OUT_OF_RANGE', + message: /^Value is too large to be represented as a JavaScript number: 9007199254740992$/, + }); + const good = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`); + good.setReadBigInts(true); + t.assert.deepStrictEqual(await good.get(), { + __proto__: null, + [`${Number.MAX_SAFE_INTEGER} + 1`]: 2n ** 53n, + }); + }); +}); + +suite('Statement.prototype.setReturnArrays()', () => { + test('throws when input is not a boolean', 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('SELECT key, val FROM data'); + t.assert.throws(() => { + stmt.setReturnArrays(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "returnArrays" argument must be a boolean/, + }); + }); +}); + +suite('Statement.prototype.get() with array output', () => { + test('returns array row when setReturnArrays is true', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec(` + CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; + INSERT INTO data (key, val) VALUES (1, 'one'); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT key, val FROM data WHERE key = 1'); + t.assert.deepStrictEqual(await query.get(), { __proto__: null, key: 1, val: 'one' }); + + query.setReturnArrays(true); + t.assert.deepStrictEqual(await query.get(), [1, 'one']); + + query.setReturnArrays(false); + t.assert.deepStrictEqual(await query.get(), { __proto__: null, key: 1, val: 'one' }); + }); + + test('returns array rows with BigInts when both flags are set', async (t) => { + const expected = [1n, 9007199254740992n]; + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec(` + CREATE TABLE big_data(id INTEGER, big_num INTEGER); + INSERT INTO big_data VALUES (1, 9007199254740992); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT id, big_num FROM big_data'); + query.setReturnArrays(true); + query.setReadBigInts(true); + + const row = await query.get(); + t.assert.deepStrictEqual(row, expected); + }); +}); + +suite('Statement.prototype.all() with array output', () => { + test('returns array rows when setReturnArrays is true', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec(` + CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; + INSERT INTO data (key, val) VALUES (1, 'one'); + INSERT INTO data (key, val) VALUES (2, 'two'); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT key, val FROM data ORDER BY key'); + t.assert.deepStrictEqual(await query.all(), [ + { __proto__: null, key: 1, val: 'one' }, + { __proto__: null, key: 2, val: 'two' }, + ]); + + query.setReturnArrays(true); + t.assert.deepStrictEqual(await query.all(), [ + [1, 'one'], + [2, 'two'], + ]); + + query.setReturnArrays(false); + t.assert.deepStrictEqual(await query.all(), [ + { __proto__: null, key: 1, val: 'one' }, + { __proto__: null, key: 2, val: 'two' }, + ]); + }); + + test('handles array rows with many columns', async (t) => { + const expected = [ + 1, + 'text1', + 1.1, + new Uint8Array([0xde, 0xad, 0xbe, 0xef]), + 5, + 'text2', + 2.2, + new Uint8Array([0xbe, 0xef, 0xca, 0xfe]), + 9, + 'text3', + ]; + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec(` + CREATE TABLE wide_table( + col1 INTEGER, col2 TEXT, col3 REAL, col4 BLOB, col5 INTEGER, + col6 TEXT, col7 REAL, col8 BLOB, col9 INTEGER, col10 TEXT + ); + INSERT INTO wide_table VALUES ( + 1, 'text1', 1.1, X'DEADBEEF', 5, + 'text2', 2.2, X'BEEFCAFE', 9, 'text3' + ); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT * FROM wide_table'); + query.setReturnArrays(true); + + const results = await query.all(); + t.assert.strictEqual(results.length, 1); + t.assert.deepStrictEqual(results[0], expected); + }); +}); + +suite('Statement.prototype.iterate() with array output', () => { + test('iterates array rows when setReturnArrays is true', async (t) => { + const db = new Database(nextDb()); + t.after(() => { db.close(); }); + const setup = await db.exec(` + CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; + INSERT INTO data (key, val) VALUES (1, 'one'); + INSERT INTO data (key, val) VALUES (2, 'two'); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT key, val FROM data ORDER BY key'); + + // Test with objects first + const objectRows = []; + for (const row of query.iterate()) { + objectRows.push(row); + } + t.assert.deepStrictEqual(objectRows, [ + { __proto__: null, key: 1, val: 'one' }, + { __proto__: null, key: 2, val: 'two' }, + ]); + + // Test with arrays + query.setReturnArrays(true); + const arrayRows = []; + for (const row of query.iterate()) { + arrayRows.push(row); + } + t.assert.deepStrictEqual(arrayRows, [ + [1, 'one'], + [2, 'two'], + ]); + + // Test toArray() method + t.assert.deepStrictEqual(query.iterate().toArray(), [ + [1, 'one'], + [2, 'two'], + ]); + }); + + test('iterator can be exited early with array rows', async (t) => { + const db = new Database(':memory:'); + await db.exec(` + CREATE TABLE test(key TEXT, val TEXT); + INSERT INTO test (key, val) VALUES ('key1', 'val1'); + INSERT INTO test (key, val) VALUES ('key2', 'val2'); + `); + const stmt = db.prepare('SELECT key, val FROM test'); + stmt.setReturnArrays(true); + + const iterator = stmt.iterate(); + const results = []; + + for (const item of iterator) { + results.push(item); + break; + } + + t.assert.deepStrictEqual(results, [ + ['key1', 'val1'], + ]); + t.assert.deepStrictEqual( + iterator.next(), + { __proto__: null, done: true, value: null }, + ); + }); +}); + +suite('Statement.prototype.setAllowBareNamedParameters()', () => { + test('bare named parameter support can be toggled', 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 ($k, $v)'); + t.assert.deepStrictEqual( + await stmt.run({ k: 1, v: 2 }), + { changes: 1, lastInsertRowid: 1 }, + ); + t.assert.strictEqual(stmt.setAllowBareNamedParameters(false), undefined); + t.assert.throws(() => { + stmt.run({ k: 2, v: 4 }); + }, { + code: 'ERR_INVALID_STATE', + message: /Unknown named parameter 'k'/, + }); + t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined); + t.assert.deepStrictEqual( + await stmt.run({ k: 3, v: 6 }), + { changes: 1, lastInsertRowid: 3 }, + ); + }); + + test('throws when input is not a boolean', 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 ($k, $v)'); + t.assert.throws(() => { + stmt.setAllowBareNamedParameters(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "allowBareNamedParameters" argument must be a boolean/, + }); + }); +});