[AI] Reduce webassembly heap by freeing all prepared statements (#9131)

* [AI] Free sql.js statements prepared by runQuery

In the browser, runQuery prepared a new statement for every string query
but only freed it for queries that fetch rows. sql.js keeps unfreed
statements in the WASM heap until the database closes, so a cold load of
a large budget (~500k spreadsheet cache writes) grew the heap by hundreds
of MB. Free (or reset) the statement in all paths, including on error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016C82Fnx6cnESa3HUTuM9SE

* [AI] Add release note

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016C82Fnx6cnESa3HUTuM9SE

* bump

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Michael Clark <[email protected]>
This commit is contained in:
Moshe Brevda
2026-10-10 13:39:54 +00:00
committed by GitHub
co-authored by Claude Opus 5.5 Michael Clark
parent 358823017c
commit 0728f1a30c
3 changed files with 43 additions and 17 deletions
@@ -193,4 +193,21 @@ describe('Web sqlite', () => {
// @ts-expect-error Property 'id' does not exist on type 'unknown'
expect(rows[0].id).toBe('id1');
});
it('should free statements it prepares', async () => {
const db = await openDatabase();
execQuery(db, initSQL);
const prepare = vi.spyOn(db, 'prepare');
runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id1', 4)");
runQuery(db, 'SELECT * FROM numbers', [], true);
expect(() =>
runQuery(db, "INSERT INTO numbers (id, number) VALUES ('id1', 5)"),
).toThrow(/constraint failed/);
expect(prepare).toHaveBeenCalledTimes(3);
for (const { value: statement } of prepare.mock.results) {
expect(() => statement.run()).toThrow(/Statement closed/);
}
});
});
@@ -109,28 +109,31 @@ export function runQuery<T>(
const stmt = typeof sql === 'string' ? db.prepare(sql) : sql;
if (fetchAll) {
try {
stmt.bind(params);
const rows = [];
try {
if (fetchAll) {
try {
stmt.bind(params);
const rows = [];
while (stmt.step()) {
rows.push(stmt.getAsObject());
while (stmt.step()) {
rows.push(stmt.getAsObject());
}
return rows;
} catch (e) {
logger.log(sql);
throw e;
}
if (typeof sql === 'string') {
stmt.free();
} else {
stmt.reset();
}
return rows;
} catch (e) {
logger.log(sql);
throw e;
}
} else {
stmt.run(params);
return { changes: db.getRowsModified() };
} finally {
// sql.js keeps statements alive in the WASM heap until they are freed.
if (typeof sql === 'string') {
stmt.free();
} else {
stmt.reset();
}
}
}
@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [mbrevda]
---
Fix high memory use in the browser after opening a large budget