Skip to content

Commit 67e76ee

Browse files
sqlite: add async Statement creation/disposal support
1 parent 3a5b1a9 commit 67e76ee

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
@@ -436,6 +436,7 @@
436436
V(space_stats_template, v8::DictionaryTemplate) \
437437
V(sqlite_column_template, v8::DictionaryTemplate) \
438438
V(sqlite_run_result_template, v8::DictionaryTemplate) \
439+
V(sqlite_statement_constructor_template, v8::FunctionTemplate) \
439440
V(sqlite_statement_sync_constructor_template, v8::FunctionTemplate) \
440441
V(sqlite_statement_sync_iterator_constructor_template, v8::FunctionTemplate) \
441442
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
@@ -3586,8 +3586,42 @@ class alignas(64) OperationResult {
35863586
resolver->Resolve(context, Undefined(isolate)).Check();
35873587
}
35883588
};
3589+
class PreparedStatement {
3590+
public:
3591+
explicit PreparedStatement(BaseObjectPtr<Database> db, sqlite3_stmt* stmt)
3592+
: db_(std::move(db)), stmt_(stmt) {}
3593+
3594+
void Connect(Isolate* isolate,
3595+
Local<Context> context,
3596+
Promise::Resolver* resolver) const {
3597+
auto* stmt = stmt_;
3598+
if (!db_->IsOpen()) {
3599+
// Database is closing, therefore directly create a disposed Statement.
3600+
(void)sqlite3_finalize(stmt);
3601+
stmt = nullptr;
3602+
}
3603+
auto stmt_obj =
3604+
Statement::Create(Environment::GetCurrent(context), db_, stmt);
3605+
if (stmt_obj) {
3606+
resolver->Resolve(context, stmt_obj->object()).Check();
3607+
} else {
3608+
Local<String> error_message =
3609+
String::NewFromUtf8(isolate,
3610+
"Failed to create Statement object",
3611+
NewStringType::kNormal)
3612+
.ToLocalChecked();
3613+
Local<Object> error =
3614+
Exception::Error(error_message)->ToObject(context).ToLocalChecked();
3615+
resolver->Reject(context, error).Check();
3616+
}
3617+
}
35893618

3590-
using variant_type = std::variant<Void, Rejected>;
3619+
private:
3620+
BaseObjectPtr<Database> db_;
3621+
sqlite3_stmt* stmt_;
3622+
};
3623+
3624+
using variant_type = std::variant<Void, Rejected, PreparedStatement>;
35913625

35923626
public:
35933627
static OperationResult RejectErrorCode(OperationBase* origin,
@@ -3601,6 +3635,11 @@ class alignas(64) OperationResult {
36013635
static OperationResult ResolveVoid(OperationBase* origin) {
36023636
return OperationResult{origin, Void{}};
36033637
}
3638+
static OperationResult ResolvePreparedStatement(OperationBase* origin,
3639+
BaseObjectPtr<Database> db,
3640+
sqlite3_stmt* stmt) {
3641+
return OperationResult{origin, PreparedStatement{std::move(db), stmt}};
3642+
}
36043643

36053644
template <typename T>
36063645
requires std::constructible_from<variant_type, T>
@@ -3622,6 +3661,47 @@ class alignas(64) OperationResult {
36223661
variant_type result_;
36233662
};
36243663

3664+
class PrepareStatementOperation : private OperationBase {
3665+
public:
3666+
PrepareStatementOperation(Global<Promise::Resolver>&& resolver,
3667+
BaseObjectPtr<Database>&& db,
3668+
std::pmr::string&& sql)
3669+
: OperationBase(std::move(resolver)),
3670+
db_(std::move(db)),
3671+
sql_(std::move(sql)) {}
3672+
3673+
OperationResult operator()(sqlite3* connection) {
3674+
sqlite3_stmt* stmt = nullptr;
3675+
int error_code =
3676+
sqlite3_prepare_v2(connection, sql_.c_str(), -1, &stmt, nullptr);
3677+
return error_code == SQLITE_OK
3678+
? OperationResult::ResolvePreparedStatement(
3679+
this, std::move(db_), stmt)
3680+
: OperationResult::RejectLastError(this, connection);
3681+
}
3682+
3683+
private:
3684+
BaseObjectPtr<Database> db_;
3685+
std::pmr::string sql_;
3686+
};
3687+
3688+
class FinalizeStatementOperation : private OperationBase {
3689+
public:
3690+
FinalizeStatementOperation(Global<Promise::Resolver>&& resolver,
3691+
sqlite3_stmt* stmt)
3692+
: OperationBase(std::move(resolver)), stmt_(stmt) {}
3693+
3694+
OperationResult operator()(sqlite3* connection) {
3695+
int error_code = sqlite3_finalize(stmt_);
3696+
CHECK_NE(error_code, SQLITE_MISUSE);
3697+
stmt_ = nullptr;
3698+
return OperationResult::ResolveVoid(this);
3699+
}
3700+
3701+
private:
3702+
sqlite3_stmt* stmt_;
3703+
};
3704+
36253705
class ExecOperation : private OperationBase {
36263706
public:
36273707
ExecOperation(Global<Promise::Resolver>&& resolver, std::pmr::string&& sql)
@@ -3657,7 +3737,21 @@ class CloseOperation : private OperationBase {
36573737
}
36583738
};
36593739

3660-
using Operation = std::variant<ExecOperation, CloseOperation>;
3740+
using Operation = std::variant<ExecOperation,
3741+
PrepareStatementOperation,
3742+
FinalizeStatementOperation,
3743+
CloseOperation>;
3744+
3745+
template <typename T, typename V>
3746+
struct is_contained_in_variant;
3747+
template <typename T, typename... Args>
3748+
struct is_contained_in_variant<T, std::variant<Args...>> {
3749+
static constexpr bool value{(std::is_same_v<T, Args> || ...)};
3750+
};
3751+
3752+
template <typename Op>
3753+
inline constexpr bool is_operation_type =
3754+
is_contained_in_variant<Op, Operation>::value;
36613755

36623756
enum class QueuePushResult {
36633757
kQueueFull = -1,
@@ -3685,7 +3779,8 @@ class DatabaseOperationQueue {
36853779
: QueuePushResult::kSuccess;
36863780
}
36873781
template <typename Op, typename... Args>
3688-
requires std::constructible_from<Operation,
3782+
requires is_operation_type<Op> &&
3783+
std::constructible_from<Operation,
36893784
std::in_place_type_t<Op>,
36903785
Global<Promise::Resolver>&&,
36913786
Args&&...>
@@ -3944,6 +4039,7 @@ v8::Local<v8::FunctionTemplate> CreateDatabaseConstructorTemplate(
39444039

39454040
SetProtoMethod(isolate, tmpl, "close", Database::Close);
39464041
SetProtoAsyncDispose(isolate, tmpl, Database::AsyncDispose);
4042+
SetProtoMethod(isolate, tmpl, "prepare", Database::Prepare);
39474043
SetProtoMethod(isolate, tmpl, "exec", Database::Exec);
39484044

39494045
Local<String> sqlite_type_key = FIXED_ONE_BYTE_STRING(isolate, "sqlite-type");
@@ -4029,13 +4125,16 @@ Local<Promise> Database::AsyncDisposeImpl() {
40294125
// We can't use Schedule here, because Schedule queues a MicroTask which
40304126
// would try to access the Database object after destruction if this is
40314127
// called from the destructor.
4032-
if (next_batch_ == nullptr) {
4033-
next_batch_ = std::make_unique<DatabaseOperationQueue>(
4034-
1U, std::pmr::get_default_resource());
4035-
}
4128+
// Furthermore we always need to schedule the CloseOperation in a separate
4129+
// batch, to ensure that it runs after all previously scheduled operations,
4130+
// because e.g. PrepareStatementOperations need to connect their results
4131+
// first.
4132+
ProcessNextBatch();
4133+
next_batch_ = std::make_unique<DatabaseOperationQueue>(
4134+
1U, std::pmr::get_default_resource());
40364135
CHECK_NE(next_batch_->PushEmplace<CloseOperation>(isolate, resolver),
40374136
QueuePushResult::kQueueFull);
4038-
executor_->ScheduleBatch(std::move(next_batch_));
4137+
ProcessNextBatch();
40394138
executor_.release()->Dispose();
40404139

40414140
connection_ = nullptr;
@@ -4084,9 +4183,43 @@ void Database::AsyncDispose(const v8::FunctionCallbackInfo<v8::Value>& args) {
40844183
return;
40854184
}
40864185

4186+
// We don't need to dispose statements during destruction, because all
4187+
// Statement instances keep a BaseObjectPtr to the Database and therefore
4188+
// can't outlive the Database. Therefore this shouldn't be executed as part of
4189+
// db->AsyncDisposeImpl().
4190+
// Calling stmt->Dispose modifies the statements_ set, therefore make a copy.
4191+
for (Statement* stmt : std::vector<Statement*>(db->statements_.begin(),
4192+
db->statements_.end())) {
4193+
stmt->Dispose();
4194+
}
4195+
40874196
args.GetReturnValue().Set(db->AsyncDisposeImpl());
40884197
}
40894198

4199+
void Database::Prepare(const v8::FunctionCallbackInfo<v8::Value>& args) {
4200+
Database* db;
4201+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
4202+
Environment* env = Environment::GetCurrent(args);
4203+
// TODO(BurningEnlightenment): these should be rejections
4204+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
4205+
4206+
if (!args[0]->IsString()) {
4207+
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
4208+
"The \"sql\" argument must be a string.");
4209+
return;
4210+
}
4211+
Utf8Value sql(env->isolate(), args[0].As<String>());
4212+
args.GetReturnValue().Set(db->Schedule<PrepareStatementOperation>(
4213+
BaseObjectPtr<Database>(db), std::pmr::string(*sql, sql.length())));
4214+
}
4215+
4216+
void Database::TrackStatement(Statement* statement) {
4217+
statements_.insert(statement);
4218+
}
4219+
void Database::UntrackStatement(Statement* statement) {
4220+
statements_.erase(statement);
4221+
}
4222+
40904223
void Database::Exec(const v8::FunctionCallbackInfo<v8::Value>& args) {
40914224
Database* db;
40924225
// TODO(BurningEnlightenment): these should be rejections
@@ -4104,6 +4237,94 @@ void Database::Exec(const v8::FunctionCallbackInfo<v8::Value>& args) {
41044237
db->Schedule<ExecOperation>(std::pmr::string(*sql, sql.length())));
41054238
}
41064239

4240+
Statement::Statement(Environment* env,
4241+
v8::Local<v8::Object> object,
4242+
BaseObjectPtr<Database> db,
4243+
sqlite3_stmt* stmt)
4244+
: BaseObject(env, object), db_(std::move(db)), statement_(stmt) {
4245+
if (stmt == nullptr) {
4246+
db_ = nullptr;
4247+
} else {
4248+
CHECK_NOT_NULL(db_);
4249+
db_->TrackStatement(this);
4250+
}
4251+
}
4252+
4253+
Statement::~Statement() {
4254+
if (!IsDisposed()) {
4255+
// Our operations keep a BaseObjectPtr to this Statement, so we can be sure
4256+
// that no operations are running or will run after this point. The only
4257+
// exception to this is the FinalizeStatementOperation, but it can only be
4258+
// queued by Statement::Dispose.
4259+
sqlite3_finalize(statement_);
4260+
db_->UntrackStatement(this);
4261+
}
4262+
}
4263+
4264+
void Statement::MemoryInfo(MemoryTracker* tracker) const {}
4265+
4266+
Local<FunctionTemplate> Statement::GetConstructorTemplate(Environment* env) {
4267+
Local<FunctionTemplate> tmpl = env->sqlite_statement_constructor_template();
4268+
if (tmpl.IsEmpty()) {
4269+
Isolate* isolate = env->isolate();
4270+
tmpl = NewFunctionTemplate(isolate, IllegalConstructor);
4271+
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "Statement"));
4272+
tmpl->InstanceTemplate()->SetInternalFieldCount(
4273+
Statement::kInternalFieldCount);
4274+
4275+
SetProtoDispose(isolate, tmpl, Statement::Dispose);
4276+
SetSideEffectFreeGetter(isolate,
4277+
tmpl,
4278+
FIXED_ONE_BYTE_STRING(isolate, "isDisposed"),
4279+
Statement::IsDisposedGetter);
4280+
4281+
env->set_sqlite_statement_constructor_template(tmpl);
4282+
}
4283+
return tmpl;
4284+
}
4285+
BaseObjectPtr<Statement> Statement::Create(Environment* env,
4286+
BaseObjectPtr<Database> db,
4287+
sqlite3_stmt* stmt) {
4288+
Local<Object> obj;
4289+
if (!GetConstructorTemplate(env)
4290+
->InstanceTemplate()
4291+
->NewInstance(env->context())
4292+
.ToLocal(&obj)) {
4293+
return nullptr;
4294+
}
4295+
4296+
return MakeBaseObject<Statement>(env, obj, std::move(db), stmt);
4297+
}
4298+
4299+
void Statement::Dispose(const FunctionCallbackInfo<Value>& args) {
4300+
Statement* stmt;
4301+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
4302+
stmt->Dispose();
4303+
args.GetReturnValue().SetUndefined();
4304+
}
4305+
4306+
void Statement::Dispose() {
4307+
if (IsDisposed()) {
4308+
return;
4309+
}
4310+
// Finalizing is a no-fail operation, so we don't need to check the result or
4311+
// return a promise.
4312+
(void)db_->Schedule<FinalizeStatementOperation>(
4313+
std::exchange(statement_, nullptr));
4314+
std::exchange(db_, nullptr)->UntrackStatement(this);
4315+
}
4316+
4317+
void Statement::IsDisposedGetter(const FunctionCallbackInfo<Value>& args) {
4318+
Statement* stmt;
4319+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
4320+
Environment* env = Environment::GetCurrent(args);
4321+
args.GetReturnValue().Set(Boolean::New(env->isolate(), stmt->IsDisposed()));
4322+
}
4323+
4324+
bool Statement::IsDisposed() const {
4325+
return statement_ == nullptr;
4326+
}
4327+
41074328
void DefineConstants(Local<Object> target) {
41084329
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_OMIT);
41094330
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_REPLACE);
@@ -4180,9 +4401,8 @@ static void Initialize(Local<Object> target,
41804401
context, target, "Session", Session::GetConstructorTemplate(env));
41814402
SetConstructorFunction(
41824403
context, target, "Database", CreateDatabaseConstructorTemplate(env));
4183-
target
4184-
->Set(context, FIXED_ONE_BYTE_STRING(isolate, "Statement"), Null(isolate))
4185-
.Check();
4404+
SetConstructorFunction(
4405+
context, target, "Statement", Statement::GetConstructorTemplate(env));
41864406

41874407
target->Set(context, env->constants_string(), constants).Check();
41884408

src/node_sqlite.h

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ class UserDefinedFunction {
370370

371371
class DatabaseOperationExecutor;
372372
class DatabaseOperationQueue;
373+
class Statement;
373374

374375
class Database final : public DatabaseCommon {
375376
public:
@@ -387,8 +388,15 @@ class Database final : public DatabaseCommon {
387388
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
388389
static void Close(const v8::FunctionCallbackInfo<v8::Value>& args);
389390
static void AsyncDispose(const v8::FunctionCallbackInfo<v8::Value>& args);
391+
static void Prepare(const v8::FunctionCallbackInfo<v8::Value>& args);
390392
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args);
391393

394+
template <typename Op, typename... Args>
395+
[[nodiscard]] v8::Local<v8::Promise> Schedule(Args&&... args);
396+
397+
void TrackStatement(Statement* statement);
398+
void UntrackStatement(Statement* statement);
399+
392400
SET_MEMORY_INFO_NAME(Database)
393401
SET_SELF_SIZE(Database)
394402

@@ -402,18 +410,43 @@ class Database final : public DatabaseCommon {
402410
void PrepareNextBatch();
403411
void ProcessNextBatch();
404412
template <typename Op, typename... Args>
405-
[[nodiscard]] v8::Local<v8::Promise> Schedule(Args&&... args);
406-
template <typename Op, typename... Args>
407413
void Schedule(v8::Isolate* isolate,
408414
v8::Local<v8::Promise::Resolver> resolver,
409415
Args&&... args);
410416

411417
std::unique_ptr<DatabaseOperationExecutor> executor_;
412418
std::unique_ptr<DatabaseOperationQueue> next_batch_;
413419

420+
std::unordered_set<Statement*> statements_;
421+
414422
static constexpr int kDefaultBatchSize = 31;
423+
};
415424

416-
friend class StatementExecutionHelper;
425+
class Statement final : public BaseObject {
426+
public:
427+
Statement(Environment* env,
428+
v8::Local<v8::Object> object,
429+
BaseObjectPtr<Database> db,
430+
sqlite3_stmt* stmt);
431+
void MemoryInfo(MemoryTracker* tracker) const override;
432+
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
433+
Environment* env);
434+
static BaseObjectPtr<Statement> Create(Environment* env,
435+
BaseObjectPtr<Database> db,
436+
sqlite3_stmt* stmt);
437+
static void Dispose(const v8::FunctionCallbackInfo<v8::Value>& args);
438+
void Dispose();
439+
static void IsDisposedGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
440+
bool IsDisposed() const;
441+
442+
SET_MEMORY_INFO_NAME(Statement)
443+
SET_SELF_SIZE(Statement)
444+
445+
private:
446+
~Statement() override;
447+
448+
BaseObjectPtr<Database> db_;
449+
sqlite3_stmt* statement_;
417450
};
418451

419452
} // namespace sqlite

0 commit comments

Comments
 (0)