diff --git a/fuzz/fuzz_seeds/clickhouse_global_in.sql b/fuzz/fuzz_seeds/clickhouse_global_in.sql new file mode 100644 index 0000000000..ba2b6bc605 --- /dev/null +++ b/fuzz/fuzz_seeds/clickhouse_global_in.sql @@ -0,0 +1,63 @@ +SELECT * FROM t WHERE x GLOBAL IN (1, 2); +SELECT * FROM t WHERE x GLOBAL NOT IN (1, 2); +SELECT * FROM t WHERE x GLOBAL IN (SELECT y FROM t2); +SELECT * FROM t WHERE x GLOBAL NOT IN (SELECT y FROM t2); +SELECT * FROM t WHERE x GLOBAL IN t2; +SELECT * FROM t WHERE x GLOBAL IN (t2); +SELECT * FROM t WHERE x GLOBAL IN db.t2; +SELECT * FROM t WHERE x GLOBAL IN t2 AND y = 2; +SELECT * FROM t WHERE x global in (1) AND y Global Not In (2); +SELECT * FROM t WHERE x GLOBAL IN (1); +SELECT * FROM t WHERE x GLOBAL IN (NULL, 1, 'a', 1.5, -1, 1 + 2, y * 2); +SELECT * FROM t WHERE x GLOBAL IN ('', '''', 'GLOBAL IN'); +SELECT * FROM t WHERE x GLOBAL IN ((1), ((2)), (((3)))); +SELECT * FROM t WHERE x GLOBAL IN ((SELECT 1), 2); +SELECT * FROM t WHERE x GLOBAL IN ((SELECT 1)); +SELECT * FROM t WHERE x GLOBAL NOT IN (((SELECT 1)), (SELECT 2) + 1); +SELECT * FROM t WHERE x GLOBAL IN (SELECT 1 UNION ALL SELECT 2); +SELECT * FROM t WHERE x GLOBAL IN (SELECT y FROM t2 WHERE z GLOBAL IN (SELECT w FROM t3 WHERE v GLOBAL NOT IN (1))); +SELECT * FROM t WHERE (a, b) GLOBAL IN ((1, 2), (3, 4)); +SELECT * FROM t WHERE (a, b) GLOBAL NOT IN (SELECT a, b FROM t2); +SELECT * FROM t WHERE tuple(a, b) GLOBAL IN (SELECT a, b FROM t2); +SELECT * FROM t WHERE (SELECT 1) GLOBAL IN (1, 2); +SELECT * FROM t WHERE toInt32(x) GLOBAL IN (lower('a'), upper('b')); +SELECT * FROM t WHERE CAST(x AS String) GLOBAL IN ('1', '2'); +SELECT * FROM t WHERE x GLOBAL IN ([1, 2], [3]); +SELECT * FROM t WHERE arr[1] GLOBAL IN (1, 2); +SELECT * FROM t WHERE tup.1 GLOBAL IN (1, 2); +SELECT * FROM t WHERE NOT x GLOBAL IN (1, 2); +SELECT * FROM t WHERE NOT (x GLOBAL NOT IN (1, 2)); +SELECT * FROM t WHERE x GLOBAL IN (1) AND y = 2 OR z GLOBAL NOT IN (3) AND w = 4; +SELECT * FROM t WHERE a = 1 OR x GLOBAL IN (1) AND y BETWEEN 1 AND 2; +SELECT * FROM t WHERE (x GLOBAL IN (1)) IS NULL; +SELECT * FROM t WHERE (x GLOBAL IN (1)) = (y GLOBAL IN (2)); +SELECT * FROM t WHERE x GLOBAL IN (1) AND y IN (2) AND z NOT IN (3) AND w GLOBAL NOT IN (4); +SELECT * FROM t WHERE x GLOBAL IN (1) AND EXISTS (SELECT 1 FROM u WHERE u.x GLOBAL IN (1)); +SELECT x GLOBAL IN (1, 2) AS flag, x GLOBAL NOT IN (SELECT y FROM t2) FROM t; +SELECT CASE WHEN x GLOBAL IN (1, 2) THEN 'a' WHEN x GLOBAL NOT IN (SELECT y FROM t2) THEN 'b' ELSE 'c' END FROM t; +SELECT if(x GLOBAL IN (1), 1, 0), multiIf(x GLOBAL NOT IN (2), 1, 0) FROM t; +SELECT count() FROM t GROUP BY x GLOBAL IN (1, 2) HAVING count() GLOBAL IN (SELECT c FROM counts); +SELECT * FROM t ORDER BY x GLOBAL IN (1, 2) DESC, y GLOBAL NOT IN (3) ASC LIMIT 10; +SELECT * FROM t PREWHERE x GLOBAL IN (SELECT y FROM t2) WHERE z GLOBAL NOT IN (1); +SELECT * FROM t JOIN u ON t.x = u.x AND u.y GLOBAL IN (SELECT y FROM t2); +SELECT * FROM t LEFT JOIN u ON t.x GLOBAL IN (1, 2) WHERE u.y GLOBAL NOT IN (3); +SELECT * FROM t ARRAY JOIN arr WHERE arr GLOBAL IN (1, 2); +SELECT arrayFilter(v -> v GLOBAL IN (1, 2), arr) FROM t; +SELECT * FROM (SELECT * FROM t WHERE x GLOBAL IN (1)) AS sub WHERE sub.x GLOBAL NOT IN (2); +WITH c AS (SELECT 1 AS v) SELECT * FROM t WHERE x GLOBAL IN (SELECT v FROM c); +SELECT * FROM t WHERE x GLOBAL IN (1, 2) SETTINGS max_threads = 1; +SELECT * FROM t WHERE x GLOBAL IN (1) UNION ALL SELECT * FROM t WHERE x GLOBAL NOT IN (2); +SELECT * FROM t WHERE x GLOBAL IN (SELECT y FROM t2) ORDER BY x LIMIT 1 BY x; +SELECT sum(x) OVER (PARTITION BY y GLOBAL IN (1, 2)) FROM t; +INSERT INTO t2 SELECT * FROM t WHERE x GLOBAL IN (1, 2); +DELETE FROM t WHERE x GLOBAL IN (SELECT y FROM t2); +UPDATE t SET y = 1 WHERE x GLOBAL IN (SELECT y FROM t2); +CREATE VIEW v AS SELECT * FROM t WHERE x GLOBAL IN (1, 2); +SELECT /* GLOBAL IN */ * FROM t WHERE x GLOBAL -- NOT +IN (1, 2); +SELECT * FROM t WHERE x +GLOBAL +NOT +IN +(1, +2) \ No newline at end of file diff --git a/src/ast/mod.rs b/src/ast/mod.rs index de925be2b4..9d3ee77e8c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -975,7 +975,7 @@ pub enum Expr { /// `true` when `NOT` is present. negated: bool, }, - /// `[ NOT ] IN (val1, val2, ...)` + /// `[ GLOBAL ] [ NOT ] IN (val1, val2, ...)` InList { /// Left-hand expression to test for membership. expr: Box, @@ -983,8 +983,10 @@ pub enum Expr { list: Vec, /// `true` when the `NOT` modifier is present. negated: bool, + /// `true` when the ClickHouse `GLOBAL` modifier is present. + global: bool, }, - /// `[ NOT ] IN (SELECT ...)` + /// `[ GLOBAL ] [ NOT ] IN (SELECT ...)` InSubquery { /// Left-hand expression to test for membership. expr: Box, @@ -992,6 +994,8 @@ pub enum Expr { subquery: Box, /// `true` when the `NOT` modifier is present. negated: bool, + /// `true` when the ClickHouse `GLOBAL` modifier is present. + global: bool, }, /// `[ NOT ] IN UNNEST(array_expression)` InUnnest { @@ -1801,10 +1805,12 @@ impl fmt::Display for Expr { expr, list, negated, + global, } => write!( f, - "{} {}IN ({})", + "{} {}{}IN ({})", expr, + if *global { "GLOBAL " } else { "" }, if *negated { "NOT " } else { "" }, display_comma_separated(list) ), @@ -1812,10 +1818,12 @@ impl fmt::Display for Expr { expr, subquery, negated, + global, } => write!( f, - "{} {}IN ({})", + "{} {}{}IN ({})", expr, + if *global { "GLOBAL " } else { "" }, if *negated { "NOT " } else { "" }, subquery ), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 945cbb0985..cae24f959e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1513,6 +1513,7 @@ impl Spanned for Expr { expr, list, negated: _, + global: _, } => union_spans( core::iter::once(expr.span()).chain(list.iter().map(|item| item.span())), ), @@ -1520,6 +1521,7 @@ impl Spanned for Expr { expr, subquery, negated: _, + global: _, } => expr.span().union(&subquery.span()), Expr::InUnnest { expr, diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index d5c130d04e..d256da28ea 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -100,6 +100,11 @@ impl Dialect for ClickHouseDialect { true } + // See + fn supports_global_in(&self) -> bool { + true + } + /// See fn supports_lambda_functions(&self) -> bool { true diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 2beb56553e..f57907e1c1 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -461,6 +461,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `GLOBAL IN` and `GLOBAL NOT IN`. + fn supports_global_in(&self) -> bool { + false + } + /// Returns true if the dialect supports `BEGIN {DEFERRED | IMMEDIATE | EXCLUSIVE | TRY | CATCH} [TRANSACTION]` statements fn supports_start_transaction_modifier(&self) -> bool { false @@ -914,6 +919,20 @@ pub trait Dialect: Debug + Any { Ok(p!(Is)) } Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)), + Token::Word(w) if w.keyword == Keyword::GLOBAL && self.supports_global_in() => { + match ( + &parser.peek_nth_token_ref(1).token, + &parser.peek_nth_token_ref(2).token, + ) { + (Token::Word(w), _) if w.keyword == Keyword::IN => Ok(p!(Between)), + (Token::Word(w), Token::Word(w2)) + if w.keyword == Keyword::NOT && w2.keyword == Keyword::IN => + { + Ok(p!(Between)) + } + _ => Ok(self.prec_unknown()), + } + } Token::Word(w) if w.keyword == Keyword::IN => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::OVERLAPS => Ok(p!(Between)), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1a46c6e50a..7463f68ed4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4155,6 +4155,11 @@ impl<'a> Parser<'a> { time_zone: Box::new(self.parse_subexpr(precedence)?), }) } + Keyword::GLOBAL if self.dialect.supports_global_in() => { + let negated = self.parse_keyword(Keyword::NOT); + self.expect_keyword_is(Keyword::IN)?; + self.parse_in(expr, negated, true) + } Keyword::NOT | Keyword::IN | Keyword::BETWEEN @@ -4184,7 +4189,7 @@ impl<'a> Parser<'a> { } else if negated && null { Ok(Expr::IsNotNull(Box::new(expr))) } else if self.parse_keyword(Keyword::IN) { - self.parse_in(expr, negated) + self.parse_in(expr, negated, false) } else if self.parse_keyword(Keyword::BETWEEN) { self.parse_between(expr, negated) } else if self.parse_keyword(Keyword::LIKE) { @@ -4447,11 +4452,16 @@ impl<'a> Parser<'a> { Ok(JsonPath { path }) } - /// Parses the parens following the `[ NOT ] IN` operator. - pub fn parse_in(&mut self, expr: Expr, negated: bool) -> Result { + /// Parses the parens following the `[ GLOBAL ] [ NOT ] IN` operator. + pub fn parse_in( + &mut self, + expr: Expr, + negated: bool, + global: bool, + ) -> Result { // BigQuery allows `IN UNNEST(array_expression)` // https://cloud.google.com/bigquery/docs/reference/standard-sql/operators#in_operators - if self.parse_keyword(Keyword::UNNEST) { + if !global && self.parse_keyword(Keyword::UNNEST) { self.expect_token(&Token::LParen)?; let array_expr = self.parse_expr()?; self.expect_token(&Token::RParen)?; @@ -4466,8 +4476,9 @@ impl<'a> Parser<'a> { { return Ok(Expr::InList { expr: Box::new(expr), - list: vec![self.parse_expr()?], + list: vec![self.parse_subexpr(self.dialect.prec_value(Precedence::Eq))?], negated, + global, }); } self.expect_token(&Token::LParen)?; @@ -4486,6 +4497,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), subquery, negated, + global, }); } missing_rparen = Some(self.index); @@ -4506,6 +4518,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), list, negated, + global, }), Err(ParserError::RecursionLimitExceeded) => Err(ParserError::RecursionLimitExceeded), Err(e) => match missing_rparen { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 17d5585265..ff818811b7 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -2084,6 +2084,125 @@ fn parse_tuple_element_access() { } } +#[test] +fn parse_global_in() { + let select = clickhouse().verified_only_select("SELECT * FROM t WHERE x GLOBAL IN (1, 2)"); + assert_eq!( + select.selection.unwrap(), + Expr::InList { + expr: Box::new(Identifier(Ident::new("x"))), + list: vec![ + Expr::Value(number("1").with_empty_span()), + Expr::Value(number("2").with_empty_span()), + ], + negated: false, + global: true, + } + ); + + let select = clickhouse() + .verified_only_select("SELECT * FROM t WHERE x GLOBAL NOT IN (SELECT y FROM t2)"); + assert_eq!( + select.selection.unwrap(), + Expr::InSubquery { + expr: Box::new(Identifier(Ident::new("x"))), + subquery: Box::new(clickhouse().verified_query("SELECT y FROM t2")), + negated: true, + global: true, + } + ); + + clickhouse().one_statement_parses_to( + "SELECT * FROM t WHERE x GLOBAL IN t2", + "SELECT * FROM t WHERE x GLOBAL IN (t2)", + ); + + let select = + clickhouse().verified_only_select("SELECT * FROM t WHERE x GLOBAL IN (1) AND y = 2"); + assert!(matches!( + select.selection.unwrap(), + BinaryOp { + op: BinaryOperator::And, + .. + } + )); + + assert!(clickhouse() + .parse_sql_statements("SELECT * FROM t WHERE x GLOBAL y") + .is_err()); + + for dialect in all_dialects_where(|d| !d.supports_global_in()).dialects { + assert!(TestedDialects::new(vec![dialect]) + .parse_sql_statements("SELECT * FROM t WHERE x GLOBAL IN (1)") + .is_err()); + } +} + +#[test] +fn parse_in_unparenthesized_expr_precedence() { + for (sql, canonical, op) in [ + ( + "SELECT * FROM t WHERE x IN t2 AND y = 2", + "SELECT * FROM t WHERE x IN (t2) AND y = 2", + BinaryOperator::And, + ), + ( + "SELECT * FROM t WHERE x GLOBAL IN t2 AND y = 2", + "SELECT * FROM t WHERE x GLOBAL IN (t2) AND y = 2", + BinaryOperator::And, + ), + ( + "SELECT * FROM t WHERE x GLOBAL NOT IN t2 OR y = 2", + "SELECT * FROM t WHERE x GLOBAL NOT IN (t2) OR y = 2", + BinaryOperator::Or, + ), + ( + "SELECT * FROM t WHERE x IN t2 = 1", + "SELECT * FROM t WHERE x IN (t2) = 1", + BinaryOperator::Eq, + ), + ] { + let select = clickhouse().verified_only_select_with_canonical(sql, canonical); + match select.selection.unwrap() { + BinaryOp { op: got, .. } => assert_eq!(got, op, "{sql}"), + other => panic!("{sql}: expected BinaryOp, got {other:?}"), + } + } + + // Higher-precedence operators still belong to the IN operand. + clickhouse().one_statement_parses_to( + "SELECT * FROM t WHERE x IN 1 + 2", + "SELECT * FROM t WHERE x IN (1 + 2)", + ); +} + +#[test] +fn global_in_bare_table_binds_before_and() { + let expr = + clickhouse().expr_parses_to("x GLOBAL IN t2 AND y = 2", "x GLOBAL IN (t2) AND y = 2"); + assert!(matches!( + expr, + BinaryOp { + left, + op: BinaryOperator::And, + .. + } if matches!(*left, Expr::InList { global: true, .. }) + )); +} + +#[test] +fn global_in_bare_table_binds_before_equality() { + let expr = clickhouse().expr_parses_to("x GLOBAL IN t2 = 1", "x GLOBAL IN (t2) = 1"); + assert!(matches!( + expr, + BinaryOp { + left, + op: BinaryOperator::Eq, + .. + } if matches!(*left, Expr::InList { global: true, .. }) + )); +} + fn clickhouse() -> TestedDialects { TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8e08c657f4..e7293266d8 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2259,6 +2259,7 @@ fn parse_not_precedence() { (Value::SingleQuotedString("a".into())).with_empty_span() )], negated: true, + global: false, }), }, ); @@ -2541,6 +2542,7 @@ fn parse_in_list() { Expr::Value((Value::SingleQuotedString("MED".to_string())).with_empty_span()), ], negated, + global: false, }, select.selection.unwrap() ); @@ -2558,6 +2560,7 @@ fn parse_in_subquery() { expr: Box::new(Expr::Identifier(Ident::new("segment"))), subquery: Box::new(verified_query("SELECT segm FROM bar")), negated: false, + global: false, }, select.selection.unwrap() ); @@ -2574,6 +2577,7 @@ fn parse_in_union() { "(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)" )), negated: false, + global: false, }, select.selection.unwrap() ); @@ -20312,6 +20316,7 @@ fn parse_in_list_leading_subquery() { expr: x(), list: vec![subquery(), Expr::value(number("1"))], negated: false, + global: false, }, select.selection.unwrap() ); @@ -20322,6 +20327,7 @@ fn parse_in_list_leading_subquery() { expr: x(), list: vec![Expr::Nested(Box::new(subquery())), Expr::value(number("1"))], negated: true, + global: false, }, select.selection.unwrap() ); @@ -20339,6 +20345,7 @@ fn parse_in_list_leading_subquery() { Expr::value(number("2")) ], negated: false, + global: false, }, select.selection.unwrap() ); @@ -20357,6 +20364,7 @@ fn parse_in_list_leading_subquery() { expr: x(), subquery: Box::new(verified_query("(SELECT a FROM u)")), negated: false, + global: false, }, select.selection.unwrap() );