From 4a0c2f777fc36f99ab376500246cb8b54e75dc0d Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Sun, 13 Sep 2026 10:15:29 +0000 Subject: [PATCH] fix: free StatementIterator resources on early loop exit (#457) A statement yielded by iterateStatements was only freed by the next next() call, so a for...of loop exited via break, return, or a thrown exception leaked the prepared statement and the allocated SQL buffer until the database was closed. Implement the optional iterator return() method so for...of reclaims those resources automatically on early termination. --- src/api.js | 15 +++++++++++++ test/test_statement_iterator.js | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/api.js b/src/api.js index 126baf1c..55914ed9 100644 --- a/src/api.js +++ b/src/api.js @@ -799,6 +799,21 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { this.sqlPtr = null; }; + /** Free the resources held by this iterator, in case it will not be run + * to completion. This is called automatically when a for..of loop exits + * early (e.g. via break, return, or an exception), and can also be called + * manually. + @return {StatementIterator.StatementIteratorResult} + */ + StatementIterator.prototype["return"] = function () { + if (this.activeStatement !== null) { + this.activeStatement.free(); + this.activeStatement = null; + } + this.finalize(); + return { done: true }; + }; + /** Get any un-executed portions remaining of the original SQL string @return {String} */ diff --git a/test/test_statement_iterator.js b/test/test_statement_iterator.js index fe5f6766..8138d0ec 100644 --- a/test/test_statement_iterator.js +++ b/test/test_statement_iterator.js @@ -52,6 +52,45 @@ exports.test = function (SQL, assert) { } assert.equal(count, 3, "For loop iterates correctly"); + // Exiting a for...of loop early frees the statement it yielded (issue #457). + // The statement is what leaks: finalizing only the iterator leaves it + // registered on the database until close(), so each case below asserts the + // statement is gone and not merely that the iterator stopped. + var earlyExit = db.iterateStatements("SELECT 1 AS x; SELECT 2 AS y"); + var brokeOn = null; + for (let statement of earlyExit) { + brokeOn = statement; + statement.step(); + break; + } + assert.throws(function () { brokeOn.step(); }, /Statement closed/, + "Statement yielded before a break is freed"); + assert.deepEqual(earlyExit.next(), { done: true }, + "Iterator done after for...of loop is exited via break"); + + var throwing = db.iterateStatements("SELECT 1 AS x; SELECT 2 AS y"); + var threwOn = null; + try { + for (let statement of throwing) { + threwOn = statement; + throw new Error("early exit"); + } + } catch (e) { /* expected */ } + assert.throws(function () { threwOn.step(); }, /Statement closed/, + "Statement yielded before a throw is freed"); + assert.deepEqual(throwing.next(), { done: true }, + "Iterator done after for...of loop body throws"); + + // return() can also be called manually to free the iterator early + var manual = db.iterateStatements("SELECT 1 AS x; SELECT 2 AS y"); + var manualStatement = manual.next().value; + assert.deepEqual(manual["return"](), { done: true }, + "return() reports done when called manually"); + assert.throws(function () { manualStatement.step(); }, /Statement closed/, + "Statement is freed when return() is called manually"); + assert.deepEqual(manual.next(), { done: true }, + "Iterator done after calling return()"); + var badsql = "SELECT 1 as x;garbage in, garbage out"; // bad sql will stop iteration