Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions src/dialect/clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ impl Dialect for ClickHouseDialect {
true
}

// See <https://clickhouse.com/docs/sql-reference/operators/in>
fn supports_in_unparenthesized_expr(&self) -> bool {
Comment thread
dinmukhamedm marked this conversation as resolved.
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
10 changes: 10 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
9 changes: 9 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {})])
}
Expand Down
39 changes: 36 additions & 3 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2375,15 +2375,45 @@ fn parse_in_unnest() {

#[test]
fn parse_in_error() {
// <expr> IN <expr> is no valid
// <expr> IN <expr> 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");
}
Comment thread
dinmukhamedm marked this conversation as resolved.
Outdated

#[test]
fn parse_string_agg() {
let sql = "SELECT a || b";
Expand Down Expand Up @@ -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()),
Comment thread
dinmukhamedm marked this conversation as resolved.
res.unwrap_err()
Expand Down