Skip to content

Commit cefe77f

Browse files
committed
sqlite: run generator return() on cursor refilter
SQLite re-invokes xFilter on a cursor it already used, as it does for the inner table of a correlated subquery or join, abandoning the previous iterator mid-loop. Call its return() method so generator `finally` blocks still run. Refactor the xClose cleanup intoCloseIterator() so both paths share it; on refilter, a throwing cleanup is surfaced as the query error. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: opencode
1 parent dd9f4da commit cefe77f

3 files changed

Lines changed: 91 additions & 38 deletions

File tree

‎src/node_sqlite.cc‎

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,50 @@ bool VirtualTableModule::CanCallIntoJS() const {
10971097
return db_ && !db_->IsInDestructor();
10981098
}
10991099

1100+
bool VirtualTableModule::CloseIterator(NodeVTabCursor* cursor) {
1101+
VirtualTableModule* mod = cursor->module;
1102+
1103+
// Skipped in two cases:
1104+
//
1105+
// - While the database is being torn down from a destructor, because those
1106+
// run from a garbage collection callback where JavaScript cannot be
1107+
// executed. An abandoned generator does not run `finally` in JavaScript
1108+
// either, so skipping matches the language.
1109+
// - When an error is already pending, because calling into JavaScript would
1110+
// discard it and the caller would see an empty result instead of the error.
1111+
// A generator whose own body threw has already run its `finally` as part of
1112+
// that throw, so this only affects an iterator abandoned while suspended
1113+
// because something else failed.
1114+
if (cursor->iterator.IsEmpty() || !mod->CanCallIntoJS() ||
1115+
mod->env_->isolate()->HasPendingException()) {
1116+
return false;
1117+
}
1118+
1119+
Environment* env = mod->env_;
1120+
Isolate* isolate = env->isolate();
1121+
HandleScope handle_scope(isolate);
1122+
CallbackDepthGuard callback_guard(mod->db_.get());
1123+
1124+
// Scoped above the property lookup so a throwing `return` getter is
1125+
// handled the same way as a throwing `return()` method.
1126+
TryCatch try_catch(isolate);
1127+
Local<Object> iterator = cursor->iterator.Get(isolate);
1128+
Local<Value> return_method;
1129+
if (iterator->Get(env->context(), FIXED_ONE_BYTE_STRING(isolate, "return"))
1130+
.ToLocal(&return_method) &&
1131+
return_method->IsFunction()) {
1132+
USE(return_method.As<Function>()->Call(
1133+
env->context(), iterator, 0, nullptr));
1134+
}
1135+
1136+
// Re-throw so that a throwing `finally` is not silently discarded.
1137+
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1138+
try_catch.ReThrow();
1139+
return true;
1140+
}
1141+
return false;
1142+
}
1143+
11001144
void VirtualTableModule::ReleaseHiddenValues(NodeVTabCursor* cursor) {
11011145
for (sqlite3_value*& value : cursor->hidden_values) {
11021146
if (value != nullptr) {
@@ -1216,44 +1260,9 @@ int VirtualTableModule::xClose(sqlite3_vtab_cursor* pCursor) {
12161260

12171261
// Close the iterator so generator `finally` blocks still run when SQLite
12181262
// stops stepping early, as it does for LIMIT or a `break` out of a for...of
1219-
// loop. Skipped in two cases:
1220-
//
1221-
// - While the database is being torn down from a destructor, because those
1222-
// run from a garbage collection callback where JavaScript cannot be
1223-
// executed. An abandoned generator does not run `finally` in JavaScript
1224-
// either, so skipping matches the language.
1225-
// - When an error is already pending, because calling into JavaScript would
1226-
// discard it and the caller would see an empty result instead of the error.
1227-
// A generator whose own body threw has already run its `finally` as part of
1228-
// that throw, so this only affects an iterator abandoned while suspended
1229-
// because something else failed.
1230-
if (!cursor->iterator.IsEmpty() && mod->CanCallIntoJS() &&
1231-
!mod->env_->isolate()->HasPendingException()) {
1232-
Environment* env = mod->env_;
1233-
Isolate* isolate = env->isolate();
1234-
HandleScope handle_scope(isolate);
1235-
CallbackDepthGuard callback_guard(mod->db_.get());
1236-
1237-
// Scoped above the property lookup so a throwing `return` getter is
1238-
// handled the same way as a throwing `return()` method.
1239-
TryCatch try_catch(isolate);
1240-
Local<Object> iterator = cursor->iterator.Get(isolate);
1241-
Local<Value> return_method;
1242-
if (iterator->Get(env->context(), FIXED_ONE_BYTE_STRING(isolate, "return"))
1243-
.ToLocal(&return_method) &&
1244-
return_method->IsFunction()) {
1245-
USE(return_method.As<Function>()->Call(
1246-
env->context(), iterator, 0, nullptr));
1247-
}
1248-
1249-
// Re-throw so that a throwing `finally` is not silently discarded. SQLite
1250-
// discards xClose's return value, so there is no SQLite error here to
1251-
// suppress; calling PropagateJSError would leave the suppression flag set
1252-
// and swallow the next unrelated SQLite error.
1253-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1254-
try_catch.ReThrow();
1255-
}
1256-
}
1263+
// loop. SQLite discards xClose's return value, so a throwing cleanup is
1264+
// re-thrown rather than paired with a SQLite error here.
1265+
mod->CloseIterator(cursor);
12571266

12581267
ReleaseHiddenValues(cursor);
12591268
cursor->iterator.Reset();
@@ -1277,6 +1286,15 @@ int VirtualTableModule::xFilter(sqlite3_vtab_cursor* pCursor,
12771286
}
12781287
CallbackDepthGuard callback_guard(mod->db_.get());
12791288

1289+
// Re-filtering a cursor occurs when SQLite re-invokes xFilter on a cursor it
1290+
// already used, as it does for the inner table of a correlated subquery or
1291+
// join. The previous iterator is abandoned mid-loop, so close it the same way
1292+
// xClose does; otherwise its generator `finally` blocks never run. A throwing
1293+
// cleanup is surfaced as the error for this query.
1294+
if (mod->CloseIterator(cursor)) {
1295+
return mod->PropagateJSError();
1296+
}
1297+
12801298
cursor->rowid = 0;
12811299
cursor->done = false;
12821300
cursor->iterator.Reset();

‎src/node_sqlite.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,11 @@ class VirtualTableModule {
717717
// from a garbage collection callback where JavaScript cannot be executed.
718718
bool CanCallIntoJS() const;
719719

720+
// Runs the iterator's return() method so generator `finally` blocks still run
721+
// when an iterator is abandoned while suspended. Returns true if the return()
722+
// threw; the exception is re-thrown for the caller to surface.
723+
bool CloseIterator(NodeVTabCursor* cursor);
724+
720725
static void ReleaseHiddenValues(NodeVTabCursor* cursor);
721726

722727
Environment* env_;

‎test/parallel/test-sqlite-virtual-table.js‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,36 @@ suite('DatabaseSync.prototype.createModule()', () => {
558558
}
559559
});
560560

561+
test('closes the iterator when a filter is reapplied to the cursor', () => {
562+
// A correlated subquery re-invokes xFilter on the same cursor per outer
563+
// row, abandoning the previous iterator; its `finally` must still run.
564+
const db = new DatabaseSync(':memory:');
565+
const cleanedUp = [];
566+
567+
db.createModule('refilter_cleanup', {
568+
columns: [
569+
{ name: 'input', type: 'INTEGER', hidden: true },
570+
],
571+
*rows(input) {
572+
try {
573+
yield [input];
574+
} finally {
575+
cleanedUp.push(input);
576+
}
577+
},
578+
});
579+
580+
db.exec('CREATE TABLE t(a); INSERT INTO t VALUES (1), (2)');
581+
const rows = db.prepare(
582+
'SELECT a FROM t WHERE EXISTS(SELECT 1 FROM refilter_cleanup(t.a))'
583+
).all();
584+
assert.deepStrictEqual(rows, [
585+
{ __proto__: null, a: 1 },
586+
{ __proto__: null, a: 2 },
587+
]);
588+
assert.deepStrictEqual(cleanedUp, [1, 2]);
589+
});
590+
561591
test('does not run cleanup when the statement is collected', () => {
562592
// The destructor runs from a GC callback, where JavaScript cannot be
563593
// executed. An abandoned generator does not run `finally` in JavaScript

0 commit comments

Comments
 (0)