Skip to content

Commit 4c068d5

Browse files
sqlite: add async Statement creation/disposal support
1 parent 82c136f commit 4c068d5

5 files changed

Lines changed: 284 additions & 31 deletions

File tree

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@
438438
V(sqlite_column_template, v8::DictionaryTemplate) \
439439
V(sqlite_limits_template, v8::ObjectTemplate) \
440440
V(sqlite_run_result_template, v8::DictionaryTemplate) \
441+
V(sqlite_statement_constructor_template, v8::FunctionTemplate) \
441442
V(sqlite_statement_sync_constructor_template, v8::FunctionTemplate) \
442443
V(sqlite_statement_sync_iterator_constructor_template, v8::FunctionTemplate) \
443444
V(sqlite_session_constructor_template, v8::FunctionTemplate) \

src/node_sqlite.cc

Lines changed: 231 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3856,8 +3856,42 @@ class alignas(64) OperationResult {
38563856
resolver->Resolve(context, Undefined(isolate)).Check();
38573857
}
38583858
};
3859+
class PreparedStatement {
3860+
public:
3861+
explicit PreparedStatement(BaseObjectPtr<Database> db, sqlite3_stmt* stmt)
3862+
: db_(std::move(db)), stmt_(stmt) {}
3863+
3864+
void Connect(Isolate* isolate,
3865+
Local<Context> context,
3866+
Promise::Resolver* resolver) const {
3867+
auto* stmt = stmt_;
3868+
if (!db_->IsOpen()) {
3869+
// Database is closing, therefore directly create a disposed Statement.
3870+
(void)sqlite3_finalize(stmt);
3871+
stmt = nullptr;
3872+
}
3873+
auto stmt_obj =
3874+
Statement::Create(Environment::GetCurrent(context), db_, stmt);
3875+
if (stmt_obj) {
3876+
resolver->Resolve(context, stmt_obj->object()).Check();
3877+
} else {
3878+
Local<String> error_message =
3879+
String::NewFromUtf8(isolate,
3880+
"Failed to create Statement object",
3881+
NewStringType::kNormal)
3882+
.ToLocalChecked();
3883+
Local<Object> error =
3884+
Exception::Error(error_message)->ToObject(context).ToLocalChecked();
3885+
resolver->Reject(context, error).Check();
3886+
}
3887+
}
38593888

3860-
using variant_type = std::variant<Void, Rejected>;
3889+
private:
3890+
BaseObjectPtr<Database> db_;
3891+
sqlite3_stmt* stmt_;
3892+
};
3893+
3894+
using variant_type = std::variant<Void, Rejected, PreparedStatement>;
38613895

38623896
public:
38633897
static OperationResult RejectErrorCode(OperationBase* origin,
@@ -3871,6 +3905,11 @@ class alignas(64) OperationResult {
38713905
static OperationResult ResolveVoid(OperationBase* origin) {
38723906
return OperationResult{origin, Void{}};
38733907
}
3908+
static OperationResult ResolvePreparedStatement(OperationBase* origin,
3909+
BaseObjectPtr<Database> db,
3910+
sqlite3_stmt* stmt) {
3911+
return OperationResult{origin, PreparedStatement{std::move(db), stmt}};
3912+
}
38743913

38753914
template <typename T>
38763915
requires std::constructible_from<variant_type, T>
@@ -3892,6 +3931,47 @@ class alignas(64) OperationResult {
38923931
variant_type result_;
38933932
};
38943933

3934+
class PrepareStatementOperation : private OperationBase {
3935+
public:
3936+
PrepareStatementOperation(Global<Promise::Resolver>&& resolver,
3937+
BaseObjectPtr<Database>&& db,
3938+
std::pmr::string&& sql)
3939+
: OperationBase(std::move(resolver)),
3940+
db_(std::move(db)),
3941+
sql_(std::move(sql)) {}
3942+
3943+
OperationResult operator()(sqlite3* connection) {
3944+
sqlite3_stmt* stmt = nullptr;
3945+
int error_code =
3946+
sqlite3_prepare_v2(connection, sql_.c_str(), -1, &stmt, nullptr);
3947+
return error_code == SQLITE_OK
3948+
? OperationResult::ResolvePreparedStatement(
3949+
this, std::move(db_), stmt)
3950+
: OperationResult::RejectLastError(this, connection);
3951+
}
3952+
3953+
private:
3954+
BaseObjectPtr<Database> db_;
3955+
std::pmr::string sql_;
3956+
};
3957+
3958+
class FinalizeStatementOperation : private OperationBase {
3959+
public:
3960+
FinalizeStatementOperation(Global<Promise::Resolver>&& resolver,
3961+
sqlite3_stmt* stmt)
3962+
: OperationBase(std::move(resolver)), stmt_(stmt) {}
3963+
3964+
OperationResult operator()(sqlite3* connection) {
3965+
int error_code = sqlite3_finalize(stmt_);
3966+
CHECK_NE(error_code, SQLITE_MISUSE);
3967+
stmt_ = nullptr;
3968+
return OperationResult::ResolveVoid(this);
3969+
}
3970+
3971+
private:
3972+
sqlite3_stmt* stmt_;
3973+
};
3974+
38953975
class ExecOperation : private OperationBase {
38963976
public:
38973977
ExecOperation(Global<Promise::Resolver>&& resolver, std::pmr::string&& sql)
@@ -3927,7 +4007,21 @@ class CloseOperation : private OperationBase {
39274007
}
39284008
};
39294009

3930-
using Operation = std::variant<ExecOperation, CloseOperation>;
4010+
using Operation = std::variant<ExecOperation,
4011+
PrepareStatementOperation,
4012+
FinalizeStatementOperation,
4013+
CloseOperation>;
4014+
4015+
template <typename T, typename V>
4016+
struct is_contained_in_variant;
4017+
template <typename T, typename... Args>
4018+
struct is_contained_in_variant<T, std::variant<Args...>> {
4019+
static constexpr bool value{(std::is_same_v<T, Args> || ...)};
4020+
};
4021+
4022+
template <typename Op>
4023+
inline constexpr bool is_operation_type =
4024+
is_contained_in_variant<Op, Operation>::value;
39314025

39324026
enum class QueuePushResult {
39334027
kQueueFull = -1,
@@ -3955,7 +4049,8 @@ class DatabaseOperationQueue {
39554049
: QueuePushResult::kSuccess;
39564050
}
39574051
template <typename Op, typename... Args>
3958-
requires std::constructible_from<Operation,
4052+
requires is_operation_type<Op> &&
4053+
std::constructible_from<Operation,
39594054
std::in_place_type_t<Op>,
39604055
Global<Promise::Resolver>&&,
39614056
Args&&...>
@@ -4214,6 +4309,7 @@ v8::Local<v8::FunctionTemplate> CreateDatabaseConstructorTemplate(
42144309

42154310
SetProtoMethod(isolate, tmpl, "close", Database::Close);
42164311
SetProtoAsyncDispose(isolate, tmpl, Database::AsyncDispose);
4312+
SetProtoMethod(isolate, tmpl, "prepare", Database::Prepare);
42174313
SetProtoMethod(isolate, tmpl, "exec", Database::Exec);
42184314

42194315
Local<String> sqlite_type_key = FIXED_ONE_BYTE_STRING(isolate, "sqlite-type");
@@ -4299,13 +4395,16 @@ Local<Promise> Database::AsyncDisposeImpl() {
42994395
// We can't use Schedule here, because Schedule queues a MicroTask which
43004396
// would try to access the Database object after destruction if this is
43014397
// called from the destructor.
4302-
if (next_batch_ == nullptr) {
4303-
next_batch_ = std::make_unique<DatabaseOperationQueue>(
4304-
1U, std::pmr::get_default_resource());
4305-
}
4398+
// Furthermore we always need to schedule the CloseOperation in a separate
4399+
// batch, to ensure that it runs after all previously scheduled operations,
4400+
// because e.g. PrepareStatementOperations need to connect their results
4401+
// first.
4402+
ProcessNextBatch();
4403+
next_batch_ = std::make_unique<DatabaseOperationQueue>(
4404+
1U, std::pmr::get_default_resource());
43064405
CHECK_NE(next_batch_->PushEmplace<CloseOperation>(isolate, resolver),
43074406
QueuePushResult::kQueueFull);
4308-
executor_->ScheduleBatch(std::move(next_batch_));
4407+
ProcessNextBatch();
43094408
executor_.release()->Dispose();
43104409

43114410
connection_ = nullptr;
@@ -4354,9 +4453,43 @@ void Database::AsyncDispose(const v8::FunctionCallbackInfo<v8::Value>& args) {
43544453
return;
43554454
}
43564455

4456+
// We don't need to dispose statements during destruction, because all
4457+
// Statement instances keep a BaseObjectPtr to the Database and therefore
4458+
// can't outlive the Database. Therefore this shouldn't be executed as part of
4459+
// db->AsyncDisposeImpl().
4460+
// Calling stmt->Dispose modifies the statements_ set, therefore make a copy.
4461+
for (Statement* stmt : std::vector<Statement*>(db->statements_.begin(),
4462+
db->statements_.end())) {
4463+
stmt->Dispose();
4464+
}
4465+
43574466
args.GetReturnValue().Set(db->AsyncDisposeImpl());
43584467
}
43594468

4469+
void Database::Prepare(const v8::FunctionCallbackInfo<v8::Value>& args) {
4470+
Database* db;
4471+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
4472+
Environment* env = Environment::GetCurrent(args);
4473+
// TODO(BurningEnlightenment): these should be rejections
4474+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
4475+
4476+
if (!args[0]->IsString()) {
4477+
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
4478+
"The \"sql\" argument must be a string.");
4479+
return;
4480+
}
4481+
Utf8Value sql(env->isolate(), args[0].As<String>());
4482+
args.GetReturnValue().Set(db->Schedule<PrepareStatementOperation>(
4483+
BaseObjectPtr<Database>(db), std::pmr::string(*sql, sql.length())));
4484+
}
4485+
4486+
void Database::TrackStatement(Statement* statement) {
4487+
statements_.insert(statement);
4488+
}
4489+
void Database::UntrackStatement(Statement* statement) {
4490+
statements_.erase(statement);
4491+
}
4492+
43604493
void Database::Exec(const v8::FunctionCallbackInfo<v8::Value>& args) {
43614494
Database* db;
43624495
// TODO(BurningEnlightenment): these should be rejections
@@ -4374,6 +4507,94 @@ void Database::Exec(const v8::FunctionCallbackInfo<v8::Value>& args) {
43744507
db->Schedule<ExecOperation>(std::pmr::string(*sql, sql.length())));
43754508
}
43764509

4510+
Statement::Statement(Environment* env,
4511+
v8::Local<v8::Object> object,
4512+
BaseObjectPtr<Database> db,
4513+
sqlite3_stmt* stmt)
4514+
: BaseObject(env, object), db_(std::move(db)), statement_(stmt) {
4515+
if (stmt == nullptr) {
4516+
db_ = nullptr;
4517+
} else {
4518+
CHECK_NOT_NULL(db_);
4519+
db_->TrackStatement(this);
4520+
}
4521+
}
4522+
4523+
Statement::~Statement() {
4524+
if (!IsDisposed()) {
4525+
// Our operations keep a BaseObjectPtr to this Statement, so we can be sure
4526+
// that no operations are running or will run after this point. The only
4527+
// exception to this is the FinalizeStatementOperation, but it can only be
4528+
// queued by Statement::Dispose.
4529+
sqlite3_finalize(statement_);
4530+
db_->UntrackStatement(this);
4531+
}
4532+
}
4533+
4534+
void Statement::MemoryInfo(MemoryTracker* tracker) const {}
4535+
4536+
Local<FunctionTemplate> Statement::GetConstructorTemplate(Environment* env) {
4537+
Local<FunctionTemplate> tmpl = env->sqlite_statement_constructor_template();
4538+
if (tmpl.IsEmpty()) {
4539+
Isolate* isolate = env->isolate();
4540+
tmpl = NewFunctionTemplate(isolate, IllegalConstructor);
4541+
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Statement"));
4542+
tmpl->InstanceTemplate()->SetInternalFieldCount(
4543+
Statement::kInternalFieldCount);
4544+
4545+
SetProtoDispose(isolate, tmpl, Statement::Dispose);
4546+
SetSideEffectFreeGetter(isolate,
4547+
tmpl,
4548+
FIXED_ONE_BYTE_STRING(isolate, "isDisposed"),
4549+
Statement::IsDisposedGetter);
4550+
4551+
env->set_sqlite_statement_constructor_template(tmpl);
4552+
}
4553+
return tmpl;
4554+
}
4555+
BaseObjectPtr<Statement> Statement::Create(Environment* env,
4556+
BaseObjectPtr<Database> db,
4557+
sqlite3_stmt* stmt) {
4558+
Local<Object> obj;
4559+
if (!GetConstructorTemplate(env)
4560+
->InstanceTemplate()
4561+
->NewInstance(env->context())
4562+
.ToLocal(&obj)) {
4563+
return nullptr;
4564+
}
4565+
4566+
return MakeBaseObject<Statement>(env, obj, std::move(db), stmt);
4567+
}
4568+
4569+
void Statement::Dispose(const FunctionCallbackInfo<Value>& args) {
4570+
Statement* stmt;
4571+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
4572+
stmt->Dispose();
4573+
args.GetReturnValue().SetUndefined();
4574+
}
4575+
4576+
void Statement::Dispose() {
4577+
if (IsDisposed()) {
4578+
return;
4579+
}
4580+
// Finalizing is a no-fail operation, so we don't need to check the result or
4581+
// return a promise.
4582+
(void)db_->Schedule<FinalizeStatementOperation>(
4583+
std::exchange(statement_, nullptr));
4584+
std::exchange(db_, nullptr)->UntrackStatement(this);
4585+
}
4586+
4587+
void Statement::IsDisposedGetter(const FunctionCallbackInfo<Value>& args) {
4588+
Statement* stmt;
4589+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
4590+
Environment* env = Environment::GetCurrent(args);
4591+
args.GetReturnValue().Set(Boolean::New(env->isolate(), stmt->IsDisposed()));
4592+
}
4593+
4594+
bool Statement::IsDisposed() const {
4595+
return statement_ == nullptr;
4596+
}
4597+
43774598
void DefineConstants(Local<Object> target) {
43784599
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_OMIT);
43794600
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_REPLACE);
@@ -4450,9 +4671,8 @@ static void Initialize(Local<Object> target,
44504671
context, target, "Session", Session::GetConstructorTemplate(env));
44514672
SetConstructorFunction(
44524673
context, target, "Database", CreateDatabaseConstructorTemplate(env));
4453-
target
4454-
->Set(context, FIXED_ONE_BYTE_STRING(isolate, "Statement"), Null(isolate))
4455-
.Check();
4674+
SetConstructorFunction(
4675+
context, target, "Statement", Statement::GetConstructorTemplate(env));
44564676

44574677
target->Set(context, env->constants_string(), constants).Check();
44584678

src/node_sqlite.h

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ class DatabaseSyncLimits : public BaseObject {
450450

451451
class DatabaseOperationExecutor;
452452
class DatabaseOperationQueue;
453+
class Statement;
453454

454455
class Database final : public DatabaseCommon {
455456
public:
@@ -467,8 +468,15 @@ class Database final : public DatabaseCommon {
467468
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
468469
static void Close(const v8::FunctionCallbackInfo<v8::Value>& args);
469470
static void AsyncDispose(const v8::FunctionCallbackInfo<v8::Value>& args);
471+
static void Prepare(const v8::FunctionCallbackInfo<v8::Value>& args);
470472
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args);
471473

474+
template <typename Op, typename... Args>
475+
[[nodiscard]] v8::Local<v8::Promise> Schedule(Args&&... args);
476+
477+
void TrackStatement(Statement* statement);
478+
void UntrackStatement(Statement* statement);
479+
472480
SET_MEMORY_INFO_NAME(Database)
473481
SET_SELF_SIZE(Database)
474482

@@ -482,18 +490,43 @@ class Database final : public DatabaseCommon {
482490
void PrepareNextBatch();
483491
void ProcessNextBatch();
484492
template <typename Op, typename... Args>
485-
[[nodiscard]] v8::Local<v8::Promise> Schedule(Args&&... args);
486-
template <typename Op, typename... Args>
487493
void Schedule(v8::Isolate* isolate,
488494
v8::Local<v8::Promise::Resolver> resolver,
489495
Args&&... args);
490496

491497
std::unique_ptr<DatabaseOperationExecutor> executor_;
492498
std::unique_ptr<DatabaseOperationQueue> next_batch_;
493499

500+
std::unordered_set<Statement*> statements_;
501+
494502
static constexpr int kDefaultBatchSize = 31;
503+
};
495504

496-
friend class StatementExecutionHelper;
505+
class Statement final : public BaseObject {
506+
public:
507+
Statement(Environment* env,
508+
v8::Local<v8::Object> object,
509+
BaseObjectPtr<Database> db,
510+
sqlite3_stmt* stmt);
511+
void MemoryInfo(MemoryTracker* tracker) const override;
512+
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
513+
Environment* env);
514+
static BaseObjectPtr<Statement> Create(Environment* env,
515+
BaseObjectPtr<Database> db,
516+
sqlite3_stmt* stmt);
517+
static void Dispose(const v8::FunctionCallbackInfo<v8::Value>& args);
518+
void Dispose();
519+
static void IsDisposedGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
520+
bool IsDisposed() const;
521+
522+
SET_MEMORY_INFO_NAME(Statement)
523+
SET_SELF_SIZE(Statement)
524+
525+
private:
526+
~Statement() override;
527+
528+
BaseObjectPtr<Database> db_;
529+
sqlite3_stmt* statement_;
497530
};
498531

499532
} // namespace sqlite

0 commit comments

Comments
 (0)