Repository navigation
sqlite: skip return() on exhausted virtual table iterators #66519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1109,7 +1109,7 @@ bool VirtualTableModule::CanCallIntoJS() const { | |
| bool VirtualTableModule::CloseIterator(NodeVTabCursor* cursor) { | ||
| VirtualTableModule* mod = cursor->module; | ||
|
|
||
| // Skipped in two cases: | ||
| // Skipped in three cases: | ||
| // | ||
| // - While the database is being torn down from a destructor, because those | ||
| // run from a garbage collection callback where JavaScript cannot be | ||
|
|
@@ -1120,7 +1120,11 @@ bool VirtualTableModule::CloseIterator(NodeVTabCursor* cursor) { | |
| // A generator whose own body threw has already run its `finally` as part of | ||
| // that throw, so this only affects an iterator abandoned while suspended | ||
| // because something else failed. | ||
| if (cursor->iterator.IsEmpty() || !mod->CanCallIntoJS() || | ||
| // - When next() has already reported `done: true`, because the iteration ran | ||
| // to completion. `return()` is only called on abrupt termination in | ||
| // `for...of`; a finished iterator must not be asked to clean up again, and | ||
| // a custom iterable may throw from `return()` even after completion. | ||
| if (cursor->iterator.IsEmpty() || cursor->done || !mod->CanCallIntoJS() || | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: what about resetting |
||
| mod->env_->isolate()->HasPendingException()) { | ||
| return false; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -622,6 +622,41 @@ suite('Database.prototype.createModule()', () => { | |
| assert.deepStrictEqual(cleanedUp, [1, 2]); | ||
| }); | ||
|
|
||
| test('does not call return() on a normally exhausted iterator', () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we get a re-filter test too? A correlated subquery should hit it. |
||
| // Once next() reports `done: true` the iterator is finished, so no | ||
| // cleanup should run. A throwing return() must not fail an otherwise | ||
| // successful query. Matches `for...of`, which only calls return() on | ||
| // abrupt termination. | ||
| const db = new Database(':memory:'); | ||
| for (const [label, makeRows] of [ | ||
| ['empty', () => { | ||
| return { | ||
| [Symbol.iterator]() { return this; }, | ||
| next() { return { value: [], done: true }; }, | ||
| return() { throw new Error('cleanup boom'); }, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe count |
||
| }; | ||
| }], | ||
| ['fully_consumed', () => { | ||
| let i = 0; | ||
| return { | ||
| [Symbol.iterator]() { return this; }, | ||
| next() { return { value: [i++], done: i > 2 }; }, | ||
| return() { throw new Error('cleanup boom'); }, | ||
| }; | ||
| }], | ||
| ]) { | ||
| db.createModule(`exhausted_${label}`, { | ||
| columns: [{ name: 'v', type: 'INTEGER' }], | ||
| rows: makeRows, | ||
| }); | ||
|
|
||
| const rows = db.prepare(`SELECT v FROM exhausted_${label}`).all(); | ||
| assert.deepStrictEqual( | ||
| rows.map((row) => row.v), | ||
| label === 'empty' ? [] : [0, 1]); | ||
| } | ||
| }); | ||
|
|
||
| test('does not run cleanup when the statement is collected', () => { | ||
| // The destructor runs from a GC callback, where JavaScript cannot be | ||
| // executed. An abandoned generator does not run `finally` in JavaScript | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This misses the protocol-error paths. If
next()returns a non-object (ornextisn't a function),donestays false, soreturn()still runs on close and can mask the original error.