Repository: nodejs/node
Pull request: sqlite: handle stmt invalidation

Fixes #61819

Changed files:
filename: src/node_sqlite.cc
patch: @@ -2421,6 +2421,11 @@ inline bool StatementSync::IsFinalized() {
   return statement_ == nullptr;
 }
 
+inline int StatementSync::ResetStatement() {
+  reset_generation_++;
+  return sqlite3_reset(statement_);
+}
+
 bool StatementSync::BindParams(const FunctionCallbackInfo<Value>& args) {
   int r = sqlite3_clear_bindings(statement_);
   CHECK_ERROR_OR_THROW(env()->isolate(), db_.get(), r, SQLITE_OK, false);
@@ -2812,7 +2817,7 @@ void StatementSync::All(const FunctionCallbackInfo<Value>& args) {
   THROW_AND_RETURN_ON_BAD_STATE(
       env, stmt->IsFinalized(), "statement has been finalized");
   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());
 
   if (!stmt->BindParams(args)) {
@@ -2838,7 +2843,7 @@ void StatementSync::Iterate(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   THROW_AND_RETURN_ON_BAD_STATE(
       env, stmt->IsFinalized(), "statement has been finalized");
-  int r = sqlite3_reset(stmt->statement_);
+  int r = stmt->ResetStatement();
   CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
 
   if (!stmt->BindParams(args)) {
@@ -2861,7 +2866,7 @@ void StatementSync::Get(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   THROW_AND_RETURN_ON_BAD_STATE(
       env, stmt->IsFinalized(), "statement has been finalized");
-  int r = sqlite3_reset(stmt->statement_);
+  int r = stmt->ResetStatement();
   CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
 
   if (!stmt->BindParams(args)) {
@@ -2885,7 +2890,7 @@ void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   THROW_AND_RETURN_ON_BAD_STATE(
       env, stmt->IsFinalized(), "statement has been finalized");
-  int r = sqlite3_reset(stmt->statement_);
+  int r = stmt->ResetStatement();
   CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
 
   if (!stmt->BindParams(args)) {
@@ -3430,6 +3435,7 @@ StatementSyncIterator::StatementSyncIterator(Environment* env,
     : BaseObject(env, object), stmt_(std::move(stmt)) {
   MakeWeak();
   done_ = false;
+  statement_reset_generation_ = stmt_->reset_generation_;
 }
 
 StatementSyncIterator::~StatementSyncIterator() {}
@@ -3488,6 +3494,11 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo<Value>& args) {
     return;
   }
 
+  THROW_AND_RETURN_ON_BAD_STATE(
+      env,
+      iter->statement_reset_generation_ != iter->stmt_->reset_generation_,
+      "iterator was invalidated");
+
   int r = sqlite3_step(iter->stmt_->statement_);
   if (r != SQLITE_ROW) {
     CHECK_ERROR_OR_THROW(

filename: src/node_sqlite.h
patch: @@ -291,7 +291,9 @@ class StatementSync : public BaseObject {
   bool use_big_ints_;
   bool allow_bare_named_params_;
   bool allow_unknown_named_params_;
+  uint64_t reset_generation_ = 0;
   std::optional<std::map<std::string, std::string>> bare_named_params_;
+  inline int ResetStatement();
   bool BindParams(const v8::FunctionCallbackInfo<v8::Value>& args);
   bool BindValue(const v8::Local<v8::Value>& value, const int index);
 
@@ -321,6 +323,7 @@ class StatementSyncIterator : public BaseObject {
   ~StatementSyncIterator() override;
   BaseObjectPtr<StatementSync> stmt_;
   bool done_;
+  uint64_t statement_reset_generation_;
 };
 
 using Sqlite3ChangesetGenFunc = int (*)(sqlite3_session*, int*, void**);

filename: test/parallel/test-sqlite-statement-sync.js
patch: @@ -171,6 +171,61 @@ suite('StatementSync.prototype.iterate()', () => {
       { __proto__: null, done: true, value: null },
     );
   });
+
+  test('iterator is invalidated when statement is reset by get/all/run/iterate', (t) => {
+    const db = new DatabaseSync(':memory:');
+    db.exec('CREATE TABLE test (value INTEGER NOT NULL)');
+    for (let i = 0; i < 5; i++) {
+      db.prepare('INSERT INTO test (value) VALUES (?)').run(i);
+    }
+    const stmt = db.prepare('SELECT * FROM test');
+
+    // Invalidated by stmt.get()
+    let it = stmt.iterate();
+    it.next();
+    stmt.get();
+    t.assert.throws(() => { it.next(); }, {
+      code: 'ERR_INVALID_STATE',
+      message: /iterator was invalidated/,
+    });
+
+    // Invalidated by stmt.all()
+    it = stmt.iterate();
+    it.next();
+    stmt.all();
+    t.assert.throws(() => { it.next(); }, {
+      code: 'ERR_INVALID_STATE',
+      message: /iterator was invalidated/,
+    });
+
+    // Invalidated by stmt.run()
+    it = stmt.iterate();
+    it.next();
+    stmt.run();
+    t.assert.throws(() => { it.next(); }, {
+      code: 'ERR_INVALID_STATE',
+      message: /iterator was invalidated/,
+    });
+
+    // Invalidated by a new stmt.iterate()
+    it = stmt.iterate();
+    it.next();
+    const it2 = stmt.iterate();
+    t.assert.throws(() => { it.next(); }, {
+      code: 'ERR_INVALID_STATE',
+      message: /iterator was invalidated/,
+    });
+
+    // New iterator works fine
+    t.assert.strictEqual(it2.next().done, false);
+
+    // Reset on a different statement does NOT invalidate this iterator
+    const stmt2 = db.prepare('SELECT * FROM test');
+    it = stmt.iterate();
+    it.next();
+    stmt2.get();
+    it.next();
+  });
 });
 
 suite('StatementSync.prototype.run()', () => {