Repository navigation
Commit 43bac51
authored
perf(sql): read the confirmation gate's text in linear time (#2249)
* fix(sql): follow an EXPLAIN chain in a loop, not one call deeper per EXPLAIN
The confirmation gate read the statement an EXPLAIN executes by calling itself on it, so a PostgreSQL text of EXPLAIN ANALYZE repeated ran V8 out of stack: the web editor, the VS Code extension's child and a Node server threw a RangeError from a 250,000-unit chain, short of the 1,000,000 units the statement analysis route accepts.
The chain is now followed in a loop, which is the same tail call written out, so every verdict stays as it was.
Bun's engine eliminates the tail call and never showed it, so the extension's query/target suite pins the depth under Node.
* perf(sql): read the code point before a raw-string prefix off its code units
Every reader asks whether an `r` on Spark continues a word, and the answer split the two code units before it into an array of code points to take the last one.
On 1,000,000 units of `r` words that was most of the confirmation gate's 1.5 s.
The code point is now read off the two code units: a surrogate pair whole, any other unit alone, as the array gave it.
* perf(sql): keep the leading reader's trivia grammar once per grammar record
`readLeadingKeyword` reads a leading `#` as a comment on every dialect, and it copied the caller's grammar record to say so on every call.
The confirmation gate reads the leading keyword of every statement of a script, so a script of 100,000 short statements made 100,000 copies.
The copy is now kept per record, which is a constant, so each dialect is copied once.
* perf(sql): stop the confirmation gate reading the rest of a text again per nested or repeated form
Four checks read the rest of the text again for every copy of what they looked for, so their cost grew with the square of the text, minutes at the 1,000,000 units the statement analysis route accepts:
- a CTE body that opens with WITH had its list read again from that WITH, crossing every body nested inside it once per level (65 s for 250,000 units of nested bodies on PostgreSQL);
- a Spark computed call looked for the end of its run, crossing every call nested inside it again (68 s for 250,000 units);
- a Snowflake CREATE STAGE looked for URL to the end of the text after every occurrence (52 s for 250,000 units);
- a FOR UPDATE lock clause in a block matched its pattern against the whole text before every UPDATE (40 s for 1,000,000 units).
Where a parenthesised run closes is now remembered for every index a walk stood on (ParenthesisedRuns), which is exact because a walk's next step depends only on its index, and the nested CTE lists are read in place to share it.
A conditional form is decided at the first occurrence its words follow, and the lock clause pattern reads the white space before the UPDATE and the four characters before that.
Every verdict stays as it was: the existing gate tests, the analysis parity tests, and a probe comparing the old gate with this one over 400,000 generated texts and every connection type agree.
* perf(sql): read a text's code words once for every form the confirmation gate asks about
The gate walked the text once for every confirmation form and shared word it looked for, and read the words after every occurrence once per form that starts with that word: about 100 walks of a Snowflake block and more than 200 of a block of CREATE words.
One walk now indexes the code words, prefixes and quoted names those forms are found by (indexCodeWords), word sequences that share their first word are compared as one tree, a quoted call's names as one set, and a grammar's forms are compiled once per record.
A statement that is the whole text, as one under the caret is, is not read a second time.
Measured on 1,000,000 units under Bun 1.4.4: 166,666 Snowflake BEGIN words 1065 ms to 40 ms, Snowflake CREATE words 2.8 s to 94 ms, 500,000 Spark words 649 ms to 69 ms, 100,000 Spark statements 645 ms to 114 ms.
Every verdict stays as it was: the gate tests, the analysis parity tests and the old-against-new probe over 400,000 generated texts and every connection type agree.
* docs: restate the statement analysis route's cost now the confirmation gate is linear
The route's bound comment, the known limit in docs/SECURITY.md and backlog U202 named 1.2 s for 1,000,000 units on Snowflake, which the gate no longer costs. They now state the 2026-11-05 measurements: about 45 ms for a PostgreSQL script and up to about 0.22 s for the slowest texts found, of which the gate is at most about 0.2 s, so one user can still ask for about 26 s of CPU in every minute.
U202 stays open, narrowed to that residual: its done-when asks for a charge that grows with the text or a gate near the splitter's 14 ms, and neither is met. S12 records a sibling found while measuring: the row limiter's CTE body reader is quadratic in nested bodies and recursive per level.
* test(sql): admit the parenthesised-run reader to the splitter's browser closure
The CTE list reader the splitter reaches through operative-keyword.ts now imports parenthesised.ts, which reaches only spans.ts and words.ts, so the closure the browser pays for gains one pure reader.
* test(vscode-extension): give the long EXPLAIN chain test room to finish on a slow runner
Two million-unit texts under c8 take about 1.2 s locally and went past mocha's 2 s default on a CI runner. The gate's growth is pinned by tests/unit/sql/dangerous-query.test.ts, so this test only needs time to finish, not a tighter bound.1 parent 5b67914 commit 43bac51
16 files changed
Lines changed: 1009 additions & 248 deletions
File tree
- docs
- src
- app/api/db/statement/analyze
- lib/sql
- tests/unit
- lib/agent
- sql
- vscode-extension/test/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
173 | 182 | | |
174 | 183 | | |
175 | 184 | | |
| |||
6998 | 7007 | | |
6999 | 7008 | | |
7000 | 7009 | | |
7001 | | - | |
7002 | | - | |
7003 | | - | |
| 7010 | + | |
| 7011 | + | |
| 7012 | + | |
| 7013 | + | |
| 7014 | + | |
7004 | 7015 | | |
7005 | 7016 | | |
7006 | 7017 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
863 | 863 | | |
864 | 864 | | |
865 | 865 | | |
866 | | - | |
867 | | - | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
868 | 869 | | |
869 | 870 | | |
870 | 871 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
23 | 23 | | |
24 | | - | |
25 | | - | |
26 | | - | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
0 commit comments