Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions fuzz/fuzz_seeds/clickhouse_global_in.sql
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 12 additions & 4 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -975,23 +975,27 @@ 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<Expr>,
/// Literal list of expressions to check against.
list: Vec<Expr>,
/// `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<Expr>,
/// The subquery providing the candidate values.
subquery: Box<Query>,
/// `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 {
Expand Down Expand Up @@ -1801,21 +1805,25 @@ 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)
),
Expr::InSubquery {
expr,
subquery,
negated,
global,
} => write!(
f,
"{} {}IN ({})",
"{} {}{}IN ({})",
expr,
if *global { "GLOBAL " } else { "" },
if *negated { "NOT " } else { "" },
subquery
),
Expand Down
2 changes: 2 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1513,13 +1513,15 @@ impl Spanned for Expr {
expr,
list,
negated: _,
global: _,
} => union_spans(
core::iter::once(expr.span()).chain(list.iter().map(|item| item.span())),
),
Expr::InSubquery {
expr,
subquery,
negated: _,
global: _,
} => expr.span().union(&subquery.span()),
Expr::InUnnest {
expr,
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ impl Dialect for ClickHouseDialect {
true
}

// See <https://clickhouse.com/docs/sql-reference/operators/in#distributed-subqueries>
fn supports_global_in(&self) -> bool {
true
}

/// See <https://clickhouse.com/docs/en/sql-reference/functions#higher-order-functions---operator-and-lambdaparams-expr-function>
fn supports_lambda_functions(&self) -> bool {
true
Expand Down
19 changes: 19 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)),
Expand Down
23 changes: 18 additions & 5 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Expr, ParserError> {
/// Parses the parens following the `[ GLOBAL ] [ NOT ] IN` operator.
pub fn parse_in(
&mut self,
expr: Expr,
negated: bool,
global: bool,
) -> Result<Expr, ParserError> {
// 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)?;
Expand All @@ -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)?;
Expand All @@ -4486,6 +4497,7 @@ impl<'a> Parser<'a> {
expr: Box::new(expr),
subquery,
negated,
global,
});
}
missing_rparen = Some(self.index);
Expand All @@ -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 {
Expand Down
119 changes: 119 additions & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some red tests

Suggested change
}
}
#[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, .. })
));
}


#[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 {})])
}
Expand Down
Loading
Loading