Skip to content

Add support for quantified comparison predicates (ALL/ANY/SOME) #1459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
24 changes: 22 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,8 @@ pub enum Expr {
left: Box<Expr>,
compare_op: BinaryOperator,
right: Box<Expr>,
// ANY and SOME are synonymous
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any one of the supported dialects supporting the SOME variant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah cool! Can we include the link in the comment here as well? It could help later on in case someone comes across this and wonders the same

is_some: bool,
},
/// `ALL` operation e.g. `foo > ALL(bar)`, comparison operator is one of `[=, >, <, =>, =<, !=]`
AllOp {
Expand Down Expand Up @@ -1332,12 +1334,30 @@ impl fmt::Display for Expr {
left,
compare_op,
right,
} => write!(f, "{left} {compare_op} ANY({right})"),
is_some,
} => {
let add_parens = !matches!(right.as_ref(), Expr::Subquery(_));
write!(
f,
"{left} {compare_op} {}{}{right}{}",
if *is_some { "SOME" } else { "ANY" },
if add_parens { "(" } else { "" },
if add_parens { ")" } else { "" },
)
}
Expr::AllOp {
left,
compare_op,
right,
} => write!(f, "{left} {compare_op} ALL({right})"),
} => {
let add_parens = !matches!(right.as_ref(), Expr::Subquery(_));
write!(
f,
"{left} {compare_op} ALL{}{right}{}",
if add_parens { "(" } else { "" },
if add_parens { ")" } else { "" },
)
}
Expr::UnaryOp { op, expr } => {
if op == &UnaryOperator::PGPostfixFactorial {
write!(f, "{expr}{op}")
Expand Down
43 changes: 26 additions & 17 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1302,10 +1302,7 @@ impl<'a> Parser<'a> {
}

fn try_parse_expr_sub_query(&mut self) -> Result<Option<Expr>, ParserError> {
if self
.parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH])
.is_none()
{
if !self.is_query_ahead() {
return Ok(None);
}
self.prev_token();
Expand Down Expand Up @@ -1334,11 +1331,7 @@ impl<'a> Parser<'a> {

// Snowflake permits a subquery to be passed as an argument without
// an enclosing set of parens if it's the only argument.
if dialect_of!(self is SnowflakeDialect)
&& self
.parse_one_of_keywords(&[Keyword::WITH, Keyword::SELECT])
.is_some()
{
if dialect_of!(self is SnowflakeDialect) && self.is_query_ahead() {
self.prev_token();
let subquery = self.parse_boxed_query()?;
self.expect_token(&Token::RParen)?;
Expand Down Expand Up @@ -2639,10 +2632,22 @@ impl<'a> Parser<'a> {
};

if let Some(op) = regular_binary_operator {
if let Some(keyword) = self.parse_one_of_keywords(&[Keyword::ANY, Keyword::ALL]) {
if let Some(keyword) =
self.parse_one_of_keywords(&[Keyword::ANY, Keyword::ALL, Keyword::SOME])
{
self.expect_token(&Token::LParen)?;
let right = self.parse_subexpr(precedence)?;
self.expect_token(&Token::RParen)?;
let right = if self.is_query_ahead() {
// We have a subquery ahead (SELECT\WITH ...) need to rewind and
// use the parenthesis for parsing the subquery as an expression.
self.prev_token(); // SELECT
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to call the self.prev_token() as part of the is_query_ahead() logic? Currently with the split it feels a bit disconnected that the method is abstracted but the caller here still has to know the details and manually rewind the token stream which seems like an arbitrary split of responsibility? So that if it works the same then the caller can only rely on the flag returned without knowing the details of how the check is done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea, implemented in the new commit.

self.prev_token(); // LParen
self.parse_subexpr(precedence)?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to call self.try_parse_expr_sub_query() here already? it seems like it could let us avoid most of the if block here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to make that work in the new commit, but since we need to rewind the LParen first it got a bit messy

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right that makes sense!

} else {
// Non-subquery expression
let right = self.parse_subexpr(precedence)?;
self.expect_token(&Token::RParen)?;
right
};

if !matches!(
op,
Expand All @@ -2667,10 +2672,11 @@ impl<'a> Parser<'a> {
compare_op: op,
right: Box::new(right),
},
Keyword::ANY => Expr::AnyOp {
Keyword::ANY | Keyword::SOME => Expr::AnyOp {
left: Box::new(expr),
compare_op: op,
right: Box::new(right),
is_some: keyword == Keyword::SOME,
},
_ => unreachable!(),
})
Expand Down Expand Up @@ -10471,10 +10477,7 @@ impl<'a> Parser<'a> {
vec![]
};
PivotValueSource::Any(order_by)
} else if self
.parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH])
.is_some()
{
} else if self.is_query_ahead() {
self.prev_token();
PivotValueSource::Subquery(self.parse_query()?)
} else {
Expand Down Expand Up @@ -12141,6 +12144,12 @@ impl<'a> Parser<'a> {
pub fn into_tokens(self) -> Vec<TokenWithLocation> {
self.tokens
}

/// Checks if the next keyword indicates a query, i.e. SELECT or WITH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Checks if the next keyword indicates a query, i.e. SELECT or WITH
/// Returns true if the next keyword indicates a sub query, i.e. SELECT or WITH

pub fn is_query_ahead(&mut self) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn is_query_ahead(&mut self) -> bool {
fn peek_sub_query(&mut self) -> bool {

also the function was made public, assuming that was unintentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in new commit

self.parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH])
.is_some()
}
}

impl Word {
Expand Down
9 changes: 9 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,7 @@ fn parse_binary_any() {
left: Box::new(Expr::Identifier(Ident::new("a"))),
compare_op: BinaryOperator::Eq,
right: Box::new(Expr::Identifier(Ident::new("b"))),
is_some: false,
}),
select.projection[0]
);
Expand Down Expand Up @@ -11309,3 +11310,11 @@ fn test_select_where_with_like_or_ilike_any() {
verified_stmt(r#"SELECT * FROM x WHERE a ILIKE ANY ('%Jo%oe%', 'T%e')"#);
verified_stmt(r#"SELECT * FROM x WHERE a LIKE ANY ('%Jo%oe%', 'T%e')"#);
}

#[test]
fn test_any_some_all_comparison() {
verified_stmt("SELECT c1 FROM tbl WHERE c1 = ANY(SELECT c2 FROM tbl)");
verified_stmt("SELECT c1 FROM tbl WHERE c1 >= ALL(SELECT c2 FROM tbl)");
verified_stmt("SELECT c1 FROM tbl WHERE c1 <> SOME(SELECT c2 FROM tbl)");
verified_stmt("SELECT 1 = ANY(WITH x AS (SELECT 1) SELECT * FROM x)");
}