Skip to content

Release statements and iterators when the database is closed - #236

Merged
penberg merged 2 commits into
mainfrom
close-finalizes-statements
Sep 18, 2026
Merged

penberg merged 2 commits into
mainfrom
close-finalizes-statements

Conversation

@penberg

@penberg penberg commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Statements and row iterators hold their own references to the libSQL connection, and libSQL only closes the SQLite handle when the last reference is dropped. Database.close() therefore left the database file open (and any read transaction of a partially consumed iterator active) until the garbage collector reclaimed every statement. On Windows this makes deleting the database file after close() fail with EBUSY, which is what breaks Windows CI on #233.

This tracks each database's statements and iterators with weak references and releases their libSQL objects on close(). In-flight operations hold their own references and finish normally; an iterator closed mid-next() drops its rows once that call completes. Using a statement or iterator after close() throws TypeError: The database connection is not open, matching better-sqlite3. The iterator wrappers in compat.js and promise.js now run errors through convertError so that surfaces as the same TypeError.

A second reference was hiding in iterator results: a local libSQL Row holds a clone of the statement (and so the connection), and the Record returned by RowsIterator.next() kept that row alive until GC. Records now copy the column values out when the row is produced (only the first column when plucking), so they no longer pin the connection.

Unlike better-sqlite3, closing with an active iterator is still allowed (better-sqlite3 throws "This database connection is busy executing a query"), so the active-iterator sync test is libsql-only.

Tests: new regression tests in sync.test.js and async.test.js fail with SQLITE_BUSY without the fix and pass with it.

Statements and row iterators hold their own references to the libSQL
connection, and libSQL only closes the SQLite handle when the last
reference is dropped. Database.close() therefore left the database file
open (and any read transaction of a partially consumed iterator active)
until the garbage collector reclaimed every statement. On Windows this
makes deleting the database file after close() fail with EBUSY.

Track each database's statements and iterators with weak references and
release their libSQL objects on close(). Using a statement or iterator
afterwards throws "The database connection is not open", matching
better-sqlite3.
A local libSQL row references the statement it was read from, and
through it the connection. The Record returned by RowsIterator.next()
kept that row alive until the garbage collector reclaimed it, so the
database file stayed open after Database.close(), failing file deletion
with EBUSY on Windows.

Read the column values when the row is produced so records no longer
reference the statement.
@penberg

penberg commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@penberg
penberg merged commit c12cd5e into main Sep 18, 2026
13 checks passed
@penberg
penberg deleted the close-finalizes-statements branch September 18, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant