@@ -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+
11001144void 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 ();
0 commit comments