From 20e8c8e512539e2d0005711ba253551b4d522883 Mon Sep 17 00:00:00 2001 From: grandwizard28 Date: Wed, 29 Jul 2026 23:30:38 +0530 Subject: [PATCH 1/2] Fix parsing of the GLOBAL/LOCAL join locality and GLOBAL NOT IN (#292) GLOBAL and LOCAL were consumed as if they were join types, so any real join type after them was rejected and the two forms that did parse lost the keyword when formatting. Parse the locality as a prefix and continue into parseJoinOp, keeping it in the join modifiers. GLOBAL also claimed IN's precedence unconditionally, so a GLOBAL that started the next join was read as an operator, and parseInfix demanded IN immediately, leaving GLOBAL NOT IN with no path. ARRAY JOIN takes no locality, and neither does LOCAL IN, matching ClickHouse 26.8.1.337. --- parser/parser_column.go | 19 +- parser/parser_query.go | 35 +- parser/parser_test.go | 14 + parser/precedence_test.go | 72 + .../select_with_global_join_locality.sql | 112 ++ .../select_with_global_join_locality.sql | 39 + ..._with_global_join_locality.sql.golden.json | 1756 +++++++++++++++++ .../select_with_global_join_locality.sql | 23 + 8 files changed, 2065 insertions(+), 5 deletions(-) create mode 100644 parser/testdata/query/format/beautify/select_with_global_join_locality.sql create mode 100644 parser/testdata/query/format/select_with_global_join_locality.sql create mode 100644 parser/testdata/query/output/select_with_global_join_locality.sql.golden.json create mode 100644 parser/testdata/query/select_with_global_join_locality.sql diff --git a/parser/parser_column.go b/parser/parser_column.go index c45fde6..99d9991 100644 --- a/parser/parser_column.go +++ b/parser/parser_column.go @@ -79,8 +79,14 @@ func (p *Parser) getNextPrecedence() int { case p.matchKeyword(KeywordIn): return precedenceIn case p.matchKeyword(KeywordGlobal): - // GLOBAL is only valid before IN, so it binds like IN itself. - return precedenceIn + // GLOBAL is an operator only before IN/NOT IN, where it binds like IN + // itself. Anywhere else it belongs to the enclosing clause — a join + // locality in `ON a = b GLOBAL LEFT JOIN c` — and must not bind here. + if p.peekKeyword(KeywordIn) || p.peekKeyword(KeywordNot) { + return precedenceIn + } + + return PrecedenceUnknown case p.matchTokenKind(TokenKindQuestionMark): return PrecedenceQuery default: @@ -159,16 +165,23 @@ func (p *Parser) parseInfix(expr Expr, precedence int) (Expr, error) { return p.parseBetweenClause(expr, false) case p.matchKeyword(KeywordGlobal): _ = p.lexer.consumeToken() + negated := p.tryConsumeKeywords(KeywordNot) if p.expectKeyword(KeywordIn) != nil { return nil, fmt.Errorf("expected IN after GLOBAL, got %s", p.currentTokenKind()) } + + op := TokenKind("GLOBAL IN") + if negated { + op = "GLOBAL NOT IN" + } + rightExpr, err := p.parseSubExpr(p.Pos(), precedence) if err != nil { return nil, err } return &BinaryOperation{ LeftExpr: expr, - Operation: "GLOBAL IN", + Operation: op, RightExpr: rightExpr, }, nil case p.matchTokenKind(TokenKindDot): diff --git a/parser/parser_query.go b/parser/parser_query.go index 17c0294..ab66ec5 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -279,12 +279,43 @@ func (p *Parser) parseJoinTableExpr(_ Pos) (Expr, error) { } } +// parseJoinLocality consumes a GLOBAL or LOCAL join locality together with the +// join type that follows it, returning them as join modifiers. It returns a nil +// slice when the keyword is not followed by a join, leaving the caller to treat +// the clause as something other than a join. +func (p *Parser) parseJoinLocality(pos Pos) ([]string, error) { + locality := p.current().String + _ = p.lexer.consumeToken() + + joinOp := p.parseJoinOp(pos) + if len(joinOp) == 0 && !p.matchKeyword(KeywordJoin) { + return nil, nil + } + + // ARRAY JOIN reads a column list rather than a distributed table, so it has + // no locality. + if slices.Contains(joinOp, KeywordArray) { + return nil, fmt.Errorf("%s cannot be combined with ARRAY JOIN", locality) + } + + return append([]string{locality}, joinOp...), nil +} + func (p *Parser) parseJoinRightExpr(pos Pos) (expr Expr, err error) { var rightExpr Expr var modifiers []string switch { - case p.tryConsumeKeywords(KeywordGlobal): - case p.tryConsumeKeywords(KeywordLocal): + case p.matchOneOfKeywords(KeywordGlobal, KeywordLocal): + // GLOBAL/LOCAL only says how the right-hand table is distributed, so the + // join type still follows it: `GLOBAL LEFT JOIN` is a LEFT join. + modifiers, err = p.parseJoinLocality(p.Pos()) + if err != nil { + return nil, err + } + + if modifiers == nil { + return nil, nil + } case p.tryConsumeTokenKind(TokenKindComma) != nil: return p.parseJoinExpr(p.Pos()) default: diff --git a/parser/parser_test.go b/parser/parser_test.go index 87218dd..c958a63 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -186,6 +186,20 @@ func TestParser_InvalidSyntax(t *testing.T) { // Invalid ARRAY JOIN types (only ARRAY JOIN, LEFT ARRAY JOIN, and INNER ARRAY JOIN are valid) "SELECT * FROM t RIGHT ARRAY JOIN arr AS a", // RIGHT ARRAY JOIN not supported "SELECT * FROM t FULL ARRAY JOIN arr AS a", // FULL ARRAY JOIN not supported + // ARRAY JOIN reads a column list, so it has no GLOBAL/LOCAL locality + "SELECT * FROM t GLOBAL ARRAY JOIN arr AS a", + "SELECT * FROM t LOCAL LEFT ARRAY JOIN arr AS a", + // A join takes at most one locality, and it precedes the join type + "SELECT * FROM t1 GLOBAL LOCAL JOIN t2 ON t1.a = t2.a", + "SELECT * FROM t1 LEFT GLOBAL JOIN t2 ON t1.a = t2.a", + // GLOBAL is an operator only before IN/NOT IN, and LOCAL is never one + "SELECT * FROM t WHERE a GLOBAL LIKE 'x'", + "SELECT * FROM t WHERE a GLOBAL NOT LIKE 'x'", + "SELECT * FROM t WHERE a GLOBAL BETWEEN 1 AND 2", + "SELECT * FROM t WHERE a NOT GLOBAL IN (SELECT 1)", + "SELECT * FROM t WHERE a GLOBAL GLOBAL IN (SELECT 1)", + "SELECT * FROM t WHERE a LOCAL IN (SELECT 1)", + "SELECT * FROM t WHERE a LOCAL NOT IN (SELECT 1)", "00e1d", // invalid number that leaves curToken nil "CREATE--", // trailing comment pushes p.Pos() past end of input (wrapError out-of-range) // Inputs that previously caused a nil-pointer dereference while diff --git a/parser/precedence_test.go b/parser/precedence_test.go index 69e6872..87bea02 100644 --- a/parser/precedence_test.go +++ b/parser/precedence_test.go @@ -143,3 +143,75 @@ func TestIntersect(t *testing.T) { require.NotNil(t, selectQuery.Intersect) require.Equal(t, "SELECT 1 INTERSECT SELECT 2", Format(stmts[0])) } + +func TestGlobalJoinLocalityPrefixesTheJoinType(t *testing.T) { + // GLOBAL/LOCAL only says how the right-hand table is distributed, so the + // join type still follows it and both have to survive into the modifiers. + for _, tc := range []struct { + sql string + modifiers []string + }{ + {"SELECT * FROM t1 GLOBAL JOIN t2 ON t1.a = t2.a", []string{"GLOBAL", "JOIN"}}, + {"SELECT * FROM t1 GLOBAL INNER JOIN t2 ON t1.a = t2.a", []string{"GLOBAL", "INNER", "JOIN"}}, + {"SELECT * FROM t1 GLOBAL LEFT OUTER JOIN t2 ON t1.a = t2.a", []string{"GLOBAL", "LEFT", "OUTER", "JOIN"}}, + {"SELECT * FROM t1 GLOBAL ANY LEFT JOIN t2 ON t1.a = t2.a", []string{"GLOBAL", "ANY", "LEFT", "JOIN"}}, + {"SELECT * FROM t1 GLOBAL CROSS JOIN t2", []string{"GLOBAL", "CROSS", "JOIN"}}, + {"SELECT * FROM t1 AS x LOCAL FULL JOIN t2 USING a", []string{"LOCAL", "FULL", "JOIN"}}, + } { + from := parseOneStmt(t, tc.sql).(*SelectQuery).From.Expr + join, ok := from.(*JoinExpr) + require.True(t, ok, "%s: expected a *JoinExpr in FROM, got %T", tc.sql, from) + + right, ok := join.Right.(*JoinExpr) + require.True(t, ok, "%s: expected the join to carry a right side, got %T", tc.sql, join.Right) + require.Equal(t, tc.modifiers, right.Modifiers, tc.sql) + require.Equal(t, tc.sql, Format(parseOneStmt(t, tc.sql)), tc.sql) + } +} + +func TestGlobalAfterJoinConstraintStartsANewJoin(t *testing.T) { + // A GLOBAL that follows an ON/USING clause belongs to the next join, so it + // must not be read as the GLOBAL IN operator. + sql := "SELECT * FROM t1 GLOBAL JOIN t2 ON t1.a = t2.a GLOBAL LEFT JOIN t3 ON t1.a = t3.a" + stmt := parseOneStmt(t, sql) + join := stmt.(*SelectQuery).From.Expr.(*JoinExpr) + + first, ok := join.Right.(*JoinExpr) + require.True(t, ok) + require.Equal(t, []string{"GLOBAL", "JOIN"}, first.Modifiers) + + second, ok := first.Right.(*JoinExpr) + require.True(t, ok, "the second GLOBAL should start another join, got %T", first.Right) + require.Equal(t, []string{"GLOBAL", "LEFT", "JOIN"}, second.Modifiers) + require.Equal(t, sql, Format(stmt)) +} + +func TestGlobalInAndGlobalNotIn(t *testing.T) { + for _, tc := range []struct { + sql string + op TokenKind + }{ + {"SELECT a GLOBAL IN (SELECT 1)", "GLOBAL IN"}, + {"SELECT a GLOBAL NOT IN (SELECT 1)", "GLOBAL NOT IN"}, + } { + expr := parseSelectItemExpr(t, tc.sql) + in, ok := expr.(*BinaryOperation) + require.True(t, ok, "%s: expected BinaryOperation at the top, got %T", tc.sql, expr) + require.Equal(t, tc.op, in.Operation, tc.sql) + } +} + +func TestGlobalInGroupsLikeIn(t *testing.T) { + // GLOBAL IN binds with IN's precedence: `a = b GLOBAL IN (1)` groups as + // `a = (b GLOBAL IN (1))`. + for _, sql := range []string{"SELECT a = b GLOBAL IN (1)", "SELECT a = b GLOBAL NOT IN (1)"} { + expr := parseSelectItemExpr(t, sql) + eq, ok := expr.(*BinaryOperation) + require.True(t, ok, "%s: expected `=` at the top, got %T", sql, expr) + require.Equal(t, TokenKind("="), eq.Operation, sql) + + in, ok := eq.RightExpr.(*BinaryOperation) + require.True(t, ok, "%s: right side of `=` should be the GLOBAL IN operation, got %T", sql, eq.RightExpr) + require.Contains(t, string(in.Operation), "GLOBAL", sql) + } +} diff --git a/parser/testdata/query/format/beautify/select_with_global_join_locality.sql b/parser/testdata/query/format/beautify/select_with_global_join_locality.sql new file mode 100644 index 0000000..e84619f --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_global_join_locality.sql @@ -0,0 +1,112 @@ +-- Origin SQL: +SELECT * FROM t1 GLOBAL JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL INNER JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL LEFT OUTER JOIN t2 USING (a); + +SELECT * FROM t1 GLOBAL ANY LEFT JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL CROSS JOIN t2; + +SELECT * FROM t1 AS x LOCAL FULL JOIN t2 ON x.a = t2.a; + +SELECT * FROM t1 AS x LOCAL RIGHT JOIN t2 USING a; + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a GLOBAL LEFT JOIN t3 ON t1.a = t3.a; + +SELECT * FROM t WHERE a GLOBAL IN (SELECT b FROM t2); + +SELECT * FROM t WHERE a GLOBAL NOT IN (SELECT b FROM t2); + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a WHERE t1.a GLOBAL NOT IN (SELECT b FROM t3); + + +-- Beautify SQL: +SELECT + * +FROM + t1 + GLOBAL JOIN + t2 ON t1.a = t2.a; +SELECT + * +FROM + t1 + GLOBAL INNER JOIN + t2 ON t1.a = t2.a; +SELECT + * +FROM + t1 + GLOBAL LEFT JOIN + t2 ON t1.a = t2.a; +SELECT + * +FROM + t1 + GLOBAL LEFT OUTER JOIN + t2 USING a; +SELECT + * +FROM + t1 + GLOBAL ANY LEFT JOIN + t2 ON t1.a = t2.a; +SELECT + * +FROM + t1 + GLOBAL CROSS JOIN + t2; +SELECT + * +FROM + t1 AS x + LOCAL FULL JOIN + t2 ON x.a = t2.a; +SELECT + * +FROM + t1 AS x + LOCAL RIGHT JOIN + t2 USING a; +SELECT + * +FROM + t1 + GLOBAL LEFT JOIN + t2 ON t1.a = t2.a + GLOBAL LEFT JOIN + t3 ON t1.a = t3.a; +SELECT + * +FROM + t +WHERE + a GLOBAL IN (SELECT + b + FROM + t2); +SELECT + * +FROM + t +WHERE + a GLOBAL NOT IN (SELECT + b + FROM + t2); +SELECT + * +FROM + t1 + GLOBAL LEFT JOIN + t2 ON t1.a = t2.a +WHERE + t1.a GLOBAL NOT IN (SELECT + b + FROM + t3); diff --git a/parser/testdata/query/format/select_with_global_join_locality.sql b/parser/testdata/query/format/select_with_global_join_locality.sql new file mode 100644 index 0000000..7a24d00 --- /dev/null +++ b/parser/testdata/query/format/select_with_global_join_locality.sql @@ -0,0 +1,39 @@ +-- Origin SQL: +SELECT * FROM t1 GLOBAL JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL INNER JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL LEFT OUTER JOIN t2 USING (a); + +SELECT * FROM t1 GLOBAL ANY LEFT JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL CROSS JOIN t2; + +SELECT * FROM t1 AS x LOCAL FULL JOIN t2 ON x.a = t2.a; + +SELECT * FROM t1 AS x LOCAL RIGHT JOIN t2 USING a; + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a GLOBAL LEFT JOIN t3 ON t1.a = t3.a; + +SELECT * FROM t WHERE a GLOBAL IN (SELECT b FROM t2); + +SELECT * FROM t WHERE a GLOBAL NOT IN (SELECT b FROM t2); + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a WHERE t1.a GLOBAL NOT IN (SELECT b FROM t3); + + +-- Format SQL: +SELECT * FROM t1 GLOBAL JOIN t2 ON t1.a = t2.a; +SELECT * FROM t1 GLOBAL INNER JOIN t2 ON t1.a = t2.a; +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a; +SELECT * FROM t1 GLOBAL LEFT OUTER JOIN t2 USING a; +SELECT * FROM t1 GLOBAL ANY LEFT JOIN t2 ON t1.a = t2.a; +SELECT * FROM t1 GLOBAL CROSS JOIN t2; +SELECT * FROM t1 AS x LOCAL FULL JOIN t2 ON x.a = t2.a; +SELECT * FROM t1 AS x LOCAL RIGHT JOIN t2 USING a; +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a GLOBAL LEFT JOIN t3 ON t1.a = t3.a; +SELECT * FROM t WHERE a GLOBAL IN (SELECT b FROM t2); +SELECT * FROM t WHERE a GLOBAL NOT IN (SELECT b FROM t2); +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a WHERE t1.a GLOBAL NOT IN (SELECT b FROM t3); diff --git a/parser/testdata/query/output/select_with_global_join_locality.sql.golden.json b/parser/testdata/query/output/select_with_global_join_locality.sql.golden.json new file mode 100644 index 0000000..bb038b1 --- /dev/null +++ b/parser/testdata/query/output/select_with_global_join_locality.sql.golden.json @@ -0,0 +1,1756 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 46, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 7, + "NameEnd": 7 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 9, + "Expr": { + "JoinPos": 14, + "Left": { + "Table": { + "TablePos": 14, + "TableEnd": 16, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 14, + "NameEnd": 16 + } + }, + "HasFinal": false + }, + "StatementEnd": 16, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 17, + "Left": { + "Table": { + "TablePos": 29, + "TableEnd": 31, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 29, + "NameEnd": 31 + } + }, + "HasFinal": false + }, + "StatementEnd": 31, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "GLOBAL", + "JOIN" + ], + "Constraints": { + "OnPos": 32, + "On": { + "ListPos": 35, + "ListEnd": 46, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "t1", + "QuoteType": 1, + "NamePos": 35, + "NameEnd": 37 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 38, + "NameEnd": 39 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "t2", + "QuoteType": 1, + "NamePos": 42, + "NameEnd": 44 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 45, + "NameEnd": 46 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": 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": 49, + "StatementEnd": 101, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 56, + "NameEnd": 56 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 58, + "Expr": { + "JoinPos": 63, + "Left": { + "Table": { + "TablePos": 63, + "TableEnd": 65, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 63, + "NameEnd": 65 + } + }, + "HasFinal": false + }, + "StatementEnd": 65, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 66, + "Left": { + "Table": { + "TablePos": 84, + "TableEnd": 86, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 84, + "NameEnd": 86 + } + }, + "HasFinal": false + }, + "StatementEnd": 86, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "GLOBAL", + "INNER", + "JOIN" + ], + "Constraints": { + "OnPos": 87, + "On": { + "ListPos": 90, + "ListEnd": 101, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "t1", + "QuoteType": 1, + "NamePos": 90, + "NameEnd": 92 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 93, + "NameEnd": 94 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "t2", + "QuoteType": 1, + "NamePos": 97, + "NameEnd": 99 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 100, + "NameEnd": 101 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": 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": 104, + "StatementEnd": 155, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 111, + "NameEnd": 111 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 113, + "Expr": { + "JoinPos": 118, + "Left": { + "Table": { + "TablePos": 118, + "TableEnd": 120, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 118, + "NameEnd": 120 + } + }, + "HasFinal": false + }, + "StatementEnd": 120, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 121, + "Left": { + "Table": { + "TablePos": 138, + "TableEnd": 140, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 138, + "NameEnd": 140 + } + }, + "HasFinal": false + }, + "StatementEnd": 140, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "GLOBAL", + "LEFT", + "JOIN" + ], + "Constraints": { + "OnPos": 141, + "On": { + "ListPos": 144, + "ListEnd": 155, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "t1", + "QuoteType": 1, + "NamePos": 144, + "NameEnd": 146 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 147, + "NameEnd": 148 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "t2", + "QuoteType": 1, + "NamePos": 151, + "NameEnd": 153 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 154, + "NameEnd": 155 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": 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": 158, + "StatementEnd": 209, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 165, + "NameEnd": 165 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 167, + "Expr": { + "JoinPos": 172, + "Left": { + "Table": { + "TablePos": 172, + "TableEnd": 174, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 172, + "NameEnd": 174 + } + }, + "HasFinal": false + }, + "StatementEnd": 174, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 175, + "Left": { + "Table": { + "TablePos": 198, + "TableEnd": 200, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 198, + "NameEnd": 200 + } + }, + "HasFinal": false + }, + "StatementEnd": 200, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "GLOBAL", + "LEFT", + "OUTER", + "JOIN" + ], + "Constraints": { + "UsingPos": 201, + "Using": { + "ListPos": 208, + "ListEnd": 209, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 208, + "NameEnd": 209 + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": 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": 213, + "StatementEnd": 268, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 220, + "NameEnd": 220 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 222, + "Expr": { + "JoinPos": 227, + "Left": { + "Table": { + "TablePos": 227, + "TableEnd": 229, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 227, + "NameEnd": 229 + } + }, + "HasFinal": false + }, + "StatementEnd": 229, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 230, + "Left": { + "Table": { + "TablePos": 251, + "TableEnd": 253, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 251, + "NameEnd": 253 + } + }, + "HasFinal": false + }, + "StatementEnd": 253, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "GLOBAL", + "ANY", + "LEFT", + "JOIN" + ], + "Constraints": { + "OnPos": 254, + "On": { + "ListPos": 257, + "ListEnd": 268, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "t1", + "QuoteType": 1, + "NamePos": 257, + "NameEnd": 259 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 260, + "NameEnd": 261 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "t2", + "QuoteType": 1, + "NamePos": 264, + "NameEnd": 266 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 267, + "NameEnd": 268 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": 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": 271, + "StatementEnd": 308, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 278, + "NameEnd": 278 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 280, + "Expr": { + "JoinPos": 285, + "Left": { + "Table": { + "TablePos": 285, + "TableEnd": 287, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 285, + "NameEnd": 287 + } + }, + "HasFinal": false + }, + "StatementEnd": 287, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 288, + "Left": { + "Table": { + "TablePos": 306, + "TableEnd": 308, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 306, + "NameEnd": 308 + } + }, + "HasFinal": false + }, + "StatementEnd": 308, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "GLOBAL", + "CROSS", + "JOIN" + ], + "Constraints": null + }, + "Modifiers": null, + "Constraints": 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": 311, + "StatementEnd": 365, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 318, + "NameEnd": 318 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 320, + "Expr": { + "JoinPos": 325, + "Left": { + "Table": { + "TablePos": 325, + "TableEnd": 332, + "Alias": null, + "Expr": { + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 325, + "NameEnd": 327 + } + }, + "AliasPos": 331, + "Alias": { + "Name": "x", + "QuoteType": 1, + "NamePos": 331, + "NameEnd": 332 + } + }, + "HasFinal": false + }, + "StatementEnd": 332, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 333, + "Left": { + "Table": { + "TablePos": 349, + "TableEnd": 351, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 349, + "NameEnd": 351 + } + }, + "HasFinal": false + }, + "StatementEnd": 351, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "LOCAL", + "FULL", + "JOIN" + ], + "Constraints": { + "OnPos": 352, + "On": { + "ListPos": 355, + "ListEnd": 365, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "x", + "QuoteType": 1, + "NamePos": 355, + "NameEnd": 356 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 357, + "NameEnd": 358 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "t2", + "QuoteType": 1, + "NamePos": 361, + "NameEnd": 363 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 364, + "NameEnd": 365 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": 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": 368, + "StatementEnd": 417, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 375, + "NameEnd": 375 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 377, + "Expr": { + "JoinPos": 382, + "Left": { + "Table": { + "TablePos": 382, + "TableEnd": 389, + "Alias": null, + "Expr": { + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 382, + "NameEnd": 384 + } + }, + "AliasPos": 388, + "Alias": { + "Name": "x", + "QuoteType": 1, + "NamePos": 388, + "NameEnd": 389 + } + }, + "HasFinal": false + }, + "StatementEnd": 389, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 390, + "Left": { + "Table": { + "TablePos": 407, + "TableEnd": 409, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 407, + "NameEnd": 409 + } + }, + "HasFinal": false + }, + "StatementEnd": 409, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "LOCAL", + "RIGHT", + "JOIN" + ], + "Constraints": { + "UsingPos": 410, + "Using": { + "ListPos": 416, + "ListEnd": 417, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 416, + "NameEnd": 417 + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": 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": 420, + "StatementEnd": 506, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 427, + "NameEnd": 427 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 429, + "Expr": { + "JoinPos": 434, + "Left": { + "Table": { + "TablePos": 434, + "TableEnd": 436, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 434, + "NameEnd": 436 + } + }, + "HasFinal": false + }, + "StatementEnd": 436, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 437, + "Left": { + "Table": { + "TablePos": 454, + "TableEnd": 456, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 454, + "NameEnd": 456 + } + }, + "HasFinal": false + }, + "StatementEnd": 456, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 472, + "Left": { + "Table": { + "TablePos": 489, + "TableEnd": 491, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t3", + "QuoteType": 1, + "NamePos": 489, + "NameEnd": 491 + } + }, + "HasFinal": false + }, + "StatementEnd": 491, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "GLOBAL", + "LEFT", + "JOIN" + ], + "Constraints": { + "OnPos": 492, + "On": { + "ListPos": 495, + "ListEnd": 506, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "t1", + "QuoteType": 1, + "NamePos": 495, + "NameEnd": 497 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 498, + "NameEnd": 499 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "t3", + "QuoteType": 1, + "NamePos": 502, + "NameEnd": 504 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 505, + "NameEnd": 506 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": [ + "GLOBAL", + "LEFT", + "JOIN" + ], + "Constraints": { + "OnPos": 457, + "On": { + "ListPos": 460, + "ListEnd": 471, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "t1", + "QuoteType": 1, + "NamePos": 460, + "NameEnd": 462 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 463, + "NameEnd": 464 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "t2", + "QuoteType": 1, + "NamePos": 467, + "NameEnd": 469 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 470, + "NameEnd": 471 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": 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": 509, + "StatementEnd": 560, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 516, + "NameEnd": 516 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 518, + "Expr": { + "Table": { + "TablePos": 523, + "TableEnd": 524, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t", + "QuoteType": 1, + "NamePos": 523, + "NameEnd": 524 + } + }, + "HasFinal": false + }, + "StatementEnd": 524, + "SampleRatio": null, + "HasFinal": false + } + }, + "Window": null, + "Prewhere": null, + "Where": { + "WherePos": 525, + "Expr": { + "LeftExpr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 531, + "NameEnd": 532 + }, + "Operation": "GLOBAL IN", + "RightExpr": { + "HasParen": true, + "Select": { + "SelectPos": 544, + "StatementEnd": 560, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "b", + "QuoteType": 1, + "NamePos": 551, + "NameEnd": 552 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 553, + "Expr": { + "Table": { + "TablePos": 558, + "TableEnd": 560, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 558, + "NameEnd": 560 + } + }, + "HasFinal": false + }, + "StatementEnd": 560, + "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 + } + }, + "HasGlobal": false, + "HasNot": false + } + }, + "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": 564, + "StatementEnd": 619, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 571, + "NameEnd": 571 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 573, + "Expr": { + "Table": { + "TablePos": 578, + "TableEnd": 579, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t", + "QuoteType": 1, + "NamePos": 578, + "NameEnd": 579 + } + }, + "HasFinal": false + }, + "StatementEnd": 579, + "SampleRatio": null, + "HasFinal": false + } + }, + "Window": null, + "Prewhere": null, + "Where": { + "WherePos": 580, + "Expr": { + "LeftExpr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 586, + "NameEnd": 587 + }, + "Operation": "GLOBAL NOT IN", + "RightExpr": { + "HasParen": true, + "Select": { + "SelectPos": 603, + "StatementEnd": 619, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "b", + "QuoteType": 1, + "NamePos": 610, + "NameEnd": 611 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 612, + "Expr": { + "Table": { + "TablePos": 617, + "TableEnd": 619, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 617, + "NameEnd": 619 + } + }, + "HasFinal": false + }, + "StatementEnd": 619, + "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 + } + }, + "HasGlobal": false, + "HasNot": false + } + }, + "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": 623, + "StatementEnd": 717, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "*", + "QuoteType": 0, + "NamePos": 630, + "NameEnd": 630 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 632, + "Expr": { + "JoinPos": 637, + "Left": { + "Table": { + "TablePos": 637, + "TableEnd": 639, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t1", + "QuoteType": 1, + "NamePos": 637, + "NameEnd": 639 + } + }, + "HasFinal": false + }, + "StatementEnd": 639, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 640, + "Left": { + "Table": { + "TablePos": 657, + "TableEnd": 659, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t2", + "QuoteType": 1, + "NamePos": 657, + "NameEnd": 659 + } + }, + "HasFinal": false + }, + "StatementEnd": 659, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "GLOBAL", + "LEFT", + "JOIN" + ], + "Constraints": { + "OnPos": 660, + "On": { + "ListPos": 663, + "ListEnd": 674, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "t1", + "QuoteType": 1, + "NamePos": 663, + "NameEnd": 665 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 666, + "NameEnd": 667 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "t2", + "QuoteType": 1, + "NamePos": 670, + "NameEnd": 672 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 673, + "NameEnd": 674 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": null + } + }, + "Window": null, + "Prewhere": null, + "Where": { + "WherePos": 675, + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "t1", + "QuoteType": 1, + "NamePos": 681, + "NameEnd": 683 + }, + { + "Name": "a", + "QuoteType": 1, + "NamePos": 684, + "NameEnd": 685 + } + ] + }, + "Operation": "GLOBAL NOT IN", + "RightExpr": { + "HasParen": true, + "Select": { + "SelectPos": 701, + "StatementEnd": 717, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "b", + "QuoteType": 1, + "NamePos": 708, + "NameEnd": 709 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 710, + "Expr": { + "Table": { + "TablePos": 715, + "TableEnd": 717, + "Alias": null, + "Expr": { + "Database": null, + "Table": { + "Name": "t3", + "QuoteType": 1, + "NamePos": 715, + "NameEnd": 717 + } + }, + "HasFinal": false + }, + "StatementEnd": 717, + "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 + } + }, + "HasGlobal": false, + "HasNot": false + } + }, + "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_global_join_locality.sql b/parser/testdata/query/select_with_global_join_locality.sql new file mode 100644 index 0000000..d99bd6a --- /dev/null +++ b/parser/testdata/query/select_with_global_join_locality.sql @@ -0,0 +1,23 @@ +SELECT * FROM t1 GLOBAL JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL INNER JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL LEFT OUTER JOIN t2 USING (a); + +SELECT * FROM t1 GLOBAL ANY LEFT JOIN t2 ON t1.a = t2.a; + +SELECT * FROM t1 GLOBAL CROSS JOIN t2; + +SELECT * FROM t1 AS x LOCAL FULL JOIN t2 ON x.a = t2.a; + +SELECT * FROM t1 AS x LOCAL RIGHT JOIN t2 USING a; + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a GLOBAL LEFT JOIN t3 ON t1.a = t3.a; + +SELECT * FROM t WHERE a GLOBAL IN (SELECT b FROM t2); + +SELECT * FROM t WHERE a GLOBAL NOT IN (SELECT b FROM t2); + +SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a WHERE t1.a GLOBAL NOT IN (SELECT b FROM t3); From 1c4a301187d60eeb40dfeabdd818fa8e89567010 Mon Sep 17 00:00:00 2001 From: grandwizard28 Date: Thu, 30 Jul 2026 00:07:10 +0530 Subject: [PATCH 2/2] Keep GLOBAL an operator unless a join actually follows it Ending the expression whenever GLOBAL was not followed by IN/NOT left the keyword for the enclosing clause to pick up, and the select item parser takes any non-reserved keyword as an implicit alias. So `SELECT a GLOBAL FROM t` began parsing as `SELECT a AS GLOBAL FROM t`, which ClickHouse rejects as a syntax error. Looking ahead for the join operator instead narrows the back-off to the locality it was meant for and leaves GLOBAL binding like IN everywhere else, as it did before. The ARRAY JOIN guard compared against the uppercase keyword, but parseJoinOp records each modifier's source spelling, so `GLOBAL array JOIN arr` slipped past it and read the column list as a table expression. --- parser/parser_column.go | 14 ++++++++------ parser/parser_query.go | 22 ++++++++++++++++++++-- parser/parser_test.go | 9 ++++++++- parser/position_test.go | 11 +++++++++++ 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/parser/parser_column.go b/parser/parser_column.go index 99d9991..ab04870 100644 --- a/parser/parser_column.go +++ b/parser/parser_column.go @@ -79,14 +79,16 @@ func (p *Parser) getNextPrecedence() int { case p.matchKeyword(KeywordIn): return precedenceIn case p.matchKeyword(KeywordGlobal): - // GLOBAL is an operator only before IN/NOT IN, where it binds like IN - // itself. Anywhere else it belongs to the enclosing clause — a join - // locality in `ON a = b GLOBAL LEFT JOIN c` — and must not bind here. - if p.peekKeyword(KeywordIn) || p.peekKeyword(KeywordNot) { - return precedenceIn + // GLOBAL is both the IN operator's prefix and a join locality. Before a + // join it belongs to the enclosing FROM clause — the locality in + // `ON a = b GLOBAL LEFT JOIN c` — so the expression has to end here + // rather than bind it. Everywhere else it binds like IN itself, which + // keeps `GLOBAL` from being mistaken for an alias in `SELECT a GLOBAL`. + if p.peekJoinAfterLocality() { + return PrecedenceUnknown } - return PrecedenceUnknown + return precedenceIn case p.matchTokenKind(TokenKindQuestionMark): return PrecedenceQuery default: diff --git a/parser/parser_query.go b/parser/parser_query.go index ab66ec5..188c4df 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -3,6 +3,7 @@ package parser import ( "errors" "fmt" + "strings" "slices" ) @@ -293,14 +294,31 @@ func (p *Parser) parseJoinLocality(pos Pos) ([]string, error) { } // ARRAY JOIN reads a column list rather than a distributed table, so it has - // no locality. - if slices.Contains(joinOp, KeywordArray) { + // no locality. parseJoinOp keeps each modifier's source spelling, so the + // keyword has to be matched case-insensitively. + if slices.ContainsFunc(joinOp, func(modifier string) bool { + return strings.EqualFold(modifier, KeywordArray) + }) { return nil, fmt.Errorf("%s cannot be combined with ARRAY JOIN", locality) } return append([]string{locality}, joinOp...), nil } +// peekJoinAfterLocality reports whether the current GLOBAL/LOCAL keyword is +// followed by a join operator. It leaves the lexer where it found it, so +// expression parsing can use it to tell a join locality apart from the GLOBAL IN +// operator. +func (p *Parser) peekJoinAfterLocality() bool { + savedState := p.lexer.saveState() + defer p.lexer.restoreState(savedState) + + _ = p.lexer.consumeToken() + p.parseJoinOp(p.Pos()) + + return p.matchKeyword(KeywordJoin) +} + func (p *Parser) parseJoinRightExpr(pos Pos) (expr Expr, err error) { var rightExpr Expr var modifiers []string diff --git a/parser/parser_test.go b/parser/parser_test.go index c958a63..ee9de09 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -186,12 +186,19 @@ func TestParser_InvalidSyntax(t *testing.T) { // Invalid ARRAY JOIN types (only ARRAY JOIN, LEFT ARRAY JOIN, and INNER ARRAY JOIN are valid) "SELECT * FROM t RIGHT ARRAY JOIN arr AS a", // RIGHT ARRAY JOIN not supported "SELECT * FROM t FULL ARRAY JOIN arr AS a", // FULL ARRAY JOIN not supported - // ARRAY JOIN reads a column list, so it has no GLOBAL/LOCAL locality + // ARRAY JOIN reads a column list, so it has no GLOBAL/LOCAL locality. + // Join modifiers keep their source spelling, so the lowercase spellings + // have to be rejected too. "SELECT * FROM t GLOBAL ARRAY JOIN arr AS a", "SELECT * FROM t LOCAL LEFT ARRAY JOIN arr AS a", + "SELECT * FROM t GLOBAL array JOIN arr AS a", + "SELECT * FROM t LOCAL left array JOIN arr AS a", // A join takes at most one locality, and it precedes the join type "SELECT * FROM t1 GLOBAL LOCAL JOIN t2 ON t1.a = t2.a", "SELECT * FROM t1 LEFT GLOBAL JOIN t2 ON t1.a = t2.a", + // GLOBAL is a keyword everywhere, never an implicit alias + "SELECT a GLOBAL FROM t", + "SELECT a GLOBAL, b FROM t", // GLOBAL is an operator only before IN/NOT IN, and LOCAL is never one "SELECT * FROM t WHERE a GLOBAL LIKE 'x'", "SELECT * FROM t WHERE a GLOBAL NOT LIKE 'x'", diff --git a/parser/position_test.go b/parser/position_test.go index dab4d8b..a8de849 100644 --- a/parser/position_test.go +++ b/parser/position_test.go @@ -90,3 +90,14 @@ func TestDictionaryAttributeEnd(t *testing.T) { require.Equal(t, "IS_OBJECT_ID", sql[36:48]) require.Equal(t, Pos(48), attrs[0].End()) } + +func TestJoinLocalityIsInsideTheJoinSpan(t *testing.T) { + sql := "SELECT * FROM t1 GLOBAL LEFT JOIN t2 ON t1.a = t2.a" + stmt := parseOneStmt(t, sql).(*SelectQuery) + join := stmt.From.Expr.(*JoinExpr).Right.(*JoinExpr) + // the join starts at GLOBAL rather than at the join type, so a rewriter that + // reads the node back from the source keeps the locality + require.Equal(t, "GLOBAL", sql[17:23]) + require.Equal(t, Pos(17), join.Pos()) + require.Equal(t, Pos(len(sql)), join.End()) +}