diff --git a/src/js/internal/sql/shared.ts b/src/js/internal/sql/shared.ts index 7dc59a8b0290..9de5a55bcce7 100644 --- a/src/js/internal/sql/shared.ts +++ b/src/js/internal/sql/shared.ts @@ -85,8 +85,16 @@ class SQLResultArray extends PublicArray { static [Symbol.toStringTag] = "SQLResults"; - constructor(values: T[] = []) { - super(...values); + constructor(values?: T[]) { + super(); + + // Not super(...values): spreading puts every row on the JS stack, which overflows + // once a result set has a few hundred thousand rows. + if (values) { + for (let i = 0, length = values.length; i < length; i++) { + $putByValDirect(this, i, values[i]); + } + } // match postgres's result array, in this way for in will not list the // properties and .map will not return undefined command and count diff --git a/test/js/sql/sqlite-sql.test.ts b/test/js/sql/sqlite-sql.test.ts index b86a49018104..100dc81edfcc 100644 --- a/test/js/sql/sqlite-sql.test.ts +++ b/test/js/sql/sqlite-sql.test.ts @@ -2030,6 +2030,35 @@ describe("Performance & Edge Cases", () => { await sql.close(); }); + test( + "returns result sets with more rows than a call can take as arguments", + async () => { + await using sql = new SQL("sqlite://:memory:"); + + // JavaScriptCore refuses to spread more than 0x100000 arguments into one call, so a result + // set this size fails no matter how much stack is free if the rows are spread into the result. + const rowCount = 0x100000 + 1; + const rows = await sql` + WITH RECURSIVE counter(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM counter WHERE n < ${rowCount}) + SELECT n FROM counter + `; + + expect({ + length: rows.length, + count: rows.count, + first: rows[0], + last: rows[rowCount - 1], + }).toEqual({ + length: rowCount, + count: rowCount, + first: { n: 1 }, + last: { n: rowCount }, + }); + }, + // bun:sqlite needs several seconds to build a million row objects in a debug build. + isDebug ? 60_000 : 10_000, + ); + test("handles many columns", async () => { const sql = new SQL(":memory:");