diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 35dbe1ffc50772..8dbf8779c8627b 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -386,13 +386,16 @@ class CustomAggregate { } Local ret; - if (!(self->*mptr) - .Get(isolate) - ->Call(env->context(), recv, argc + 1, js_argv.data()) - .ToLocal(&ret)) { - self->db_->SetIgnoreNextSQLiteError(true); - sqlite3_result_error(ctx, "", 0); - return; + { + auto guard = self->db_->EnterUserFunctionCallback(); + if (!(self->*mptr) + .Get(isolate) + ->Call(env->context(), recv, argc + 1, js_argv.data()) + .ToLocal(&ret)) { + self->db_->SetIgnoreNextSQLiteError(true); + sqlite3_result_error(ctx, "", 0); + return; + } } agg->value.Reset(isolate, ret); @@ -422,6 +425,7 @@ class CustomAggregate { Local::New(env->isolate(), self->result_fn_); Local js_arg[] = {Local::New(isolate, agg->value)}; + auto guard = self->db_->EnterUserFunctionCallback(); if (!fn->Call(env->context(), Null(isolate), 1, js_arg) .ToLocal(&result)) { self->db_->SetIgnoreNextSQLiteError(true); @@ -455,6 +459,7 @@ class CustomAggregate { Local start_v = Local::New(isolate, start_); if (start_v->IsFunction()) { auto fn = start_v.As(); + auto guard = db_->EnterUserFunctionCallback(); MaybeLocal retval = fn->Call(env_->context(), Null(isolate), 0, nullptr); if (!retval.ToLocal(&start_v)) { @@ -698,6 +703,7 @@ void UserDefinedFunction::xFunc(sqlite3_context* ctx, js_argv.emplace_back(local); } + auto guard = self->db_->EnterUserFunctionCallback(); MaybeLocal retval = fn->Call(env->context(), recv, argc, js_argv.data()); Local result; @@ -1420,6 +1426,10 @@ void DatabaseSync::Close(const FunctionCallbackInfo& args) { 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"); + THROW_AND_RETURN_ON_BAD_STATE( + env, + db->IsInUserFunctionCallback(), + "database cannot be closed inside a user-defined function callback"); db->FinalizeStatements(); db->DeleteSessions(); int r = sqlite3_close_v2(db->connection_); @@ -1808,6 +1818,11 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo& args) { 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"); + THROW_AND_RETURN_ON_BAD_STATE( + env, + db->IsInUserFunctionCallback(), + "database operation is not allowed inside a user-defined function " + "callback"); if (!args[0]->IsUint8Array()) { THROW_ERR_INVALID_ARG_TYPE(env->isolate(), @@ -2573,6 +2588,9 @@ StatementSync::~StatementSync() { } void StatementSync::Finalize() { + if (statement_ == nullptr) { + return; + } sqlite3_finalize(statement_); statement_ = nullptr; InvalidateColumnNameCache(); @@ -3018,6 +3036,8 @@ void StatementSync::All(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsStepping(), "statement is currently being executed"); Isolate* isolate = env->isolate(); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void()); @@ -3026,9 +3046,9 @@ void StatementSync::All(const FunctionCallbackInfo& args) { return; } - auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); - Local result; + auto step = stmt->MarkStepping(); + auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); if (StatementExecutionHelper::All(env, stmt->db_.get(), stmt->statement_, @@ -3045,6 +3065,8 @@ void StatementSync::Iterate(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsStepping(), "statement is currently being executed"); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); @@ -3068,6 +3090,8 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsStepping(), "statement is currently being executed"); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); @@ -3076,6 +3100,7 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { } Local result; + auto step = stmt->MarkStepping(); if (StatementExecutionHelper::Get(env, stmt->db_.get(), stmt->statement_, @@ -3092,6 +3117,8 @@ void StatementSync::Run(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, stmt->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsStepping(), "statement is currently being executed"); int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); @@ -3100,6 +3127,7 @@ void StatementSync::Run(const FunctionCallbackInfo& args) { } Local result; + auto step = stmt->MarkStepping(); if (StatementExecutionHelper::Run( env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_) .ToLocal(&result)) { @@ -3352,8 +3380,11 @@ void SQLTagStore::Run(const FunctionCallbackInfo& args) { return; } + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsStepping(), "statement is currently being executed"); + uint32_t n_params = args.Length() - 1; - int r = sqlite3_reset(stmt->statement_); + int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); int param_count = sqlite3_bind_parameter_count(stmt->statement_); for (int i = 0; i < static_cast(n_params) && i < param_count; ++i) { @@ -3364,6 +3395,7 @@ void SQLTagStore::Run(const FunctionCallbackInfo& args) { } Local result; + auto step = stmt->MarkStepping(); if (StatementExecutionHelper::Run( env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_) .ToLocal(&result)) { @@ -3385,8 +3417,11 @@ void SQLTagStore::Iterate(const FunctionCallbackInfo& args) { return; } + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsStepping(), "statement is currently being executed"); + uint32_t n_params = args.Length() - 1; - int r = sqlite3_reset(stmt->statement_); + int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void()); int param_count = sqlite3_bind_parameter_count(stmt->statement_); for (int i = 0; i < static_cast(n_params) && i < param_count; ++i) { @@ -3420,10 +3455,13 @@ void SQLTagStore::Get(const FunctionCallbackInfo& args) { return; } + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsStepping(), "statement is currently being executed"); + uint32_t n_params = args.Length() - 1; Isolate* isolate = env->isolate(); - int r = sqlite3_reset(stmt->statement_); + int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void()); int param_count = sqlite3_bind_parameter_count(stmt->statement_); @@ -3435,6 +3473,7 @@ void SQLTagStore::Get(const FunctionCallbackInfo& args) { } Local result; + auto step = stmt->MarkStepping(); if (StatementExecutionHelper::Get(env, stmt->db_.get(), stmt->statement_, @@ -3459,10 +3498,13 @@ void SQLTagStore::All(const FunctionCallbackInfo& args) { return; } + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsStepping(), "statement is currently being executed"); + uint32_t n_params = args.Length() - 1; Isolate* isolate = env->isolate(); - int r = sqlite3_reset(stmt->statement_); + int r = stmt->ResetStatement(); CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void()); int param_count = sqlite3_bind_parameter_count(stmt->statement_); @@ -3473,8 +3515,9 @@ void SQLTagStore::All(const FunctionCallbackInfo& args) { } } - auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); Local result; + auto step = stmt->MarkStepping(); + auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); if (StatementExecutionHelper::All(env, stmt->db_.get(), stmt->statement_, @@ -3488,6 +3531,11 @@ void SQLTagStore::All(const FunctionCallbackInfo& args) { void SQLTagStore::Clear(const FunctionCallbackInfo& args) { SQLTagStore* store; ASSIGN_OR_RETURN_UNWRAP(&store, args.This()); + Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, + store->database_->IsInUserFunctionCallback(), + "tag store cannot be cleared inside a user-defined function callback"); store->sql_tags_.Clear(); } @@ -3679,6 +3727,8 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, iter->stmt_->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_ON_BAD_STATE( + env, iter->stmt_->IsStepping(), "statement is currently being executed"); Isolate* isolate = env->isolate(); auto iter_template = getLazyIterTemplate(env); @@ -3701,6 +3751,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo& args) { iter->statement_reset_generation_ != iter->stmt_->reset_generation_, "iterator was invalidated"); + auto step = iter->stmt_->MarkStepping(); int r = sqlite3_step(iter->stmt_->statement_); if (r != SQLITE_ROW) { CHECK_ERROR_OR_THROW( @@ -3755,6 +3806,8 @@ void StatementSyncIterator::Return(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_AND_RETURN_ON_BAD_STATE( env, iter->stmt_->IsFinalized(), "statement has been finalized"); + THROW_AND_RETURN_ON_BAD_STATE( + env, iter->stmt_->IsStepping(), "statement is currently being executed"); Isolate* isolate = env->isolate(); sqlite3_reset(iter->stmt_->statement_); diff --git a/src/node_sqlite.h b/src/node_sqlite.h index e7281ed266af5d..9c672e60337a71 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -221,6 +221,24 @@ class DatabaseSync : public BaseObject { } sqlite3* Connection(); + // SQLite forbids closing the database while a user-defined scalar or + // aggregate function callback is on the stack. Wrap every such + // callback with the RAII guard returned by EnterUserFunctionCallback(). + // db.close()/deserialize() and SQL tag store .clear() check + // IsInUserFunctionCallback() and refuse to run, since they would + // finalize statements (potentially the running one). Reentry into the + // *running* statement (recursive step, reset, or finalize) is + // detected separately via the per-statement + // StatementSync::IsStepping() flag, which leaves cross-statement use + // (the "lookup" pattern) unaffected. + inline auto EnterUserFunctionCallback() { + user_function_callback_depth_++; + return OnScopeLeave([this]() { user_function_callback_depth_--; }); + } + bool IsInUserFunctionCallback() const { + return user_function_callback_depth_ > 0; + } + // In some situations, such as when using custom functions, it is possible // that SQLite reports an error while JavaScript already has a pending // exception. In this case, the SQLite error should be ignored. These methods @@ -241,6 +259,7 @@ class DatabaseSync : public BaseObject { bool enable_load_extension_; sqlite3* connection_; bool ignore_next_sqlite_error_; + int user_function_callback_depth_ = 0; std::set backups_; std::set sessions_; @@ -283,6 +302,18 @@ class StatementSync : public BaseObject { bool GetCachedColumnNames(v8::LocalVector* keys); void Finalize(); bool IsFinalized(); + bool IsStepping() const { return stepping_; } + + // RAII guard: marks this statement as being stepped while alive. + // JS-callable methods that would step, reset, or finalize this + // statement check IsStepping() and throw — that's the + // sqlite3_step / sqlite3_reset / sqlite3_finalize reentry SQLite + // forbids while the statement's user-defined function callback is + // on the stack. + inline auto MarkStepping() { + stepping_ = true; + return OnScopeLeave([this]() { stepping_ = false; }); + } SET_MEMORY_INFO_NAME(StatementSync) SET_SELF_SIZE(StatementSync) @@ -295,6 +326,7 @@ class StatementSync : public BaseObject { bool use_big_ints_; bool allow_bare_named_params_; bool allow_unknown_named_params_; + bool stepping_ = false; uint64_t reset_generation_ = 0; std::optional> bare_named_params_; inline int ResetStatement(); diff --git a/test/parallel/test-sqlite-custom-functions.js b/test/parallel/test-sqlite-custom-functions.js index 6b5f974ede893e..4d887bc63e3820 100644 --- a/test/parallel/test-sqlite-custom-functions.js +++ b/test/parallel/test-sqlite-custom-functions.js @@ -411,4 +411,349 @@ suite('DatabaseSync.prototype.function()', () => { message: /database is not open/, }); }); + + suite('reentrant database operations from inside a user function callback', () => { + const reentrantCloseError = { + code: 'ERR_INVALID_STATE', + message: /database cannot be closed inside a user-defined function callback/, + }; + const reentrantStmtError = { + code: 'ERR_INVALID_STATE', + message: /statement is currently being executed/, + }; + + function newDbWithRows() { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)'); + db.prepare('INSERT INTO t VALUES (1, 10)').run(); + db.prepare('INSERT INTO t VALUES (2, 20)').run(); + return db; + } + + test('.all() rejects db.close() and completes without crashing', () => { + const db = newDbWithRows(); + db.function('close_db', mustCall((x) => { + assert.throws(() => db.close(), reentrantCloseError); + return x; + }, 2)); + const rows = db.prepare('SELECT close_db(v) AS v FROM t').all(); + assert.deepStrictEqual(rows, [ + { __proto__: null, v: 10 }, + { __proto__: null, v: 20 }, + ]); + assert.strictEqual(db.isOpen, true); + }); + + test('.get() rejects db.close() and completes without crashing', () => { + const db = newDbWithRows(); + db.function('close_db', mustCall((x) => { + assert.throws(() => db.close(), reentrantCloseError); + return x; + })); + const row = db.prepare('SELECT close_db(v) AS v FROM t').get(); + assert.deepStrictEqual(row, { __proto__: null, v: 10 }); + assert.strictEqual(db.isOpen, true); + }); + + test('.run() rejects db.close() and completes without crashing', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)'); + db.function('close_db', mustCall((x) => { + assert.throws(() => db.close(), reentrantCloseError); + return x; + }, 2)); + assert.deepStrictEqual( + db.prepare('INSERT INTO t SELECT close_db(1), close_db(2)').run(), + { changes: 1, lastInsertRowid: 1 }, + ); + assert.strictEqual(db.isOpen, true); + }); + + test('iterator rejects db.close() and completes without crashing', () => { + const db = newDbWithRows(); + db.function('close_db', mustCall((x) => { + assert.throws(() => db.close(), reentrantCloseError); + return x; + }, 2)); + const iter = db.prepare('SELECT close_db(v) AS v FROM t').iterate(); + assert.deepStrictEqual(iter.next(), { + __proto__: null, + done: false, + value: { __proto__: null, v: 10 }, + }); + assert.deepStrictEqual(iter.next(), { + __proto__: null, + done: false, + value: { __proto__: null, v: 20 }, + }); + assert.deepStrictEqual(iter.next(), { + __proto__: null, + done: true, + value: null, + }); + assert.strictEqual(db.isOpen, true); + }); + + test('SQL tag store .run() rejects db.close() and completes without crashing', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)'); + db.function('close_db', mustCall((x) => { + assert.throws(() => db.close(), reentrantCloseError); + return x; + }, 2)); + const sql = db.createTagStore(4); + assert.deepStrictEqual( + sql.run`INSERT INTO t SELECT close_db(${1}), close_db(${2})`, + { changes: 1, lastInsertRowid: 1 }, + ); + assert.strictEqual(db.isOpen, true); + }); + + test('SQL tag store .all() rejects db.close() and completes without crashing', () => { + const db = newDbWithRows(); + db.function('close_db', mustCall((x) => { + assert.throws(() => db.close(), reentrantCloseError); + return x; + }, 2)); + const sql = db.createTagStore(4); + const rows = sql.all`SELECT close_db(v) AS v FROM t`; + assert.deepStrictEqual(rows, [ + { __proto__: null, v: 10 }, + { __proto__: null, v: 20 }, + ]); + assert.strictEqual(db.isOpen, true); + }); + + test('SQL tag store .get() rejects db.close() and completes without crashing', () => { + const db = newDbWithRows(); + db.function('close_db', mustCall((x) => { + assert.throws(() => db.close(), reentrantCloseError); + return x; + })); + const sql = db.createTagStore(4); + const row = sql.get`SELECT close_db(v) AS v FROM t`; + assert.deepStrictEqual(row, { __proto__: null, v: 10 }); + assert.strictEqual(db.isOpen, true); + }); + + test('uncaught db.close() failure is propagated from the callback', () => { + const db = new DatabaseSync(':memory:'); + db.function('close_db', () => db.close()); + assert.throws( + () => db.prepare('SELECT close_db()').get(), + reentrantCloseError, + ); + assert.strictEqual(db.isOpen, true); + }); + + test('resetting a statement from a callback is rejected', () => { + const db = new DatabaseSync(':memory:'); + let stmt; + db.function('x', () => stmt.get()); + stmt = db.prepare('SELECT x()'); + assert.throws(() => stmt.get(), reentrantStmtError); + assert.strictEqual(db.isOpen, true); + }); + + test('reentrant iter.next() from a callback is rejected', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)'); + db.prepare('INSERT INTO t VALUES (1, 10)').run(); + let iter; + db.function('reenter', mustCall(() => { + assert.throws(() => iter.next(), reentrantStmtError); + return 0; + })); + iter = db.prepare('SELECT reenter() FROM t').iterate(); + assert.strictEqual(iter.next().done, false); + assert.strictEqual(iter.next().done, true); + assert.strictEqual(db.isOpen, true); + }); + + test('aggregate step rejects db.close() and completes', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)'); + db.prepare('INSERT INTO t VALUES (1, 10)').run(); + db.prepare('INSERT INTO t VALUES (2, 20)').run(); + db.aggregate('agg_close', { + start: 0, + step: mustCall((acc, v) => { + assert.throws(() => db.close(), reentrantCloseError); + return acc + v; + }, 2), + }); + assert.deepStrictEqual( + db.prepare('SELECT agg_close(v) AS s FROM t').get(), + { __proto__: null, s: 30 }, + ); + assert.strictEqual(db.isOpen, true); + }); + + test('aggregate result rejects db.close() and completes', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)'); + db.prepare('INSERT INTO t VALUES (1, 10)').run(); + db.aggregate('agg_result', { + start: 0, + step: (acc, v) => acc + v, + result: mustCall((acc) => { + assert.throws(() => db.close(), reentrantCloseError); + return acc; + }), + }); + assert.deepStrictEqual( + db.prepare('SELECT agg_result(v) AS s FROM t').get(), + { __proto__: null, s: 10 }, + ); + assert.strictEqual(db.isOpen, true); + }); + + test('aggregate start function rejects db.close() and completes', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)'); + db.prepare('INSERT INTO t VALUES (1, 10)').run(); + db.aggregate('agg_start', { + start: mustCall(() => { + assert.throws(() => db.close(), reentrantCloseError); + return 0; + }), + step: (acc, v) => acc + v, + }); + assert.deepStrictEqual( + db.prepare('SELECT agg_start(v) AS s FROM t').get(), + { __proto__: null, s: 10 }, + ); + assert.strictEqual(db.isOpen, true); + }); + + test('SQL tag store clear from a callback is rejected', () => { + const db = new DatabaseSync(':memory:'); + const sql = db.createTagStore(4); + assert.deepStrictEqual(sql.get`SELECT 1 AS one`, { __proto__: null, one: 1 }); + db.function('x', () => sql.clear()); + assert.throws(() => db.prepare('SELECT x()').get(), { + code: 'ERR_INVALID_STATE', + message: /tag store cannot be cleared inside a user-defined function callback/, + }); + assert.strictEqual(sql.size, 1); + }); + + // Regression: a user function may run other prepared statements on + // the same connection (the "lookup" pattern). Only the *currently + // running* statement is unsafe to step/reset/finalize. + test('callback can run a different prepared statement on the same db', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE lookup (id INTEGER PRIMARY KEY, v INTEGER)'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY)'); + db.prepare('INSERT INTO lookup VALUES (1, 100), (2, 200)').run(); + db.prepare('INSERT INTO t VALUES (1), (2)').run(); + const lookup = db.prepare('SELECT v FROM lookup WHERE id = ?'); + db.function('lookup_v', mustCall((id) => lookup.get(id).v, 2)); + assert.deepStrictEqual( + db.prepare('SELECT lookup_v(id) AS v FROM t ORDER BY id').all(), + [ + { __proto__: null, v: 100 }, + { __proto__: null, v: 200 }, + ], + ); + }); + + test('callback can use SQL tag store with different SQL', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER)'); + db.prepare('INSERT INTO t VALUES (1, 10), (2, 20)').run(); + const sql = db.createTagStore(4); + db.function('double_v', mustCall((id) => { + return sql.get`SELECT v * 2 AS d FROM t WHERE id = ${id}`.d; + }, 2)); + assert.deepStrictEqual( + db.prepare('SELECT double_v(id) AS d FROM t ORDER BY id').all(), + [ + { __proto__: null, d: 20 }, + { __proto__: null, d: 40 }, + ], + ); + }); + + // Cover IsStepping rejection on every JS-callable statement method. + for (const method of ['all', 'run', 'iterate']) { + test(`stmt.${method}() same-stmt reentry is rejected`, () => { + const db = new DatabaseSync(':memory:'); + let stmt; + db.function('x', mustCall(() => { + assert.throws(() => stmt[method](), reentrantStmtError); + return 0; + })); + stmt = db.prepare('SELECT x()'); + if (method === 'iterate') { + stmt.iterate().next(); + } else { + stmt[method](); + } + }); + } + + test('iter.return() same-stmt reentry is rejected', () => { + const db = new DatabaseSync(':memory:'); + let iter; + db.function('x', mustCall(() => { + assert.throws(() => iter.return(), reentrantStmtError); + return 0; + })); + iter = db.prepare('SELECT x()').iterate(); + iter.next(); + }); + + // Cover IsStepping rejection on every SQLTagStore execution method. + for (const method of ['run', 'get', 'all', 'iterate']) { + test(`sql.${method}\`...\` same-stmt reentry is rejected`, () => { + const db = new DatabaseSync(':memory:'); + const sql = db.createTagStore(4); + db.function('x', mustCall(() => { + // Re-running the same tag returns the cached stmt — which is + // currently stepping — so the entry check throws. + assert.throws(() => sql[method]`SELECT x()`, reentrantStmtError); + return 0; + })); + if (method === 'iterate') { + sql.iterate`SELECT x()`.next(); + } else { + assert.ok(sql[method]`SELECT x()`); + } + }); + } + + test('db.deserialize() from a callback is rejected', () => { + const db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE t (id INTEGER)'); + const snapshot = db.serialize(); + db.function('x', mustCall(() => { + assert.throws(() => db.deserialize(snapshot), { + code: 'ERR_INVALID_STATE', + message: /database operation is not allowed inside a user-defined function callback/, + }); + return 0; + })); + db.prepare('SELECT x()').get(); + assert.strictEqual(db.isOpen, true); + }); + + test('db[Symbol.dispose]() is a no-op when invoked from a callback', () => { + const db = new DatabaseSync(':memory:'); + db.function('do_dispose', mustCall(() => { + db[Symbol.dispose](); + return 1; + })); + // The dispose attempt is silently skipped; the outer query still + // completes and the database stays open. + assert.deepStrictEqual( + db.prepare('SELECT do_dispose() AS x').get(), + { __proto__: null, x: 1 }, + ); + assert.strictEqual(db.isOpen, true); + // Outside of any callback, dispose works normally. + db[Symbol.dispose](); + assert.strictEqual(db.isOpen, false); + }); + }); });