Skip to content

Commit b478611

Browse files
authored
ClickHouse: Support unparenthesized IN right-hand side (#2385)
1 parent a023b78 commit b478611

5 files changed

Lines changed: 75 additions & 3 deletions

File tree

src/dialect/clickhouse.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ impl Dialect for ClickHouseDialect {
8080
true
8181
}
8282

83+
// See <https://clickhouse.com/docs/sql-reference/operators/in>
84+
fn supports_in_unparenthesized_expr(&self) -> bool {
85+
true
86+
}
87+
8388
/// See <https://clickhouse.com/docs/en/sql-reference/functions#higher-order-functions---operator-and-lambdaparams-expr-function>
8489
fn supports_lambda_functions(&self) -> bool {
8590
true

src/dialect/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,12 @@ pub trait Dialect: Debug + Any {
447447
false
448448
}
449449

450+
/// Returns true if the dialect supports a bare expression as the right-hand
451+
/// side of `IN`, without a parenthesized list — as in `x IN 'a'`.
452+
fn supports_in_unparenthesized_expr(&self) -> bool {
453+
false
454+
}
455+
450456
/// Returns true if the dialect supports `BEGIN {DEFERRED | IMMEDIATE | EXCLUSIVE | TRY | CATCH} [TRANSACTION]` statements
451457
fn supports_start_transaction_modifier(&self) -> bool {
452458
false
@@ -2063,6 +2069,10 @@ mod tests {
20632069
self.0.supports_in_empty_list()
20642070
}
20652071

2072+
fn supports_in_unparenthesized_expr(&self) -> bool {
2073+
self.0.supports_in_unparenthesized_expr()
2074+
}
2075+
20662076
fn convert_type_before_value(&self) -> bool {
20672077
self.0.convert_type_before_value()
20682078
}

src/parser/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4392,6 +4392,15 @@ impl<'a> Parser<'a> {
43924392
negated,
43934393
});
43944394
}
4395+
if self.dialect.supports_in_unparenthesized_expr()
4396+
&& self.peek_token_ref().token != Token::LParen
4397+
{
4398+
return Ok(Expr::InList {
4399+
expr: Box::new(expr),
4400+
list: vec![self.parse_expr()?],
4401+
negated,
4402+
});
4403+
}
43954404
self.expect_token(&Token::LParen)?;
43964405
let in_op = match self.maybe_parse(|p| p.parse_query())? {
43974406
Some(subquery) => Expr::InSubquery {

tests/sqlparser_clickhouse.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,29 @@ fn parse_inner_array_join() {
18461846
}
18471847
}
18481848

1849+
#[test]
1850+
fn parse_in_unparenthesized_expr() {
1851+
// IN [expr] parses to IN ([expr]) and does not cause regressions
1852+
clickhouse().expr_parses_to("x IN 'a'", "x IN ('a')");
1853+
1854+
// The branch must not fire when the next token is `(` (regressions).
1855+
clickhouse().verified_expr("x IN (1, 2, 3)");
1856+
clickhouse().verified_stmt("SELECT * FROM t WHERE x IN (SELECT y FROM u)");
1857+
}
1858+
1859+
#[test]
1860+
fn parse_in_unparenthesized_dictionary_placeholder() {
1861+
// IN [{placeholder:Type}] parses to IN ({placholder:Type})
1862+
clickhouse().expr_parses_to("x IN {ids:Array(UInt64)}", "x IN ({ids: Array(UInt64)})");
1863+
clickhouse().expr_parses_to(
1864+
"x NOT IN {ids:Array(UInt64)}",
1865+
"x NOT IN ({ids: Array(UInt64)})",
1866+
);
1867+
clickhouse().verified_expr("x IN ({ids: Array(UInt64)})");
1868+
// Precedence: the trailing `AND` is not swallowed.
1869+
clickhouse().verified_expr("x IN ({p: Array(UInt64)}) AND y = 1");
1870+
}
1871+
18491872
fn clickhouse() -> TestedDialects {
18501873
TestedDialects::new(vec![Box::new(ClickHouseDialect {})])
18511874
}

tests/sqlparser_common.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2383,15 +2383,29 @@ fn parse_in_unnest() {
23832383

23842384
#[test]
23852385
fn parse_in_error() {
2386-
// <expr> IN <expr> is no valid
2386+
// <expr> IN <expr> is no valid, except in dialects that accept an
2387+
// unparenthesized expression as the IN right-hand side (e.g. ClickHouse).
23872388
let sql = "SELECT * FROM customers WHERE segment in segment";
2388-
let res = parse_sql_statements(sql);
2389+
let res =
2390+
all_dialects_except(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql);
23892391
assert_eq!(
23902392
ParserError::ParserError("Expected: (, found: segment".to_string()),
23912393
res.unwrap_err()
23922394
);
23932395
}
23942396

2397+
#[test]
2398+
fn parse_in_unparenthesized_expr() {
2399+
// Dialects supporting an unparenthesized IN right-hand side wrap a bare expression
2400+
// into a single-element list (e.g. `x IN 'a'` -> `x IN ('a')`).
2401+
let dialects = all_dialects_where(|d| d.supports_in_unparenthesized_expr());
2402+
dialects.expr_parses_to("x IN 'a'", "x IN ('a')");
2403+
2404+
// The branch must not fire when the next token is `(` (regressions).
2405+
dialects.verified_expr("x IN (1, 2, 3)");
2406+
dialects.verified_stmt("SELECT * FROM t WHERE x IN (SELECT y FROM u)");
2407+
}
2408+
23952409
#[test]
23962410
fn parse_string_agg() {
23972411
let sql = "SELECT a || b";
@@ -10842,12 +10856,23 @@ fn parse_position() {
1084210856

1084310857
#[test]
1084410858
fn parse_position_negative() {
10859+
// Dialects that accept an unparenthesized IN right-hand side (e.g. ClickHouse)
10860+
// report a different error here, so exclude them.
1084510861
let sql = "SELECT POSITION(foo IN) from bar";
10846-
let res = parse_sql_statements(sql);
10862+
let res =
10863+
all_dialects_except(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql);
1084710864
assert_eq!(
1084810865
ParserError::ParserError("Expected: (, found: )".to_string()),
1084910866
res.unwrap_err()
1085010867
);
10868+
10869+
let result_unparenthesized =
10870+
all_dialects_where(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql);
10871+
10872+
assert_eq!(
10873+
ParserError::ParserError("Expected: an expression, found: )".to_string()),
10874+
result_unparenthesized.unwrap_err()
10875+
);
1085110876
}
1085210877

1085310878
#[test]

0 commit comments

Comments
 (0)