From 6544625175c2065bde112a507d7735b6c18e2028 Mon Sep 17 00:00:00 2001 From: arc-oai <249853679+arc-oai@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:50:41 -0700 Subject: [PATCH] Avoid quadratic SQL expression list accumulation --- src/parser/grammar.ne | 61 +++++++++++++++++++++++++++++++++---------- test/perftest.ts | 2 +- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/parser/grammar.ne b/src/parser/grammar.ne index 6b4cef1487..e97af95121 100644 --- a/src/parser/grammar.ne +++ b/src/parser/grammar.ne @@ -34,6 +34,28 @@ interface CommentAttachments { trailing?: CommentNode[]; } +interface ExpressionList { + previous: ExpressionList | null; + value: AstNode; +} + +interface PendingExpressions { + expressions: ExpressionList | null; + clauses: AstNode[]; +} + +const materializeExpressionList = (expressions: ExpressionList | null): AstNode[] => { + const result: AstNode[] = []; + for (let current = expressions; current !== null; current = current.previous) { + result.push(current.value); + } + result.reverse(); + return result; +}; + +const materializeExpressions = ({ expressions, clauses }: PendingExpressions): AstNode[] => + materializeExpressionList(expressions).concat(clauses); + const addComments = (node: AstNode, { leading, trailing }: CommentAttachments): AstNode => { if (leading?.length) { node = { ...node, leadingComments: leading }; @@ -83,14 +105,19 @@ main -> statement:* {% statement -> expressions_or_clauses (%DELIMITER | %EOF) {% ([children, [delimiter]]) => ({ type: NodeType.statement, - children, + children: materializeExpressions(children), hasSemicolon: delimiter.type === TokenType.DELIMITER, }) %} # To avoid ambiguity, plain expressions can only come before clauses -expressions_or_clauses -> free_form_sql:* clause:* {% - ([expressions, clauses]) => [...expressions, ...clauses] +expressions_or_clauses -> expression_list clause:* {% + ([expressions, clauses]) => ({ expressions, clauses }) +%} + +expression_list -> null {% () => null %} +expression_list -> expression_list free_form_sql {% + ([previous, value]) => ({ previous, value }) %} clause -> @@ -119,11 +146,13 @@ limit_clause -> %LIMIT _ expression_chain_ (%COMMA free_form_sql:+):? {% } %} -select_clause -> %RESERVED_SELECT (all_columns_asterisk free_form_sql:* | asteriskless_free_form_sql free_form_sql:*) {% +select_clause -> %RESERVED_SELECT (all_columns_asterisk expression_list | asteriskless_free_form_sql expression_list) {% ([nameToken, [exp, expressions]]) => ({ type: NodeType.clause, nameKw: toKeywordNode(nameToken), - children: [exp, ...expressions], + get children(): AstNode[] { + return [exp, ...materializeExpressionList(expressions)]; + }, }) %} select_clause -> %RESERVED_SELECT {% @@ -138,19 +167,23 @@ all_columns_asterisk -> %ASTERISK {% () => ({ type: NodeType.all_columns_asterisk }) %} -other_clause -> %RESERVED_CLAUSE free_form_sql:* {% +other_clause -> %RESERVED_CLAUSE expression_list {% ([nameToken, children]) => ({ type: NodeType.clause, nameKw: toKeywordNode(nameToken), - children, + get children(): AstNode[] { + return materializeExpressionList(children); + }, }) %} -set_operation -> %RESERVED_SET_OPERATION free_form_sql:* {% +set_operation -> %RESERVED_SET_OPERATION expression_list {% ([nameToken, children]) => ({ type: NodeType.set_operation, nameKw: toKeywordNode(nameToken), - children, + get children(): AstNode[] { + return materializeExpressionList(children); + }, }) %} @@ -232,25 +265,25 @@ function_call -> %RESERVED_FUNCTION_NAME _ parenthesis {% parenthesis -> "(" expressions_or_clauses ")" {% ([open, children, close]) => ({ type: NodeType.parenthesis, - children: children, + children: materializeExpressions(children), openParen: "(", closeParen: ")", }) %} -curly_braces -> "{" free_form_sql:* "}" {% +curly_braces -> "{" expression_list "}" {% ([open, children, close]) => ({ type: NodeType.parenthesis, - children: children, + children: materializeExpressionList(children), openParen: "{", closeParen: "}", }) %} -square_brackets -> "[" free_form_sql:* "]" {% +square_brackets -> "[" expression_list "]" {% ([open, children, close]) => ({ type: NodeType.parenthesis, - children: children, + children: materializeExpressionList(children), openParen: "[", closeParen: "]", }) diff --git a/test/perftest.ts b/test/perftest.ts index ccd64fb5a7..d814683286 100644 --- a/test/perftest.ts +++ b/test/perftest.ts @@ -10,7 +10,7 @@ describe('Performance test', () => { }); // Issue #840 - it.skip('should use less than 100 MB of additional memory to format ~100 KB of SQL', () => { + it('should use less than 100 MB of additional memory to format ~100 KB of SQL', () => { // Long list of values const values = Array(10000).fill('myid'); const sql = `SELECT ${values.join(', ')}`;