Skip to content

Commit f611954

Browse files
committed
Snowflake: parse LIKE/ILIKE ANY/ALL multi-pattern lists
Add an additive Expr::LikeAnyAll variant modelling <subj> [NOT] {LIKE|ILIKE} {ANY|ALL} (p1, ..., pN) [ESCAPE e], leaving the single-pattern Expr::Like / Expr::ILike shape untouched. The LIKE/ILIKE infix parser now routes a parenthesized {ANY|ALL} (...) list into the new variant.
1 parent 2b9fdc4 commit f611954

3 files changed

Lines changed: 99 additions & 18 deletions

File tree

src/ast/mod.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,23 @@ pub enum Expr {
10311031
/// Optional escape character.
10321032
escape_char: Option<ValueWithSpan>,
10331033
},
1034+
/// Snowflake `<expr> [NOT] {LIKE|ILIKE} {ANY|ALL} (<p1>, ..., <pN>) [ESCAPE <char>]`
1035+
/// matching a subject against a parenthesized list of patterns.
1036+
/// <https://docs.snowflake.com/en/sql-reference/functions/like_any>
1037+
LikeAnyAll {
1038+
/// `true` when `NOT` is present.
1039+
negated: bool,
1040+
/// `true` for `ILIKE`, `false` for `LIKE`.
1041+
ilike: bool,
1042+
/// `true` for the `ALL` quantifier, `false` for `ANY`.
1043+
all: bool,
1044+
/// Subject expression to match.
1045+
expr: Box<Expr>,
1046+
/// List of pattern expressions.
1047+
patterns: Vec<Expr>,
1048+
/// Optional escape character applied to every pattern.
1049+
escape_char: Option<ValueWithSpan>,
1050+
},
10341051
/// `SIMILAR TO` regex
10351052
SimilarTo {
10361053
/// `true` when `NOT` is present.
@@ -1853,6 +1870,28 @@ impl fmt::Display for Expr {
18531870
pattern
18541871
),
18551872
},
1873+
Expr::LikeAnyAll {
1874+
negated,
1875+
ilike,
1876+
all,
1877+
expr,
1878+
patterns,
1879+
escape_char,
1880+
} => {
1881+
write!(
1882+
f,
1883+
"{} {}{} {} ({})",
1884+
expr,
1885+
if *negated { "NOT " } else { "" },
1886+
if *ilike { "ILIKE" } else { "LIKE" },
1887+
if *all { "ALL" } else { "ANY" },
1888+
display_comma_separated(patterns),
1889+
)?;
1890+
if let Some(ch) = escape_char {
1891+
write!(f, " ESCAPE {ch}")?;
1892+
}
1893+
Ok(())
1894+
}
18561895
Expr::RLike {
18571896
negated,
18581897
expr,

src/ast/spans.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,16 @@ impl Spanned for Expr {
16451645
escape_char: _,
16461646
any: _,
16471647
} => expr.span().union(&pattern.span()),
1648+
Expr::LikeAnyAll {
1649+
negated: _,
1650+
ilike: _,
1651+
all: _,
1652+
expr,
1653+
patterns,
1654+
escape_char: _,
1655+
} => union_spans(
1656+
core::iter::once(expr.span()).chain(patterns.iter().map(|p| p.span())),
1657+
),
16481658
Expr::RLike { .. } => Span::empty(),
16491659
Expr::IsNormalized {
16501660
expr,

src/parser/mod.rs

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4365,25 +4365,9 @@ impl<'a> Parser<'a> {
43654365
} else if self.parse_keyword(Keyword::BETWEEN) {
43664366
self.parse_between(expr, negated)
43674367
} else if self.parse_keyword(Keyword::LIKE) {
4368-
Ok(Expr::Like {
4369-
negated,
4370-
any: self.parse_keyword(Keyword::ANY),
4371-
expr: Box::new(expr),
4372-
pattern: Box::new(
4373-
self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?,
4374-
),
4375-
escape_char: self.parse_escape_char()?,
4376-
})
4368+
self.parse_like_expr(negated, false, expr)
43774369
} else if self.parse_keyword(Keyword::ILIKE) {
4378-
Ok(Expr::ILike {
4379-
negated,
4380-
any: self.parse_keyword(Keyword::ANY),
4381-
expr: Box::new(expr),
4382-
pattern: Box::new(
4383-
self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?,
4384-
),
4385-
escape_char: self.parse_escape_char()?,
4386-
})
4370+
self.parse_like_expr(negated, true, expr)
43874371
} else if self.parse_keywords(&[Keyword::SIMILAR, Keyword::TO]) {
43884372
Ok(Expr::SimilarTo {
43894373
negated,
@@ -4446,6 +4430,54 @@ impl<'a> Parser<'a> {
44464430
}
44474431
}
44484432

4433+
/// Parse the tail of a `LIKE` / `ILIKE` predicate after the keyword has
4434+
/// been consumed. Handles Snowflake's `{ANY|ALL} (<p1>, ..., <pN>)`
4435+
/// multi-pattern list form as well as the ordinary single-pattern form.
4436+
fn parse_like_expr(
4437+
&mut self,
4438+
negated: bool,
4439+
ilike: bool,
4440+
expr: Expr,
4441+
) -> Result<Expr, ParserError> {
4442+
let quantifier = self.parse_one_of_keywords(&[Keyword::ANY, Keyword::ALL]);
4443+
if let Some(kw) = quantifier {
4444+
if self.consume_token(&Token::LParen) {
4445+
let patterns = self.parse_comma_separated0(Parser::parse_expr, Token::RParen)?;
4446+
self.expect_token(&Token::RParen)?;
4447+
return Ok(Expr::LikeAnyAll {
4448+
negated,
4449+
ilike,
4450+
all: kw == Keyword::ALL,
4451+
expr: Box::new(expr),
4452+
patterns,
4453+
escape_char: self.parse_escape_char()?,
4454+
});
4455+
}
4456+
// `ANY` without a parenthesized list falls back to the legacy
4457+
// single-pattern form (`ALL` has no single-pattern meaning here).
4458+
}
4459+
let any = quantifier == Some(Keyword::ANY);
4460+
let pattern = Box::new(self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?);
4461+
let escape_char = self.parse_escape_char()?;
4462+
if ilike {
4463+
Ok(Expr::ILike {
4464+
negated,
4465+
any,
4466+
expr: Box::new(expr),
4467+
pattern,
4468+
escape_char,
4469+
})
4470+
} else {
4471+
Ok(Expr::Like {
4472+
negated,
4473+
any,
4474+
expr: Box::new(expr),
4475+
pattern,
4476+
escape_char,
4477+
})
4478+
}
4479+
}
4480+
44494481
/// Parse the `ESCAPE CHAR` portion of `LIKE`, `ILIKE`, and `SIMILAR TO`
44504482
pub fn parse_escape_char(&mut self) -> Result<Option<ValueWithSpan>, ParserError> {
44514483
if self.parse_keyword(Keyword::ESCAPE) {

0 commit comments

Comments
 (0)