From 59b689da9a9d00ab0c55df2e78159edecd29aa77 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sun, 18 Oct 2026 12:17:23 +0200 Subject: [PATCH 1/2] Merge the `IN` expression variants into `Expr::In` --- src/ast/mod.rs | 84 +++++++++++++---------------------- src/ast/spans.rs | 31 +++++-------- src/parser/mod.rs | 48 +++++++++----------- tests/sqlparser_clickhouse.rs | 14 +++--- tests/sqlparser_common.rs | 52 ++++++++++++---------- tests/sqlparser_sqlite.rs | 6 ++- 6 files changed, 105 insertions(+), 130 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f38ff0f17a..e9b7cdbd14 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -871,6 +871,30 @@ impl fmt::Display for CaseWhen { } } +/// The right-hand side of an `IN` predicate, see [`Expr::In`]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum InTarget { + /// `(val1, val2, ...)` + List(Vec), + /// `(SELECT ...)` + Subquery(Box), + /// `UNNEST(array_expression)`, see + /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/operators#in_operators) + Unnest(Box), +} + +impl fmt::Display for InTarget { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + InTarget::List(list) => write!(f, "({})", display_comma_separated(list)), + InTarget::Subquery(subquery) => write!(f, "({subquery})"), + InTarget::Unnest(array_expr) => write!(f, "UNNEST({array_expr})"), + } + } +} + /// An SQL expression of any type. /// /// # Semantics / Type Checking @@ -975,37 +999,17 @@ pub enum Expr { /// `true` when `NOT` is present. negated: bool, }, - /// `[ GLOBAL ] [ NOT ] IN (val1, val2, ...)` - InList { + /// ` [ GLOBAL ] [ NOT ] IN ` + In { /// Left-hand expression to test for membership. expr: Box, - /// Literal list of expressions to check against. - list: Vec, + /// What `expr` is tested against. + target: InTarget, /// `true` when the `NOT` modifier is present. negated: bool, /// `true` when the ClickHouse `GLOBAL` modifier is present. global: bool, }, - /// `[ GLOBAL ] [ NOT ] IN (SELECT ...)` - InSubquery { - /// Left-hand expression to test for membership. - expr: Box, - /// The subquery providing the candidate values. - 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 { - /// Left-hand expression to test for membership. - expr: Box, - /// Array expression being unnested. - array_expr: Box, - /// `true` when the `NOT` modifier is present. - negated: bool, - }, /// ` [ NOT ] BETWEEN AND ` Between { /// Expression being compared. @@ -1801,42 +1805,18 @@ impl fmt::Display for Expr { } Ok(()) } - Expr::InList { + Expr::In { expr, - list, - negated, - global, - } => write!( - f, - "{} {}{}IN ({})", - expr, - if *global { "GLOBAL " } else { "" }, - if *negated { "NOT " } else { "" }, - display_comma_separated(list) - ), - Expr::InSubquery { - expr, - subquery, + target, negated, global, } => write!( f, - "{} {}{}IN ({})", + "{} {}{}IN {}", expr, if *global { "GLOBAL " } else { "" }, if *negated { "NOT " } else { "" }, - subquery - ), - Expr::InUnnest { - expr, - array_expr, - negated, - } => write!( - f, - "{} {}IN UNNEST({})", - expr, - if *negated { "NOT " } else { "" }, - array_expr + target ), Expr::Between { expr, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index eab9f71a37..7b92b52bb7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -36,9 +36,9 @@ use super::{ CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, - HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert, Interpolate, InterpolateExpr, - Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, - MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, + HavingBound, IfStatement, IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, + InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, + LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, @@ -1525,25 +1525,18 @@ impl Spanned for Expr { } => expr.span(), Expr::IsDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()), Expr::IsNotDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()), - Expr::InList { + Expr::In { expr, - list, + target, 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, - array_expr, - negated: _, - } => expr.span().union(&array_expr.span()), + } => match target { + InTarget::List(list) => union_spans( + core::iter::once(expr.span()).chain(list.iter().map(|item| item.span())), + ), + InTarget::Subquery(subquery) => expr.span().union(&subquery.span()), + InTarget::Unnest(array_expr) => expr.span().union(&array_expr.span()), + }, Expr::Between { expr, negated: _, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index db54296fce..ac7d23a958 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4476,26 +4476,30 @@ impl<'a> Parser<'a> { ) -> Result { // BigQuery allows `IN UNNEST(array_expression)` // https://cloud.google.com/bigquery/docs/reference/standard-sql/operators#in_operators - if !global && self.parse_keyword(Keyword::UNNEST) { + let target = if !global && self.parse_keyword(Keyword::UNNEST) { self.expect_token(&Token::LParen)?; let array_expr = self.parse_expr()?; self.expect_token(&Token::RParen)?; - return Ok(Expr::InUnnest { - expr: Box::new(expr), - array_expr: Box::new(array_expr), - negated, - }); - } - if self.dialect.supports_in_unparenthesized_expr() + InTarget::Unnest(Box::new(array_expr)) + } else 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_subexpr(self.dialect.prec_value(Precedence::Eq))?], - negated, - global, - }); - } + InTarget::List(vec![ + self.parse_subexpr(self.dialect.prec_value(Precedence::Eq))? + ]) + } else { + self.parse_in_parenthesized_target()? + }; + Ok(Expr::In { + expr: Box::new(expr), + target, + negated, + global, + }) + } + + /// Parses the parenthesized subquery or value list of an `IN` predicate. + fn parse_in_parenthesized_target(&mut self) -> Result { self.expect_token(&Token::LParen)?; // A leading query is the whole subquery only when `)` follows it, // otherwise it is the first item of a list, as in `IN ((SELECT 1), 2)`. @@ -4508,12 +4512,7 @@ impl<'a> Parser<'a> { let mut missing_rparen = None; if let Some(subquery) = self.maybe_parse(|p| p.parse_query())? { if self.consume_token(&Token::RParen) { - return Ok(Expr::InSubquery { - expr: Box::new(expr), - subquery, - negated, - global, - }); + return Ok(InTarget::Subquery(subquery)); } missing_rparen = Some(self.index); self.index = start; @@ -4529,12 +4528,7 @@ impl<'a> Parser<'a> { self.parse_comma_separated(Parser::parse_expr) }; match list.and_then(|list| self.expect_token(&Token::RParen).map(|_| list)) { - Ok(list) => Ok(Expr::InList { - expr: Box::new(expr), - list, - negated, - global, - }), + Ok(list) => Ok(InTarget::List(list)), Err(ParserError::RecursionLimitExceeded) => Err(ParserError::RecursionLimitExceeded), Err(e) => match missing_rparen { Some(index) if self.index <= index => { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index bd216f940e..45b7649d1a 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -2120,12 +2120,12 @@ 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::In { expr: Box::new(Identifier(Ident::new("x"))), - list: vec![ + target: InTarget::List(vec![ Expr::Value(number("1").with_empty_span()), Expr::Value(number("2").with_empty_span()), - ], + ]), negated: false, global: true, } @@ -2135,9 +2135,9 @@ fn parse_global_in() { .verified_only_select("SELECT * FROM t WHERE x GLOBAL NOT IN (SELECT y FROM t2)"); assert_eq!( select.selection.unwrap(), - Expr::InSubquery { + Expr::In { expr: Box::new(Identifier(Ident::new("x"))), - subquery: Box::new(clickhouse().verified_query("SELECT y FROM t2")), + target: InTarget::Subquery(Box::new(clickhouse().verified_query("SELECT y FROM t2"))), negated: true, global: true, } @@ -2222,7 +2222,7 @@ fn global_in_bare_table_binds_before_and() { left, op: BinaryOperator::And, .. - } if matches!(*left, Expr::InList { global: true, .. }) + } if matches!(*left, Expr::In { target: InTarget::List(_), global: true, .. }) )); } @@ -2235,7 +2235,7 @@ fn global_in_bare_table_binds_before_equality() { left, op: BinaryOperator::Eq, .. - } if matches!(*left, Expr::InList { global: true, .. }) + } if matches!(*left, Expr::In { target: InTarget::List(_), global: true, .. }) )); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 025c2cf207..773ec9d569 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2264,11 +2264,11 @@ fn parse_not_precedence() { verified_expr(sql), Expr::UnaryOp { op: UnaryOperator::Not, - expr: Box::new(Expr::InList { + expr: Box::new(Expr::In { expr: Box::new(Expr::Identifier("a".into())), - list: vec![Expr::Value( + target: InTarget::List(vec![Expr::Value( (Value::SingleQuotedString("a".into())).with_empty_span() - )], + )]), negated: true, global: false, }), @@ -2546,12 +2546,12 @@ fn parse_in_list() { ); let select = verified_only_select(sql); assert_eq!( - Expr::InList { + Expr::In { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - list: vec![ + target: InTarget::List(vec![ Expr::Value((Value::SingleQuotedString("HIGH".to_string())).with_empty_span()), Expr::Value((Value::SingleQuotedString("MED".to_string())).with_empty_span()), - ], + ]), negated, global: false, }, @@ -2567,9 +2567,9 @@ fn parse_in_subquery() { let sql = "SELECT * FROM customers WHERE segment IN (SELECT segm FROM bar)"; let select = verified_only_select(sql); assert_eq!( - Expr::InSubquery { + Expr::In { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: Box::new(verified_query("SELECT segm FROM bar")), + target: InTarget::Subquery(Box::new(verified_query("SELECT segm FROM bar"))), negated: false, global: false, }, @@ -2582,11 +2582,11 @@ fn parse_in_union() { let sql = "SELECT * FROM customers WHERE segment IN ((SELECT segm FROM bar) UNION (SELECT segm FROM bar2))"; let select = verified_only_select(sql); assert_eq!( - Expr::InSubquery { + Expr::In { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - subquery: Box::new(verified_query( + target: InTarget::Subquery(Box::new(verified_query( "(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)" - )), + ))), negated: false, global: false, }, @@ -2603,10 +2603,11 @@ fn parse_in_unnest() { ); let select = verified_only_select(sql); assert_eq!( - Expr::InUnnest { + Expr::In { expr: Box::new(Expr::Identifier(Ident::new("segment"))), - array_expr: Box::new(verified_expr("expr")), + target: InTarget::Unnest(Box::new(verified_expr("expr"))), negated, + global: false, }, select.selection.unwrap() ); @@ -20490,9 +20491,9 @@ fn parse_in_list_leading_subquery() { let select = verified_only_select("SELECT * FROM t WHERE x IN ((SELECT a FROM u), 1)"); assert_eq!( - Expr::InList { + Expr::In { expr: x(), - list: vec![subquery(), Expr::value(number("1"))], + target: InTarget::List(vec![subquery(), Expr::value(number("1"))]), negated: false, global: false, }, @@ -20501,9 +20502,12 @@ fn parse_in_list_leading_subquery() { let select = verified_only_select("SELECT * FROM t WHERE x NOT IN (((SELECT a FROM u)), 1)"); assert_eq!( - Expr::InList { + Expr::In { expr: x(), - list: vec![Expr::Nested(Box::new(subquery())), Expr::value(number("1"))], + target: InTarget::List(vec![ + Expr::Nested(Box::new(subquery())), + Expr::value(number("1")) + ]), negated: true, global: false, }, @@ -20512,16 +20516,16 @@ fn parse_in_list_leading_subquery() { let select = verified_only_select("SELECT * FROM t WHERE x IN ((SELECT a FROM u) + 1, 2)"); assert_eq!( - Expr::InList { + Expr::In { expr: x(), - list: vec![ + target: InTarget::List(vec![ Expr::BinaryOp { left: Box::new(subquery()), op: BinaryOperator::Plus, right: Box::new(Expr::value(number("1"))), }, Expr::value(number("2")) - ], + ]), negated: false, global: false, }, @@ -20531,16 +20535,16 @@ fn parse_in_list_leading_subquery() { let select = pg_and_generic() .verified_only_select("SELECT * FROM t WHERE x IN ((SELECT a FROM u)[1], 2)"); assert_matches!( - select.selection, - Some(Expr::InList { ref list, .. }) + select.selection.as_ref(), + Some(Expr::In { target: InTarget::List(list), .. }) if matches!(&list[0], Expr::CompoundFieldAccess { root, .. } if **root == subquery()) ); let select = verified_only_select("SELECT * FROM t WHERE x IN ((SELECT a FROM u))"); assert_eq!( - Expr::InSubquery { + Expr::In { expr: x(), - subquery: Box::new(verified_query("(SELECT a FROM u)")), + target: InTarget::Subquery(Box::new(verified_query("(SELECT a FROM u)"))), negated: false, global: false, }, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 2e60c75efa..3e31049473 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -524,7 +524,11 @@ fn parse_update_tuple_row_values() { fn parse_where_in_empty_list() { let sql = "SELECT * FROM t1 WHERE a IN ()"; let select = sqlite().verified_only_select(sql); - if let Expr::InList { list, .. } = select.selection.as_ref().unwrap() { + if let Expr::In { + target: InTarget::List(list), + .. + } = select.selection.as_ref().unwrap() + { assert_eq!(list.len(), 0); } else { unreachable!() From 358a424cb25747a40075aaae8165d6ebdc54fb2e Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sun, 18 Oct 2026 16:49:32 +0200 Subject: [PATCH 2/2] Test source spans for `IN` expression targets --- src/ast/spans.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7b92b52bb7..8267c71798 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -3242,4 +3242,20 @@ SETTINGS update_parallel_mode = 'auto'"#; stmt => panic!("expected ALTER TABLE; got {stmt:?}"), } } + + #[test] + fn test_in_expression_spans() { + for (sql, expected) in [ + (" needle IN (first, last)", "needle IN (first, last"), + ( + " needle IN (SELECT last FROM items)", + "needle IN (SELECT last FROM items", + ), + (" needle IN UNNEST(items)", "needle IN UNNEST(items"), + ] { + let mut test = SpanTest::new(&GenericDialect, sql); + let expr = test.0.parse_expr().unwrap(); + assert_eq!(test.get_source(expr.span()), expected, "{sql}"); + } + } }