From 5a58e7bc373454e71d518227e3fb686f72563d35 Mon Sep 17 00:00:00 2001 From: grandwizard28 Date: Fri, 7 Aug 2026 23:13:27 +0530 Subject: [PATCH 1/4] Fix parsing of a parenthesized left operand of a set operator parseSelectQuery bound UNION/EXCEPT/INTERSECT before consuming the closing paren of a parenthesized select, so the operator never got a chance to bind and ((SELECT 1) UNION ALL (SELECT 2)) failed with "expected ')'". Parse a parenthesized operand by recursing into parseSelectQuery and closing the paren first, then attach the operator's right operand to the tail of the chain the left operand already carries. Statements starting with '(' now dispatch to parseSelectQuery as well. Fixes #311 --- parser/parser_query.go | 71 +- parser/parser_table.go | 2 +- parser/parser_test.go | 31 + .../select_with_parenthesized_union.sql | 70 ++ .../select_with_parenthesized_union.sql | 20 + ...t_with_parenthesized_union.sql.golden.json | 920 ++++++++++++++++++ .../query/select_with_parenthesized_union.sql | 8 + 7 files changed, 1103 insertions(+), 19 deletions(-) create mode 100644 parser/testdata/query/format/beautify/select_with_parenthesized_union.sql create mode 100644 parser/testdata/query/format/select_with_parenthesized_union.sql create mode 100644 parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json create mode 100644 parser/testdata/query/select_with_parenthesized_union.sql diff --git a/parser/parser_query.go b/parser/parser_query.go index 6b5e48c..3facc12 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -1053,48 +1053,83 @@ func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { return nil, fmt.Errorf("expected SELECT, WITH or (, got %s", p.currentTokenKind()) } - hasParen := p.tryConsumeTokenKind(TokenKindLParen) != nil - selectStmt, err := p.parseSelectStmt(p.Pos()) - if err != nil { + var selectStmt *SelectQuery + var err error + if p.tryConsumeTokenKind(TokenKindLParen) != nil { + selectStmt, err = p.parseSelectQuery(p.Pos()) + if err != nil { + return nil, err + } + if err := p.expectTokenKind(TokenKindRParen); err != nil { + return nil, err + } + } else { + selectStmt, err = p.parseSelectStmt(p.Pos()) + if err != nil { + return nil, err + } + } + + if err := p.parseSetOperation(selectStmt); err != nil { return nil, err } + + return selectStmt, nil +} + +// parseSetOperation binds a trailing UNION|EXCEPT|INTERSECT to selectStmt. +// A parenthesized left operand may already carry a set-operation chain, +// e.g. (SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3, so the right +// operand is attached to the tail of that chain. +func (p *Parser) parseSetOperation(selectStmt *SelectQuery) error { + tail := selectStmt + for { + if tail.UnionAll != nil { + tail = tail.UnionAll + } else if tail.UnionDistinct != nil { + tail = tail.UnionDistinct + } else if tail.Except != nil { + tail = tail.Except + } else if tail.Intersect != nil { + tail = tail.Intersect + } else { + break + } + } + switch { case p.tryConsumeKeywords(KeywordUnion): switch { case p.tryConsumeKeywords(KeywordAll): unionAllExpr, err := p.parseSelectQuery(p.Pos()) if err != nil { - return nil, err + return err } - selectStmt.UnionAll = unionAllExpr + tail.UnionAll = unionAllExpr case p.tryConsumeKeywords(KeywordDistinct): unionDistinctExpr, err := p.parseSelectQuery(p.Pos()) if err != nil { - return nil, err + return err } - selectStmt.UnionDistinct = unionDistinctExpr + tail.UnionDistinct = unionDistinctExpr default: - return nil, fmt.Errorf("expected ALL or DISTINCT, got %s", p.currentTokenKind()) + return fmt.Errorf("expected ALL or DISTINCT, got %s", p.currentTokenKind()) } case p.tryConsumeKeywords(KeywordExcept): exceptExpr, err := p.parseSelectQuery(p.Pos()) if err != nil { - return nil, err + return err } - selectStmt.Except = exceptExpr + tail.Except = exceptExpr case p.tryConsumeKeywords(KeywordIntersect): intersectExpr, err := p.parseSelectQuery(p.Pos()) if err != nil { - return nil, err + return err } - selectStmt.Intersect = intersectExpr + tail.Intersect = intersectExpr } - if hasParen { - if err := p.expectTokenKind(TokenKindRParen); err != nil { - return nil, err - } - } - return selectStmt, nil + + return nil } func (p *Parser) parseSelectStmt(pos Pos) (*SelectQuery, error) { // nolint: funlen diff --git a/parser/parser_table.go b/parser/parser_table.go index a29fd45..30bd1d9 100644 --- a/parser/parser_table.go +++ b/parser/parser_table.go @@ -1498,7 +1498,7 @@ func (p *Parser) parseStmt(pos Pos) (Expr, error) { p.matchKeyword(KeywordTruncate), p.matchKeyword(KeywordRename): expr, err = p.parseDDL(pos) - case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordWith): + case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordWith), p.matchTokenKind(TokenKindLParen): expr, err = p.parseSelectQuery(pos) case p.matchKeyword(KeywordDelete): expr, err = p.parseDeleteClause(pos) diff --git a/parser/parser_test.go b/parser/parser_test.go index 78a2b93..fe4f900 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -237,6 +237,10 @@ func TestParser_InvalidSyntax(t *testing.T) { "SELECT a GLOBAL", "SELECT a REGEXP", "SELECT * FROM t WHERE a AND", + // A parenthesized select must still be closed and UNION still needs + // ALL or DISTINCT + "(SELECT 1", + "(SELECT 1) UNION SELECT 2", } for _, sql := range invalidSQLs { parser := NewParser(sql) @@ -244,3 +248,30 @@ func TestParser_InvalidSyntax(t *testing.T) { require.Error(t, err, "Expected error for SQL: %s", sql) } } + +func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { + // The right operand attaches to the tail of the chain the parenthesized + // left operand already carries, so the result is one flat chain. + stmts, err := NewParser("(SELECT 1 UNION DISTINCT SELECT 2) UNION ALL SELECT 3").ParseStmts() + require.NoError(t, err) + require.Len(t, stmts, 1) + + first, ok := stmts[0].(*SelectQuery) + require.True(t, ok) + require.Nil(t, first.UnionAll) + require.NotNil(t, first.UnionDistinct) + require.NotNil(t, first.UnionDistinct.UnionAll) + require.Nil(t, first.UnionDistinct.UnionAll.UnionAll) + + stmts, err = NewParser("SELECT a FROM ((SELECT 1 AS a) UNION ALL (SELECT 2 AS a))").ParseStmts() + require.NoError(t, err) + require.Len(t, stmts, 1) + + outer, ok := stmts[0].(*SelectQuery) + require.True(t, ok) + joinTable, ok := outer.From.Expr.(*JoinTableExpr) + require.True(t, ok) + subQuery, ok := joinTable.Table.Expr.(*SubQuery) + require.True(t, ok) + require.NotNil(t, subQuery.Select.UnionAll) +} diff --git a/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql b/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql new file mode 100644 index 0000000..4b95ad1 --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql @@ -0,0 +1,70 @@ +-- Origin SQL: +SELECT a FROM ((SELECT 1 AS a) UNION ALL (SELECT 2 AS a)); +SELECT a FROM ((SELECT 1 AS a) UNION DISTINCT SELECT 2 AS a); +SELECT a FROM ((SELECT 1 AS a) EXCEPT (SELECT 2 AS a)); +SELECT a FROM ((SELECT 1 AS a) INTERSECT (SELECT 2 AS a)); +(SELECT 1 AS a) UNION ALL SELECT 2 AS a; +(SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3; +SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; +SELECT a FROM (((SELECT 1 AS a))); + + +-- Beautify SQL: +SELECT + a +FROM + (SELECT + 1 AS a + UNION ALL + SELECT + 2 AS a); +SELECT + a +FROM + (SELECT + 1 AS a + UNION DISTINCT + SELECT + 2 AS a); +SELECT + a +FROM + (SELECT + 1 AS a + EXCEPT + SELECT + 2 AS a); +SELECT + a +FROM + (SELECT + 1 AS a + INTERSECT + SELECT + 2 AS a); +SELECT + 1 AS a +UNION ALL +SELECT + 2 AS a; +SELECT + 1 +UNION ALL +SELECT + 2 +UNION ALL +SELECT + 3; +SELECT + 1 +UNION ALL +SELECT + 2 +UNION ALL +SELECT + 3; +SELECT + a +FROM + (SELECT + 1 AS a); diff --git a/parser/testdata/query/format/select_with_parenthesized_union.sql b/parser/testdata/query/format/select_with_parenthesized_union.sql new file mode 100644 index 0000000..d7081f4 --- /dev/null +++ b/parser/testdata/query/format/select_with_parenthesized_union.sql @@ -0,0 +1,20 @@ +-- Origin SQL: +SELECT a FROM ((SELECT 1 AS a) UNION ALL (SELECT 2 AS a)); +SELECT a FROM ((SELECT 1 AS a) UNION DISTINCT SELECT 2 AS a); +SELECT a FROM ((SELECT 1 AS a) EXCEPT (SELECT 2 AS a)); +SELECT a FROM ((SELECT 1 AS a) INTERSECT (SELECT 2 AS a)); +(SELECT 1 AS a) UNION ALL SELECT 2 AS a; +(SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3; +SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; +SELECT a FROM (((SELECT 1 AS a))); + + +-- Format SQL: +SELECT a FROM (SELECT 1 AS a UNION ALL SELECT 2 AS a); +SELECT a FROM (SELECT 1 AS a UNION DISTINCT SELECT 2 AS a); +SELECT a FROM (SELECT 1 AS a EXCEPT SELECT 2 AS a); +SELECT a FROM (SELECT 1 AS a INTERSECT SELECT 2 AS a); +SELECT 1 AS a UNION ALL SELECT 2 AS a; +SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; +SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; +SELECT a FROM (SELECT 1 AS a); diff --git a/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json b/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json new file mode 100644 index 0000000..4089efb --- /dev/null +++ b/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json @@ -0,0 +1,920 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 29, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 7, + "NameEnd": 8 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 9, + "Expr": { + "Table": { + "TablePos": 14, + "TableEnd": 29, + "Alias": null, + "Expr": { + "HasParen": true, + "Select": { + "SelectPos": 16, + "StatementEnd": 29, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 23, + "NumEnd": 24, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 28, + "NameEnd": 29 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 42, + "StatementEnd": 55, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 49, + "NumEnd": 50, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 54, + "NameEnd": 55 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + } + }, + "HasFinal": false + }, + "StatementEnd": 29, + "SampleRatio": null, + "HasFinal": false + } + }, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 59, + "StatementEnd": 88, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 66, + "NameEnd": 67 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 68, + "Expr": { + "Table": { + "TablePos": 73, + "TableEnd": 88, + "Alias": null, + "Expr": { + "HasParen": true, + "Select": { + "SelectPos": 75, + "StatementEnd": 88, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 82, + "NumEnd": 83, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 87, + "NameEnd": 88 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": { + "SelectPos": 105, + "StatementEnd": 118, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 112, + "NumEnd": 113, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 117, + "NameEnd": 118 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "Except": null, + "Intersect": null + } + }, + "HasFinal": false + }, + "StatementEnd": 88, + "SampleRatio": null, + "HasFinal": false + } + }, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 121, + "StatementEnd": 150, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 128, + "NameEnd": 129 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 130, + "Expr": { + "Table": { + "TablePos": 135, + "TableEnd": 150, + "Alias": null, + "Expr": { + "HasParen": true, + "Select": { + "SelectPos": 137, + "StatementEnd": 150, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 144, + "NumEnd": 145, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 149, + "NameEnd": 150 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": { + "SelectPos": 160, + "StatementEnd": 173, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 167, + "NumEnd": 168, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 172, + "NameEnd": 173 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "Intersect": null + } + }, + "HasFinal": false + }, + "StatementEnd": 150, + "SampleRatio": null, + "HasFinal": false + } + }, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 177, + "StatementEnd": 206, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 184, + "NameEnd": 185 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 186, + "Expr": { + "Table": { + "TablePos": 191, + "TableEnd": 206, + "Alias": null, + "Expr": { + "HasParen": true, + "Select": { + "SelectPos": 193, + "StatementEnd": 206, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 200, + "NumEnd": 201, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 205, + "NameEnd": 206 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": { + "SelectPos": 219, + "StatementEnd": 232, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 226, + "NumEnd": 227, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 231, + "NameEnd": 232 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + } + } + }, + "HasFinal": false + }, + "StatementEnd": 206, + "SampleRatio": null, + "HasFinal": false + } + }, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 237, + "StatementEnd": 250, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 244, + "NumEnd": 245, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 249, + "NameEnd": 250 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 262, + "StatementEnd": 275, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 269, + "NumEnd": 270, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 274, + "NameEnd": 275 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 278, + "StatementEnd": 286, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 285, + "NumEnd": 286, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 297, + "StatementEnd": 305, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 304, + "NumEnd": 305, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 317, + "StatementEnd": 325, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 324, + "NumEnd": 325, + "Literal": "3", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 327, + "StatementEnd": 335, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 334, + "NumEnd": 335, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 347, + "StatementEnd": 355, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 354, + "NumEnd": 355, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 367, + "StatementEnd": 375, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 374, + "NumEnd": 375, + "Literal": "3", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 377, + "StatementEnd": 407, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 384, + "NameEnd": 385 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 386, + "Expr": { + "Table": { + "TablePos": 391, + "TableEnd": 407, + "Alias": null, + "Expr": { + "HasParen": true, + "Select": { + "SelectPos": 394, + "StatementEnd": 407, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 401, + "NumEnd": 402, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 406, + "NameEnd": 407 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + } + }, + "HasFinal": false + }, + "StatementEnd": 407, + "SampleRatio": null, + "HasFinal": false + } + }, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + } +] \ No newline at end of file diff --git a/parser/testdata/query/select_with_parenthesized_union.sql b/parser/testdata/query/select_with_parenthesized_union.sql new file mode 100644 index 0000000..ab02d55 --- /dev/null +++ b/parser/testdata/query/select_with_parenthesized_union.sql @@ -0,0 +1,8 @@ +SELECT a FROM ((SELECT 1 AS a) UNION ALL (SELECT 2 AS a)); +SELECT a FROM ((SELECT 1 AS a) UNION DISTINCT SELECT 2 AS a); +SELECT a FROM ((SELECT 1 AS a) EXCEPT (SELECT 2 AS a)); +SELECT a FROM ((SELECT 1 AS a) INTERSECT (SELECT 2 AS a)); +(SELECT 1 AS a) UNION ALL SELECT 2 AS a; +(SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3; +SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; +SELECT a FROM (((SELECT 1 AS a))); From 55dc98bf4327b59449d4d10b0c0def10676648b8 Mon Sep 17 00:00:00 2001 From: grandwizard28 Date: Fri, 7 Aug 2026 23:52:49 +0530 Subject: [PATCH 2/4] Keep parenthesized set-operation operands as a Paren group in the AST Flattening a parenthesized operand into the existing chain changes semantics on the format round trip: ClickHouse gives INTERSECT higher precedence than UNION/EXCEPT, so (SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2 returns 2 while its flattened form returns 1 and 2. A parenthesized operand is now kept as a SelectQuery with the new Paren field wrapping the inner query, so the operator after ')' binds to the whole group and FormatSQL reproduces the parentheses. parseCTEStmt consumes the parens around a CTE body itself so CTE ASTs are unchanged, and Paren marshals with omitempty, so no existing golden changes. --- parser/ast.go | 15 +- parser/format.go | 11 + parser/parser_query.go | 45 +- parser/parser_test.go | 24 +- .../select_with_parenthesized_union.sql | 62 +- .../select_with_parenthesized_union.sql | 20 +- ...t_with_parenthesized_union.sql.golden.json | 1052 +++++++++++++---- .../query/select_with_parenthesized_union.sql | 2 + parser/walk.go | 3 + 9 files changed, 925 insertions(+), 309 deletions(-) diff --git a/parser/ast.go b/parser/ast.go index b6eb41f..81c3819 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -5267,8 +5267,14 @@ func (f *WindowFrameParam) Accept(visitor ASTVisitor) error { } type SelectQuery struct { - SelectPos Pos - StatementEnd Pos + SelectPos Pos + StatementEnd Pos + // Paren makes this node a parenthesized group wrapping the inner query: + // (SELECT 1 UNION ALL SELECT 2). When set, every other clause field is + // empty and only the set-operation fields below may be populated, binding + // an operator that follows the closing ')'. SelectPos and StatementEnd + // span the parentheses. + Paren *SelectQuery `json:",omitempty"` With *WithClause Top *TopClause HasDistinct bool @@ -5303,6 +5309,11 @@ func (s *SelectQuery) End() Pos { func (s *SelectQuery) Accept(visitor ASTVisitor) error { visitor.Enter(s) defer visitor.Leave(s) + if s.Paren != nil { + if err := s.Paren.Accept(visitor); err != nil { + return err + } + } if s.With != nil { if err := s.With.Accept(visitor); err != nil { return err diff --git a/parser/format.go b/parser/format.go index 752c746..2724ea7 100644 --- a/parser/format.go +++ b/parser/format.go @@ -2286,6 +2286,13 @@ func (s *SelectItem) FormatSQL(formatter *Formatter) { } func (s *SelectQuery) FormatSQL(formatter *Formatter) { + if s.Paren != nil { + formatter.WriteByte('(') + formatter.WriteExpr(s.Paren) + formatter.WriteByte(')') + s.formatSetOperation(formatter) + return + } if s.With != nil { formatter.WriteString("WITH") formatter.Indent() @@ -2368,6 +2375,10 @@ func (s *SelectQuery) FormatSQL(formatter *Formatter) { formatter.Break() formatter.WriteExpr(s.Format) } + s.formatSetOperation(formatter) +} + +func (s *SelectQuery) formatSetOperation(formatter *Formatter) { if s.UnionAll != nil { formatter.Break() formatter.WriteString("UNION ALL") diff --git a/parser/parser_query.go b/parser/parser_query.go index 3facc12..716b418 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -1055,14 +1055,22 @@ func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { var selectStmt *SelectQuery var err error - if p.tryConsumeTokenKind(TokenKindLParen) != nil { - selectStmt, err = p.parseSelectQuery(p.Pos()) + if lparen := p.tryConsumeTokenKind(TokenKindLParen); lparen != nil { + inner, err := p.parseSelectQuery(p.Pos()) if err != nil { return nil, err } + + rparenPos := p.Pos() if err := p.expectTokenKind(TokenKindRParen); err != nil { return nil, err } + + selectStmt = &SelectQuery{ + SelectPos: lparen.Pos, + StatementEnd: rparenPos + 1, + Paren: inner, + } } else { selectStmt, err = p.parseSelectStmt(p.Pos()) if err != nil { @@ -1078,25 +1086,9 @@ func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { } // parseSetOperation binds a trailing UNION|EXCEPT|INTERSECT to selectStmt. -// A parenthesized left operand may already carry a set-operation chain, -// e.g. (SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3, so the right -// operand is attached to the tail of that chain. +// The right operand consumes the rest of the chain by recursing into +// parseSelectQuery, so at most one operator is bound per call. func (p *Parser) parseSetOperation(selectStmt *SelectQuery) error { - tail := selectStmt - for { - if tail.UnionAll != nil { - tail = tail.UnionAll - } else if tail.UnionDistinct != nil { - tail = tail.UnionDistinct - } else if tail.Except != nil { - tail = tail.Except - } else if tail.Intersect != nil { - tail = tail.Intersect - } else { - break - } - } - switch { case p.tryConsumeKeywords(KeywordUnion): switch { @@ -1105,13 +1097,13 @@ func (p *Parser) parseSetOperation(selectStmt *SelectQuery) error { if err != nil { return err } - tail.UnionAll = unionAllExpr + selectStmt.UnionAll = unionAllExpr case p.tryConsumeKeywords(KeywordDistinct): unionDistinctExpr, err := p.parseSelectQuery(p.Pos()) if err != nil { return err } - tail.UnionDistinct = unionDistinctExpr + selectStmt.UnionDistinct = unionDistinctExpr default: return fmt.Errorf("expected ALL or DISTINCT, got %s", p.currentTokenKind()) } @@ -1120,13 +1112,13 @@ func (p *Parser) parseSetOperation(selectStmt *SelectQuery) error { if err != nil { return err } - tail.Except = exceptExpr + selectStmt.Except = exceptExpr case p.tryConsumeKeywords(KeywordIntersect): intersectExpr, err := p.parseSelectQuery(p.Pos()) if err != nil { return err } - tail.Intersect = intersectExpr + selectStmt.Intersect = intersectExpr } return nil @@ -1300,11 +1292,14 @@ func (p *Parser) parseCTEStmt(pos Pos) (*CTEStmt, error) { if err := p.expectKeyword(KeywordAs); err != nil { return nil, err } - if p.matchTokenKind(TokenKindLParen) { + if p.tryConsumeTokenKind(TokenKindLParen) != nil { selectQuery, err := p.parseSelectQuery(p.Pos()) if err != nil { return nil, err } + if err := p.expectTokenKind(TokenKindRParen); err != nil { + return nil, err + } return &CTEStmt{ CTEPos: pos, Expr: expr, diff --git a/parser/parser_test.go b/parser/parser_test.go index fe4f900..dc584b6 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -250,18 +250,18 @@ func TestParser_InvalidSyntax(t *testing.T) { } func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { - // The right operand attaches to the tail of the chain the parenthesized - // left operand already carries, so the result is one flat chain. + // A parenthesized operand becomes a Paren group, so the operator after + // ')' binds to the whole group instead of leaking into its chain. stmts, err := NewParser("(SELECT 1 UNION DISTINCT SELECT 2) UNION ALL SELECT 3").ParseStmts() require.NoError(t, err) require.Len(t, stmts, 1) - first, ok := stmts[0].(*SelectQuery) + group, ok := stmts[0].(*SelectQuery) require.True(t, ok) - require.Nil(t, first.UnionAll) - require.NotNil(t, first.UnionDistinct) - require.NotNil(t, first.UnionDistinct.UnionAll) - require.Nil(t, first.UnionDistinct.UnionAll.UnionAll) + require.NotNil(t, group.Paren) + require.NotNil(t, group.Paren.UnionDistinct) + require.NotNil(t, group.UnionAll) + require.Nil(t, group.Paren.UnionDistinct.UnionAll) stmts, err = NewParser("SELECT a FROM ((SELECT 1 AS a) UNION ALL (SELECT 2 AS a))").ParseStmts() require.NoError(t, err) @@ -273,5 +273,15 @@ func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { require.True(t, ok) subQuery, ok := joinTable.Table.Expr.(*SubQuery) require.True(t, ok) + require.NotNil(t, subQuery.Select.Paren) require.NotNil(t, subQuery.Select.UnionAll) + require.NotNil(t, subQuery.Select.UnionAll.Paren) + + // Grouping survives the round trip: ClickHouse gives INTERSECT higher + // precedence than UNION, so dropping the parens would change semantics. + sql := "(SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2" + stmts, err = NewParser(sql).ParseStmts() + require.NoError(t, err) + require.Len(t, stmts, 1) + require.Equal(t, sql, Format(stmts[0])) } diff --git a/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql b/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql index 4b95ad1..e2e37b6 100644 --- a/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql +++ b/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql @@ -7,64 +7,82 @@ SELECT a FROM ((SELECT 1 AS a) INTERSECT (SELECT 2 AS a)); (SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3; SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; SELECT a FROM (((SELECT 1 AS a))); +(SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2; +SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 1); -- Beautify SQL: SELECT a FROM - (SELECT - 1 AS a + ((SELECT + 1 AS a) UNION ALL - SELECT - 2 AS a); + (SELECT + 2 AS a)); SELECT a FROM - (SELECT - 1 AS a + ((SELECT + 1 AS a) UNION DISTINCT SELECT 2 AS a); SELECT a FROM - (SELECT - 1 AS a + ((SELECT + 1 AS a) EXCEPT - SELECT - 2 AS a); + (SELECT + 2 AS a)); SELECT a FROM - (SELECT - 1 AS a + ((SELECT + 1 AS a) INTERSECT - SELECT - 2 AS a); -SELECT - 1 AS a + (SELECT + 2 AS a)); +(SELECT + 1 AS a) UNION ALL SELECT 2 AS a; -SELECT +(SELECT 1 UNION ALL SELECT - 2 + 2) UNION ALL SELECT 3; SELECT 1 UNION ALL -SELECT - 2 +(SELECT + 2) UNION ALL SELECT 3; SELECT a FROM - (SELECT - 1 AS a); + (((SELECT + 1 AS a))); +(SELECT + 1 +UNION ALL +SELECT + 2) +INTERSECT +SELECT + 2; +SELECT + 1 +INTERSECT +(SELECT + 2 +UNION ALL +SELECT + 1); diff --git a/parser/testdata/query/format/select_with_parenthesized_union.sql b/parser/testdata/query/format/select_with_parenthesized_union.sql index d7081f4..630997b 100644 --- a/parser/testdata/query/format/select_with_parenthesized_union.sql +++ b/parser/testdata/query/format/select_with_parenthesized_union.sql @@ -7,14 +7,18 @@ SELECT a FROM ((SELECT 1 AS a) INTERSECT (SELECT 2 AS a)); (SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3; SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; SELECT a FROM (((SELECT 1 AS a))); +(SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2; +SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 1); -- Format SQL: -SELECT a FROM (SELECT 1 AS a UNION ALL SELECT 2 AS a); -SELECT a FROM (SELECT 1 AS a UNION DISTINCT SELECT 2 AS a); -SELECT a FROM (SELECT 1 AS a EXCEPT SELECT 2 AS a); -SELECT a FROM (SELECT 1 AS a INTERSECT SELECT 2 AS a); -SELECT 1 AS a UNION ALL SELECT 2 AS a; -SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; -SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3; -SELECT a FROM (SELECT 1 AS a); +SELECT a FROM ((SELECT 1 AS a) UNION ALL (SELECT 2 AS a)); +SELECT a FROM ((SELECT 1 AS a) UNION DISTINCT SELECT 2 AS a); +SELECT a FROM ((SELECT 1 AS a) EXCEPT (SELECT 2 AS a)); +SELECT a FROM ((SELECT 1 AS a) INTERSECT (SELECT 2 AS a)); +(SELECT 1 AS a) UNION ALL SELECT 2 AS a; +(SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3; +SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; +SELECT a FROM (((SELECT 1 AS a))); +(SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2; +SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 1); diff --git a/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json b/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json index 4089efb..7e8f55e 100644 --- a/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json +++ b/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json @@ -1,7 +1,7 @@ [ { "SelectPos": 0, - "StatementEnd": 29, + "StatementEnd": 30, "With": null, "Top": null, "HasDistinct": false, @@ -23,34 +23,59 @@ "Expr": { "Table": { "TablePos": 14, - "TableEnd": 29, + "TableEnd": 30, "Alias": null, "Expr": { "HasParen": true, "Select": { - "SelectPos": 16, - "StatementEnd": 29, + "SelectPos": 15, + "StatementEnd": 30, + "Paren": { + "SelectPos": 16, + "StatementEnd": 29, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 23, + "NumEnd": 24, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 28, + "NameEnd": 29 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 23, - "NumEnd": 24, - "Literal": "1", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 28, - "NameEnd": 29 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -64,29 +89,54 @@ "Settings": null, "Format": null, "UnionAll": { - "SelectPos": 42, - "StatementEnd": 55, + "SelectPos": 41, + "StatementEnd": 56, + "Paren": { + "SelectPos": 42, + "StatementEnd": 55, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 49, + "NumEnd": 50, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 54, + "NameEnd": 55 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 49, - "NumEnd": 50, - "Literal": "2", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 54, - "NameEnd": 55 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -111,7 +161,7 @@ }, "HasFinal": false }, - "StatementEnd": 29, + "StatementEnd": 30, "SampleRatio": null, "HasFinal": false } @@ -134,7 +184,7 @@ }, { "SelectPos": 59, - "StatementEnd": 88, + "StatementEnd": 89, "With": null, "Top": null, "HasDistinct": false, @@ -156,34 +206,59 @@ "Expr": { "Table": { "TablePos": 73, - "TableEnd": 88, + "TableEnd": 89, "Alias": null, "Expr": { "HasParen": true, "Select": { - "SelectPos": 75, - "StatementEnd": 88, + "SelectPos": 74, + "StatementEnd": 89, + "Paren": { + "SelectPos": 75, + "StatementEnd": 88, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 82, + "NumEnd": 83, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 87, + "NameEnd": 88 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 82, - "NumEnd": 83, - "Literal": "1", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 87, - "NameEnd": 88 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -244,7 +319,7 @@ }, "HasFinal": false }, - "StatementEnd": 88, + "StatementEnd": 89, "SampleRatio": null, "HasFinal": false } @@ -267,7 +342,7 @@ }, { "SelectPos": 121, - "StatementEnd": 150, + "StatementEnd": 151, "With": null, "Top": null, "HasDistinct": false, @@ -289,34 +364,59 @@ "Expr": { "Table": { "TablePos": 135, - "TableEnd": 150, + "TableEnd": 151, "Alias": null, "Expr": { "HasParen": true, "Select": { - "SelectPos": 137, - "StatementEnd": 150, + "SelectPos": 136, + "StatementEnd": 151, + "Paren": { + "SelectPos": 137, + "StatementEnd": 150, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 144, + "NumEnd": 145, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 149, + "NameEnd": 150 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 144, - "NumEnd": 145, - "Literal": "1", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 149, - "NameEnd": 150 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -332,29 +432,54 @@ "UnionAll": null, "UnionDistinct": null, "Except": { - "SelectPos": 160, - "StatementEnd": 173, + "SelectPos": 159, + "StatementEnd": 174, + "Paren": { + "SelectPos": 160, + "StatementEnd": 173, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 167, + "NumEnd": 168, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 172, + "NameEnd": 173 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 167, - "NumEnd": 168, - "Literal": "2", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 172, - "NameEnd": 173 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -377,7 +502,7 @@ }, "HasFinal": false }, - "StatementEnd": 150, + "StatementEnd": 151, "SampleRatio": null, "HasFinal": false } @@ -400,7 +525,7 @@ }, { "SelectPos": 177, - "StatementEnd": 206, + "StatementEnd": 207, "With": null, "Top": null, "HasDistinct": false, @@ -422,34 +547,59 @@ "Expr": { "Table": { "TablePos": 191, - "TableEnd": 206, + "TableEnd": 207, "Alias": null, "Expr": { "HasParen": true, "Select": { - "SelectPos": 193, - "StatementEnd": 206, + "SelectPos": 192, + "StatementEnd": 207, + "Paren": { + "SelectPos": 193, + "StatementEnd": 206, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 200, + "NumEnd": 201, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 205, + "NameEnd": 206 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 200, - "NumEnd": 201, - "Literal": "1", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 205, - "NameEnd": 206 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -466,29 +616,54 @@ "UnionDistinct": null, "Except": null, "Intersect": { - "SelectPos": 219, - "StatementEnd": 232, + "SelectPos": 218, + "StatementEnd": 233, + "Paren": { + "SelectPos": 219, + "StatementEnd": 232, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 226, + "NumEnd": 227, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 231, + "NameEnd": 232 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 226, - "NumEnd": 227, - "Literal": "2", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 231, - "NameEnd": 232 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -510,7 +685,7 @@ }, "HasFinal": false }, - "StatementEnd": 206, + "StatementEnd": 207, "SampleRatio": null, "HasFinal": false } @@ -532,29 +707,54 @@ "Intersect": null }, { - "SelectPos": 237, - "StatementEnd": 250, + "SelectPos": 236, + "StatementEnd": 251, + "Paren": { + "SelectPos": 237, + "StatementEnd": 250, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 244, + "NumEnd": 245, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 249, + "NameEnd": 250 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 244, - "NumEnd": 245, - "Literal": "1", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 249, - "NameEnd": 250 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -613,39 +813,11 @@ "Intersect": null }, { - "SelectPos": 278, - "StatementEnd": 286, - "With": null, - "Top": null, - "HasDistinct": false, - "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 285, - "NumEnd": 286, - "Literal": "1", - "Base": 10 - }, - "Modifiers": [], - "Alias": null - } - ], - "From": null, - "Window": null, - "Prewhere": null, - "Where": null, - "GroupBy": null, - "WithTotal": false, - "Having": null, - "OrderBy": null, - "LimitBy": null, - "Limit": null, - "Settings": null, - "Format": null, - "UnionAll": { - "SelectPos": 297, - "StatementEnd": 305, + "SelectPos": 277, + "StatementEnd": 306, + "Paren": { + "SelectPos": 278, + "StatementEnd": 286, "With": null, "Top": null, "HasDistinct": false, @@ -653,9 +825,9 @@ "SelectItems": [ { "Expr": { - "NumPos": 304, - "NumEnd": 305, - "Literal": "2", + "NumPos": 285, + "NumEnd": 286, + "Literal": "1", "Base": 10 }, "Modifiers": [], @@ -675,8 +847,8 @@ "Settings": null, "Format": null, "UnionAll": { - "SelectPos": 317, - "StatementEnd": 325, + "SelectPos": 297, + "StatementEnd": 305, "With": null, "Top": null, "HasDistinct": false, @@ -684,9 +856,9 @@ "SelectItems": [ { "Expr": { - "NumPos": 324, - "NumEnd": 325, - "Literal": "3", + "NumPos": 304, + "NumEnd": 305, + "Literal": "2", "Base": 10 }, "Modifiers": [], @@ -714,10 +886,63 @@ "Except": null, "Intersect": null }, - "UnionDistinct": null, - "Except": null, - "Intersect": null - }, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": null, + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 317, + "StatementEnd": 325, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 324, + "NumEnd": 325, + "Literal": "3", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, { "SelectPos": 327, "StatementEnd": 335, @@ -750,24 +975,49 @@ "Settings": null, "Format": null, "UnionAll": { - "SelectPos": 347, - "StatementEnd": 355, + "SelectPos": 346, + "StatementEnd": 356, + "Paren": { + "SelectPos": 347, + "StatementEnd": 355, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 354, + "NumEnd": 355, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 354, - "NumEnd": 355, - "Literal": "2", - "Base": 10 - }, - "Modifiers": [], - "Alias": null - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -826,7 +1076,7 @@ }, { "SelectPos": 377, - "StatementEnd": 407, + "StatementEnd": 409, "With": null, "Top": null, "HasDistinct": false, @@ -848,34 +1098,84 @@ "Expr": { "Table": { "TablePos": 391, - "TableEnd": 407, + "TableEnd": 409, "Alias": null, "Expr": { "HasParen": true, "Select": { - "SelectPos": 394, - "StatementEnd": 407, + "SelectPos": 392, + "StatementEnd": 409, + "Paren": { + "SelectPos": 393, + "StatementEnd": 408, + "Paren": { + "SelectPos": 394, + "StatementEnd": 407, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 401, + "NumEnd": 402, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 406, + "NameEnd": 407 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": null, + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, "With": null, "Top": null, "HasDistinct": false, "DistinctOn": null, - "SelectItems": [ - { - "Expr": { - "NumPos": 401, - "NumEnd": 402, - "Literal": "1", - "Base": 10 - }, - "Modifiers": [], - "Alias": { - "Name": "a", - "QuoteType": 1, - "NamePos": 406, - "NameEnd": 407 - } - } - ], + "SelectItems": null, "From": null, "Window": null, "Prewhere": null, @@ -896,7 +1196,7 @@ }, "HasFinal": false }, - "StatementEnd": 407, + "StatementEnd": 409, "SampleRatio": null, "HasFinal": false } @@ -916,5 +1216,267 @@ "UnionDistinct": null, "Except": null, "Intersect": null + }, + { + "SelectPos": 412, + "StatementEnd": 441, + "Paren": { + "SelectPos": 413, + "StatementEnd": 421, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 420, + "NumEnd": 421, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 432, + "StatementEnd": 440, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 439, + "NumEnd": 440, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": null, + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": { + "SelectPos": 452, + "StatementEnd": 460, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 459, + "NumEnd": 460, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + } + }, + { + "SelectPos": 462, + "StatementEnd": 470, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 469, + "NumEnd": 470, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": { + "SelectPos": 481, + "StatementEnd": 510, + "Paren": { + "SelectPos": 482, + "StatementEnd": 490, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 489, + "NumEnd": 490, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 501, + "StatementEnd": 509, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 508, + "NumEnd": 509, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": null, + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + } } ] \ No newline at end of file diff --git a/parser/testdata/query/select_with_parenthesized_union.sql b/parser/testdata/query/select_with_parenthesized_union.sql index ab02d55..c121090 100644 --- a/parser/testdata/query/select_with_parenthesized_union.sql +++ b/parser/testdata/query/select_with_parenthesized_union.sql @@ -6,3 +6,5 @@ SELECT a FROM ((SELECT 1 AS a) INTERSECT (SELECT 2 AS a)); (SELECT 1 UNION ALL SELECT 2) UNION ALL SELECT 3; SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; SELECT a FROM (((SELECT 1 AS a))); +(SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2; +SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 1); diff --git a/parser/walk.go b/parser/walk.go index e129a35..97a38ae 100644 --- a/parser/walk.go +++ b/parser/walk.go @@ -19,6 +19,9 @@ func Walk(node Expr, fn WalkFunc) bool { switch n := node.(type) { case *SelectQuery: + if !Walk(n.Paren, fn) { + return false + } if !Walk(n.With, fn) { return false } From 9ab2484c219b0818ef68003ea475862fdb9f975a Mon Sep 17 00:00:00 2001 From: grandwizard28 Date: Sat, 8 Aug 2026 00:22:27 +0530 Subject: [PATCH 3/4] Bind SETTINGS and FORMAT after a parenthesized select group ClickHouse allows SETTINGS and FORMAT after the closing paren of a parenthesized select: (SELECT 1) SETTINGS max_threads=1 FORMAT JSONEachRow. The Paren group only bound set operators, so SETTINGS was rejected and a trailing FORMAT was consumed by parseStmt's discard call and silently dropped from the AST. Parse SETTINGS and FORMAT onto the group's own fields, mirroring the tail of parseSelectStmt, and have FormatSQL emit them. Once either clause is bound, no set operator may follow, matching ClickHouse, which rejects (SELECT 1) SETTINGS max_threads=1 UNION ALL SELECT 2. --- parser/ast.go | 6 +- parser/format.go | 8 + parser/parser_query.go | 25 +++ parser/parser_test.go | 14 ++ .../select_with_parenthesized_union.sql | 14 ++ .../select_with_parenthesized_union.sql | 4 + ...t_with_parenthesized_union.sql.golden.json | 205 ++++++++++++++++++ .../query/select_with_parenthesized_union.sql | 2 + 8 files changed, 275 insertions(+), 3 deletions(-) diff --git a/parser/ast.go b/parser/ast.go index 81c3819..a96c0d8 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -5271,9 +5271,9 @@ type SelectQuery struct { StatementEnd Pos // Paren makes this node a parenthesized group wrapping the inner query: // (SELECT 1 UNION ALL SELECT 2). When set, every other clause field is - // empty and only the set-operation fields below may be populated, binding - // an operator that follows the closing ')'. SelectPos and StatementEnd - // span the parentheses. + // empty except Settings, Format and the set-operation fields below, which + // bind clauses that follow the closing ')'. SelectPos and StatementEnd + // span the parentheses and any trailing clause. Paren *SelectQuery `json:",omitempty"` With *WithClause Top *TopClause diff --git a/parser/format.go b/parser/format.go index 2724ea7..17cc764 100644 --- a/parser/format.go +++ b/parser/format.go @@ -2290,6 +2290,14 @@ func (s *SelectQuery) FormatSQL(formatter *Formatter) { formatter.WriteByte('(') formatter.WriteExpr(s.Paren) formatter.WriteByte(')') + if s.Settings != nil { + formatter.Break() + formatter.WriteExpr(s.Settings) + } + if s.Format != nil { + formatter.Break() + formatter.WriteExpr(s.Format) + } s.formatSetOperation(formatter) return } diff --git a/parser/parser_query.go b/parser/parser_query.go index 716b418..c0235f1 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -1071,6 +1071,31 @@ func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { StatementEnd: rparenPos + 1, Paren: inner, } + + settings, err := p.tryParseSettingsClause(p.Pos()) + if err != nil { + return nil, err + } + if settings != nil { + selectStmt.Settings = settings + selectStmt.StatementEnd = settings.End() + } + + format, err := p.tryParseFormat(p.Pos()) + if err != nil { + return nil, err + } + if format != nil { + selectStmt.Format = format + selectStmt.StatementEnd = format.End() + } + + // ClickHouse allows a set operator after ')' only when no SETTINGS + // or FORMAT was consumed: (SELECT 1) SETTINGS a=1 UNION ALL SELECT 2 + // is a syntax error there. + if settings != nil || format != nil { + return selectStmt, nil + } } else { selectStmt, err = p.parseSelectStmt(p.Pos()) if err != nil { diff --git a/parser/parser_test.go b/parser/parser_test.go index dc584b6..92135b4 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -241,6 +241,9 @@ func TestParser_InvalidSyntax(t *testing.T) { // ALL or DISTINCT "(SELECT 1", "(SELECT 1) UNION SELECT 2", + // ClickHouse rejects a set operator once SETTINGS is bound to a + // parenthesized group + "(SELECT 1) SETTINGS max_threads=1 UNION ALL SELECT 2", } for _, sql := range invalidSQLs { parser := NewParser(sql) @@ -284,4 +287,15 @@ func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { require.NoError(t, err) require.Len(t, stmts, 1) require.Equal(t, sql, Format(stmts[0])) + + // SETTINGS and FORMAT after ')' bind to the group. + stmts, err = NewParser("(SELECT 1) SETTINGS max_threads=1 FORMAT JSONEachRow").ParseStmts() + require.NoError(t, err) + require.Len(t, stmts, 1) + + group, ok = stmts[0].(*SelectQuery) + require.True(t, ok) + require.NotNil(t, group.Paren) + require.NotNil(t, group.Settings) + require.NotNil(t, group.Format) } diff --git a/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql b/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql index e2e37b6..7c775b9 100644 --- a/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql +++ b/parser/testdata/query/format/beautify/select_with_parenthesized_union.sql @@ -9,6 +9,8 @@ SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; SELECT a FROM (((SELECT 1 AS a))); (SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2; SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 1); +(SELECT 1) SETTINGS max_threads=1; +(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1 FORMAT JSONEachRow; -- Beautify SQL: @@ -86,3 +88,15 @@ INTERSECT UNION ALL SELECT 1); +(SELECT + 1) +SETTINGS + max_threads=1; +(SELECT + 1 +UNION ALL +SELECT + 2) +SETTINGS + max_threads=1 +FORMAT JSONEachRow; diff --git a/parser/testdata/query/format/select_with_parenthesized_union.sql b/parser/testdata/query/format/select_with_parenthesized_union.sql index 630997b..52e14ca 100644 --- a/parser/testdata/query/format/select_with_parenthesized_union.sql +++ b/parser/testdata/query/format/select_with_parenthesized_union.sql @@ -9,6 +9,8 @@ SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; SELECT a FROM (((SELECT 1 AS a))); (SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2; SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 1); +(SELECT 1) SETTINGS max_threads=1; +(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1 FORMAT JSONEachRow; -- Format SQL: @@ -22,3 +24,5 @@ SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; SELECT a FROM (((SELECT 1 AS a))); (SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2; SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 1); +(SELECT 1) SETTINGS max_threads=1; +(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1 FORMAT JSONEachRow; diff --git a/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json b/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json index 7e8f55e..dea07aa 100644 --- a/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json +++ b/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json @@ -1478,5 +1478,210 @@ "Except": null, "Intersect": null } + }, + { + "SelectPos": 512, + "StatementEnd": 545, + "Paren": { + "SelectPos": 513, + "StatementEnd": 521, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 520, + "NumEnd": 521, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": null, + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": { + "SettingsPos": 523, + "ListEnd": 545, + "Items": [ + { + "SettingsPos": 532, + "Name": { + "Name": "max_threads", + "QuoteType": 1, + "NamePos": 532, + "NameEnd": 543 + }, + "Expr": { + "NumPos": 544, + "NumEnd": 545, + "Literal": "1", + "Base": 10 + } + } + ] + }, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 547, + "StatementEnd": 618, + "Paren": { + "SelectPos": 548, + "StatementEnd": 556, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 555, + "NumEnd": 556, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": { + "SelectPos": 567, + "StatementEnd": 575, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 574, + "NumEnd": 575, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": null, + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": { + "SettingsPos": 577, + "ListEnd": 599, + "Items": [ + { + "SettingsPos": 586, + "Name": { + "Name": "max_threads", + "QuoteType": 1, + "NamePos": 586, + "NameEnd": 597 + }, + "Expr": { + "NumPos": 598, + "NumEnd": 599, + "Literal": "1", + "Base": 10 + } + } + ] + }, + "Format": { + "FormatPos": 600, + "Format": { + "Name": "JSONEachRow", + "QuoteType": 1, + "NamePos": 607, + "NameEnd": 618 + } + }, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null } ] \ No newline at end of file diff --git a/parser/testdata/query/select_with_parenthesized_union.sql b/parser/testdata/query/select_with_parenthesized_union.sql index c121090..766d02c 100644 --- a/parser/testdata/query/select_with_parenthesized_union.sql +++ b/parser/testdata/query/select_with_parenthesized_union.sql @@ -8,3 +8,5 @@ SELECT 1 UNION ALL (SELECT 2) UNION ALL SELECT 3; SELECT a FROM (((SELECT 1 AS a))); (SELECT 1 UNION ALL SELECT 2) INTERSECT SELECT 2; SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 1); +(SELECT 1) SETTINGS max_threads=1; +(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1 FORMAT JSONEachRow; From b58d1368e63dcfd75130334e7e842a7c43eb5c5c Mon Sep 17 00:00:00 2001 From: grandwizard28 Date: Tue, 11 Aug 2026 00:43:36 +0530 Subject: [PATCH 4/4] Rename SelectQuery.Paren to InnerQuery --- parser/ast.go | 8 ++--- parser/format.go | 4 +-- parser/parser_query.go | 2 +- parser/parser_test.go | 14 ++++---- ...t_with_parenthesized_union.sql.golden.json | 32 +++++++++---------- parser/walk.go | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/parser/ast.go b/parser/ast.go index a96c0d8..3566641 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -5269,12 +5269,12 @@ func (f *WindowFrameParam) Accept(visitor ASTVisitor) error { type SelectQuery struct { SelectPos Pos StatementEnd Pos - // Paren makes this node a parenthesized group wrapping the inner query: + // InnerQuery makes this node a parenthesized group wrapping that query: // (SELECT 1 UNION ALL SELECT 2). When set, every other clause field is // empty except Settings, Format and the set-operation fields below, which // bind clauses that follow the closing ')'. SelectPos and StatementEnd // span the parentheses and any trailing clause. - Paren *SelectQuery `json:",omitempty"` + InnerQuery *SelectQuery `json:",omitempty"` With *WithClause Top *TopClause HasDistinct bool @@ -5309,8 +5309,8 @@ func (s *SelectQuery) End() Pos { func (s *SelectQuery) Accept(visitor ASTVisitor) error { visitor.Enter(s) defer visitor.Leave(s) - if s.Paren != nil { - if err := s.Paren.Accept(visitor); err != nil { + if s.InnerQuery != nil { + if err := s.InnerQuery.Accept(visitor); err != nil { return err } } diff --git a/parser/format.go b/parser/format.go index 17cc764..1f55d2d 100644 --- a/parser/format.go +++ b/parser/format.go @@ -2286,9 +2286,9 @@ func (s *SelectItem) FormatSQL(formatter *Formatter) { } func (s *SelectQuery) FormatSQL(formatter *Formatter) { - if s.Paren != nil { + if s.InnerQuery != nil { formatter.WriteByte('(') - formatter.WriteExpr(s.Paren) + formatter.WriteExpr(s.InnerQuery) formatter.WriteByte(')') if s.Settings != nil { formatter.Break() diff --git a/parser/parser_query.go b/parser/parser_query.go index c0235f1..128d008 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -1069,7 +1069,7 @@ func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { selectStmt = &SelectQuery{ SelectPos: lparen.Pos, StatementEnd: rparenPos + 1, - Paren: inner, + InnerQuery: inner, } settings, err := p.tryParseSettingsClause(p.Pos()) diff --git a/parser/parser_test.go b/parser/parser_test.go index 92135b4..142c4e8 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -253,7 +253,7 @@ func TestParser_InvalidSyntax(t *testing.T) { } func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { - // A parenthesized operand becomes a Paren group, so the operator after + // A parenthesized operand becomes a group node, so the operator after // ')' binds to the whole group instead of leaking into its chain. stmts, err := NewParser("(SELECT 1 UNION DISTINCT SELECT 2) UNION ALL SELECT 3").ParseStmts() require.NoError(t, err) @@ -261,10 +261,10 @@ func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { group, ok := stmts[0].(*SelectQuery) require.True(t, ok) - require.NotNil(t, group.Paren) - require.NotNil(t, group.Paren.UnionDistinct) + require.NotNil(t, group.InnerQuery) + require.NotNil(t, group.InnerQuery.UnionDistinct) require.NotNil(t, group.UnionAll) - require.Nil(t, group.Paren.UnionDistinct.UnionAll) + require.Nil(t, group.InnerQuery.UnionDistinct.UnionAll) stmts, err = NewParser("SELECT a FROM ((SELECT 1 AS a) UNION ALL (SELECT 2 AS a))").ParseStmts() require.NoError(t, err) @@ -276,9 +276,9 @@ func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { require.True(t, ok) subQuery, ok := joinTable.Table.Expr.(*SubQuery) require.True(t, ok) - require.NotNil(t, subQuery.Select.Paren) + require.NotNil(t, subQuery.Select.InnerQuery) require.NotNil(t, subQuery.Select.UnionAll) - require.NotNil(t, subQuery.Select.UnionAll.Paren) + require.NotNil(t, subQuery.Select.UnionAll.InnerQuery) // Grouping survives the round trip: ClickHouse gives INTERSECT higher // precedence than UNION, so dropping the parens would change semantics. @@ -295,7 +295,7 @@ func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { group, ok = stmts[0].(*SelectQuery) require.True(t, ok) - require.NotNil(t, group.Paren) + require.NotNil(t, group.InnerQuery) require.NotNil(t, group.Settings) require.NotNil(t, group.Format) } diff --git a/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json b/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json index dea07aa..c86e647 100644 --- a/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json +++ b/parser/testdata/query/output/select_with_parenthesized_union.sql.golden.json @@ -30,7 +30,7 @@ "Select": { "SelectPos": 15, "StatementEnd": 30, - "Paren": { + "InnerQuery": { "SelectPos": 16, "StatementEnd": 29, "With": null, @@ -91,7 +91,7 @@ "UnionAll": { "SelectPos": 41, "StatementEnd": 56, - "Paren": { + "InnerQuery": { "SelectPos": 42, "StatementEnd": 55, "With": null, @@ -213,7 +213,7 @@ "Select": { "SelectPos": 74, "StatementEnd": 89, - "Paren": { + "InnerQuery": { "SelectPos": 75, "StatementEnd": 88, "With": null, @@ -371,7 +371,7 @@ "Select": { "SelectPos": 136, "StatementEnd": 151, - "Paren": { + "InnerQuery": { "SelectPos": 137, "StatementEnd": 150, "With": null, @@ -434,7 +434,7 @@ "Except": { "SelectPos": 159, "StatementEnd": 174, - "Paren": { + "InnerQuery": { "SelectPos": 160, "StatementEnd": 173, "With": null, @@ -554,7 +554,7 @@ "Select": { "SelectPos": 192, "StatementEnd": 207, - "Paren": { + "InnerQuery": { "SelectPos": 193, "StatementEnd": 206, "With": null, @@ -618,7 +618,7 @@ "Intersect": { "SelectPos": 218, "StatementEnd": 233, - "Paren": { + "InnerQuery": { "SelectPos": 219, "StatementEnd": 232, "With": null, @@ -709,7 +709,7 @@ { "SelectPos": 236, "StatementEnd": 251, - "Paren": { + "InnerQuery": { "SelectPos": 237, "StatementEnd": 250, "With": null, @@ -815,7 +815,7 @@ { "SelectPos": 277, "StatementEnd": 306, - "Paren": { + "InnerQuery": { "SelectPos": 278, "StatementEnd": 286, "With": null, @@ -977,7 +977,7 @@ "UnionAll": { "SelectPos": 346, "StatementEnd": 356, - "Paren": { + "InnerQuery": { "SelectPos": 347, "StatementEnd": 355, "With": null, @@ -1105,10 +1105,10 @@ "Select": { "SelectPos": 392, "StatementEnd": 409, - "Paren": { + "InnerQuery": { "SelectPos": 393, "StatementEnd": 408, - "Paren": { + "InnerQuery": { "SelectPos": 394, "StatementEnd": 407, "With": null, @@ -1220,7 +1220,7 @@ { "SelectPos": 412, "StatementEnd": 441, - "Paren": { + "InnerQuery": { "SelectPos": 413, "StatementEnd": 421, "With": null, @@ -1385,7 +1385,7 @@ "Intersect": { "SelectPos": 481, "StatementEnd": 510, - "Paren": { + "InnerQuery": { "SelectPos": 482, "StatementEnd": 490, "With": null, @@ -1482,7 +1482,7 @@ { "SelectPos": 512, "StatementEnd": 545, - "Paren": { + "InnerQuery": { "SelectPos": 513, "StatementEnd": 521, "With": null, @@ -1563,7 +1563,7 @@ { "SelectPos": 547, "StatementEnd": 618, - "Paren": { + "InnerQuery": { "SelectPos": 548, "StatementEnd": 556, "With": null, diff --git a/parser/walk.go b/parser/walk.go index 97a38ae..67ff901 100644 --- a/parser/walk.go +++ b/parser/walk.go @@ -19,7 +19,7 @@ func Walk(node Expr, fn WalkFunc) bool { switch n := node.(type) { case *SelectQuery: - if !Walk(n.Paren, fn) { + if !Walk(n.InnerQuery, fn) { return false } if !Walk(n.With, fn) {