diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 6ee60cc99..9061036f7 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -80,6 +80,11 @@ impl Dialect for ClickHouseDialect { true } + // See + fn supports_in_unparenthesized_expr(&self) -> bool { + true + } + /// See fn supports_lambda_functions(&self) -> bool { true diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 8a963cd42..432b99e68 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -435,6 +435,12 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports a bare expression as the right-hand + /// side of `IN`, without a parenthesized list — as in `x IN 'a'`. + fn supports_in_unparenthesized_expr(&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 @@ -2051,6 +2057,10 @@ mod tests { self.0.supports_in_empty_list() } + fn supports_in_unparenthesized_expr(&self) -> bool { + self.0.supports_in_unparenthesized_expr() + } + fn convert_type_before_value(&self) -> bool { self.0.convert_type_before_value() } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6540cdc0d..fe205c0fc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4392,6 +4392,15 @@ impl<'a> Parser<'a> { negated, }); } + if self.dialect.supports_in_unparenthesized_expr() + && self.peek_token_ref().token != Token::LParen + { + return Ok(Expr::InList { + expr: Box::new(expr), + list: vec![self.parse_expr()?], + negated, + }); + } self.expect_token(&Token::LParen)?; let in_op = match self.maybe_parse(|p| p.parse_query())? { Some(subquery) => Expr::InSubquery { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index cb2df1ff6..258f44367 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -1846,6 +1846,29 @@ fn parse_inner_array_join() { } } +#[test] +fn parse_in_unparenthesized_expr() { + // IN [expr] parses to IN ([expr]) and does not cause regressions + clickhouse().expr_parses_to("x IN 'a'", "x IN ('a')"); + + // The branch must not fire when the next token is `(` (regressions). + clickhouse().verified_expr("x IN (1, 2, 3)"); + clickhouse().verified_stmt("SELECT * FROM t WHERE x IN (SELECT y FROM u)"); +} + +#[test] +fn parse_in_unparenthesized_dictionary_placeholder() { + // IN [{placeholder:Type}] parses to IN ({placholder:Type}) + clickhouse().expr_parses_to("x IN {ids:Array(UInt64)}", "x IN ({ids: Array(UInt64)})"); + clickhouse().expr_parses_to( + "x NOT IN {ids:Array(UInt64)}", + "x NOT IN ({ids: Array(UInt64)})", + ); + clickhouse().verified_expr("x IN ({ids: Array(UInt64)})"); + // Precedence: the trailing `AND` is not swallowed. + clickhouse().verified_expr("x IN ({p: Array(UInt64)}) AND y = 1"); +} + fn clickhouse() -> TestedDialects { TestedDialects::new(vec![Box::new(ClickHouseDialect {})]) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index be3026f63..85c81bc91 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2375,15 +2375,45 @@ fn parse_in_unnest() { #[test] fn parse_in_error() { - // IN is no valid + // IN is no valid, except in dialects that accept an + // unparenthesized expression as the IN right-hand side (e.g. ClickHouse). let sql = "SELECT * FROM customers WHERE segment in segment"; - let res = parse_sql_statements(sql); + let res = + all_dialects_except(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql); assert_eq!( ParserError::ParserError("Expected: (, found: segment".to_string()), res.unwrap_err() ); } +#[test] +fn parse_in_unparenthesized_expr() { + // Dialects supporting an unparenthesized IN right-hand side wrap a bare expression + // into a single-element list (e.g. `x IN 'a'` -> `x IN ('a')`). + let dialects = all_dialects_where(|d| d.supports_in_unparenthesized_expr()); + dialects.expr_parses_to("x IN 'a'", "x IN ('a')"); + + // The branch must not fire when the next token is `(` (regressions). + dialects.verified_expr("x IN (1, 2, 3)"); + dialects.verified_stmt("SELECT * FROM t WHERE x IN (SELECT y FROM u)"); +} + +#[test] +fn parse_in_unparenthesized_dictionary_placeholder() { + // The `{name:Type}` placeholder form additionally requires dictionary syntax. + let dialects = all_dialects_where(|d| { + d.supports_in_unparenthesized_expr() && d.supports_dictionary_syntax() + }); + dialects.expr_parses_to("x IN {ids:Array(UInt64)}", "x IN ({ids: Array(UInt64)})"); + dialects.expr_parses_to( + "x NOT IN {ids:Array(UInt64)}", + "x NOT IN ({ids: Array(UInt64)})", + ); + dialects.verified_expr("x IN ({ids: Array(UInt64)})"); + // Precedence: the trailing `AND` is not swallowed. + dialects.verified_expr("x IN ({p: Array(UInt64)}) AND y = 1"); +} + #[test] fn parse_string_agg() { let sql = "SELECT a || b"; @@ -10834,8 +10864,11 @@ fn parse_position() { #[test] fn parse_position_negative() { + // Dialects that accept an unparenthesized IN right-hand side (e.g. ClickHouse) + // report a different error here, so exclude them. let sql = "SELECT POSITION(foo IN) from bar"; - let res = parse_sql_statements(sql); + let res = + all_dialects_except(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql); assert_eq!( ParserError::ParserError("Expected: (, found: )".to_string()), res.unwrap_err()