Skip to content

Commit fbaceb5

Browse files
committed
fix(editor): collect a statement's sources and CTE names one at a time
statementScope added a level's sources and its CTE names to its lists as the arguments of one call, so a long enough FROM list or WITH list overflowed the stack in the SQL completion provider: at 700,000 under Bun 1.4.4 and at 130,000 under Node 24.22.1, the V8 of Chrome and Edge. Both are now pushed in a loop, and a list of 1,000,000 names every item.
1 parent dd2c4da commit fbaceb5

2 files changed

Lines changed: 21 additions & 2 deletions

File tree

‎src/lib/editor/sql-scope.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,15 +264,17 @@ export function statementScope(sql: string, cursor: number): StatementScope {
264264
const end = depth === 0 ? sql.length : closeOf(sql, open);
265265
const read = readCtes(sql, open + 1, end, ctes, depth);
266266
ctes = read.ctes;
267-
cteNames.push(...read.names);
267+
// One at a time here and below, never as the arguments of one call: a level may name more CTEs and sources than a
268+
// runtime takes arguments (measured: 700,000 overflowed the stack under Bun 1.4.4, and 130,000 under Node 24.22.1).
269+
for (const name of read.names) cteNames.push(name);
268270
for (const body of read.bodies) cteBodies.add(body);
269271
levels.push({ open, end, ctes });
270272
}
271273

272274
const sources: ScopeSource[] = [];
273275
for (let depth = opens.length; depth >= 0; depth--) {
274276
const { open, end, ctes: visible } = levels[depth];
275-
sources.push(...sourcesIn(sql, open + 1, end, new Map(visible), depth));
277+
for (const source of sourcesIn(sql, open + 1, end, new Map(visible), depth)) sources.push(source);
276278
// A CTE body is a query of its own: the statement around it is no enclosing query.
277279
if (cteBodies.has(open)) break;
278280
}

‎tests/unit/editor/sql-scope.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,21 @@ describe("statementScope", () => {
206206
"users",
207207
]);
208208
});
209+
210+
/**
211+
* A level's sources and its CTE names were once added to the scope's lists as the arguments of one call, which
212+
* overflows the stack past the arguments a runtime takes: measured on 2026-11-04, 700,000 overflowed under Bun 1.4.4
213+
* (130,000 did not) and 130,000 under Node 24.22.1, whose V8 is the engine of Chrome and Edge. A million is past both.
214+
*/
215+
test("a FROM list of 1,000,000 tables names every one", () => {
216+
const { sources } = scopeAt(`SELECT | FROM ${"t, ".repeat(999_999)}last`);
217+
expect(sources.length).toBe(1_000_000);
218+
expect(names([sources[0], sources[999_998], sources[999_999]])).toEqual(["t", "t", "last"]);
219+
});
220+
221+
test("a WITH list of 1,000,000 CTEs names every one", () => {
222+
const { ctes } = scopeAt(`WITH ${"c AS (), ".repeat(999_999)}last AS () SELECT |`);
223+
expect(ctes.length).toBe(1_000_000);
224+
expect([ctes[0], ctes[999_998], ctes[999_999]]).toEqual(["c", "c", "last"]);
225+
});
209226
});

0 commit comments

Comments
 (0)