mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-07-07 17:04:59 +00:00
Replace Method
and CompositeAccess
with CompoundFieldAccess
(#1716)
This commit is contained in:
parent
c75a992621
commit
3e90a18f6d
9 changed files with 348 additions and 264 deletions
|
@ -661,11 +661,6 @@ pub enum Expr {
|
|||
/// The path to the data to extract.
|
||||
path: JsonPath,
|
||||
},
|
||||
/// CompositeAccess eg: SELECT foo(bar).z, (information_schema._pg_expandarray(array['i','i'])).n
|
||||
CompositeAccess {
|
||||
expr: Box<Expr>,
|
||||
key: Ident,
|
||||
},
|
||||
/// `IS FALSE` operator
|
||||
IsFalse(Box<Expr>),
|
||||
/// `IS NOT FALSE` operator
|
||||
|
@ -915,23 +910,6 @@ pub enum Expr {
|
|||
},
|
||||
/// Scalar function call e.g. `LEFT(foo, 5)`
|
||||
Function(Function),
|
||||
/// Arbitrary expr method call
|
||||
///
|
||||
/// Syntax:
|
||||
///
|
||||
/// `<arbitrary-expr>.<function-call>.<function-call-expr>...`
|
||||
///
|
||||
/// > `arbitrary-expr` can be any expression including a function call.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```sql
|
||||
/// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)')
|
||||
/// SELECT CONVERT(XML,'<Book>abc</Book>').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)')
|
||||
/// ```
|
||||
///
|
||||
/// (mssql): <https://learn.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver16>
|
||||
Method(Method),
|
||||
/// `CASE [<operand>] WHEN <condition> THEN <result> ... [ELSE <result>] END`
|
||||
///
|
||||
/// Note we only recognize a complete single expression as `<condition>`,
|
||||
|
@ -1631,7 +1609,6 @@ impl fmt::Display for Expr {
|
|||
write!(f, " {value}")
|
||||
}
|
||||
Expr::Function(fun) => write!(f, "{fun}"),
|
||||
Expr::Method(method) => write!(f, "{method}"),
|
||||
Expr::Case {
|
||||
operand,
|
||||
conditions,
|
||||
|
@ -1789,9 +1766,6 @@ impl fmt::Display for Expr {
|
|||
Expr::JsonAccess { value, path } => {
|
||||
write!(f, "{value}{path}")
|
||||
}
|
||||
Expr::CompositeAccess { expr, key } => {
|
||||
write!(f, "{expr}.{key}")
|
||||
}
|
||||
Expr::AtTimeZone {
|
||||
timestamp,
|
||||
time_zone,
|
||||
|
|
|
@ -1288,7 +1288,6 @@ impl Spanned for Expr {
|
|||
match self {
|
||||
Expr::Identifier(ident) => ident.span,
|
||||
Expr::CompoundIdentifier(vec) => union_spans(vec.iter().map(|i| i.span)),
|
||||
Expr::CompositeAccess { expr, key } => expr.span().union(&key.span),
|
||||
Expr::CompoundFieldAccess { root, access_chain } => {
|
||||
union_spans(iter::once(root.span()).chain(access_chain.iter().map(|i| i.span())))
|
||||
}
|
||||
|
@ -1478,7 +1477,6 @@ impl Spanned for Expr {
|
|||
Expr::OuterJoin(expr) => expr.span(),
|
||||
Expr::Prior(expr) => expr.span(),
|
||||
Expr::Lambda(_) => Span::empty(),
|
||||
Expr::Method(_) => Span::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -251,6 +251,11 @@ pub trait Dialect: Debug + Any {
|
|||
false
|
||||
}
|
||||
|
||||
/// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN.
|
||||
fn supports_outer_join_operator(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if the dialect supports CONNECT BY.
|
||||
fn supports_connect_by(&self) -> bool {
|
||||
false
|
||||
|
@ -352,15 +357,6 @@ pub trait Dialect: Debug + Any {
|
|||
false
|
||||
}
|
||||
|
||||
/// Returns true if the dialect supports method calls, for example:
|
||||
///
|
||||
/// ```sql
|
||||
/// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)')
|
||||
/// ```
|
||||
fn supports_methods(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if the dialect supports multiple variable assignment
|
||||
/// using parentheses in a `SET` variable declaration.
|
||||
///
|
||||
|
@ -581,6 +577,7 @@ pub trait Dialect: Debug + Any {
|
|||
Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)),
|
||||
Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(p!(Between)),
|
||||
Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)),
|
||||
Token::Period => Ok(p!(Period)),
|
||||
Token::Eq
|
||||
| Token::Lt
|
||||
| Token::LtEq
|
||||
|
@ -654,6 +651,7 @@ pub trait Dialect: Debug + Any {
|
|||
/// Uses (APPROXIMATELY) <https://www.postgresql.org/docs/7.0/operators.htm#AEN2026> as a reference
|
||||
fn prec_value(&self, prec: Precedence) -> u8 {
|
||||
match prec {
|
||||
Precedence::Period => 100,
|
||||
Precedence::DoubleColon => 50,
|
||||
Precedence::AtTz => 41,
|
||||
Precedence::MulDivModOp => 40,
|
||||
|
@ -925,6 +923,7 @@ pub trait Dialect: Debug + Any {
|
|||
/// higher number -> higher precedence
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Precedence {
|
||||
Period,
|
||||
DoubleColon,
|
||||
AtTz,
|
||||
MulDivModOp,
|
||||
|
|
|
@ -46,6 +46,10 @@ impl Dialect for MsSqlDialect {
|
|||
true
|
||||
}
|
||||
|
||||
fn supports_outer_join_operator(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_connect_by(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -63,10 +67,6 @@ impl Dialect for MsSqlDialect {
|
|||
false
|
||||
}
|
||||
|
||||
fn supports_methods(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_named_fn_args_with_colon_operator(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ use crate::tokenizer::Token;
|
|||
#[derive(Debug)]
|
||||
pub struct PostgreSqlDialect {}
|
||||
|
||||
const PERIOD_PREC: u8 = 200;
|
||||
const DOUBLE_COLON_PREC: u8 = 140;
|
||||
const BRACKET_PREC: u8 = 130;
|
||||
const COLLATE_PREC: u8 = 120;
|
||||
|
@ -144,6 +145,7 @@ impl Dialect for PostgreSqlDialect {
|
|||
|
||||
fn prec_value(&self, prec: Precedence) -> u8 {
|
||||
match prec {
|
||||
Precedence::Period => PERIOD_PREC,
|
||||
Precedence::DoubleColon => DOUBLE_COLON_PREC,
|
||||
Precedence::AtTz => AT_TZ_PREC,
|
||||
Precedence::MulDivModOp => MUL_DIV_MOD_OP_PREC,
|
||||
|
|
|
@ -87,6 +87,11 @@ impl Dialect for SnowflakeDialect {
|
|||
true
|
||||
}
|
||||
|
||||
/// See <https://docs.snowflake.com/en/sql-reference/constructs/where#joins-in-the-where-clause>
|
||||
fn supports_outer_join_operator(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_connect_by(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -1021,6 +1021,8 @@ impl<'a> Parser<'a> {
|
|||
debug!("parsing expr");
|
||||
let mut expr = self.parse_prefix()?;
|
||||
|
||||
expr = self.parse_compound_expr(expr, vec![])?;
|
||||
|
||||
debug!("prefix: {:?}", expr);
|
||||
loop {
|
||||
let next_precedence = self.get_next_precedence()?;
|
||||
|
@ -1030,6 +1032,12 @@ impl<'a> Parser<'a> {
|
|||
break;
|
||||
}
|
||||
|
||||
// The period operator is handled exclusively by the
|
||||
// compound field access parsing.
|
||||
if Token::Period == self.peek_token_ref().token {
|
||||
break;
|
||||
}
|
||||
|
||||
expr = self.parse_infix(expr, next_precedence)?;
|
||||
}
|
||||
Ok(expr)
|
||||
|
@ -1105,8 +1113,8 @@ impl<'a> Parser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// Tries to parse an expression by matching the specified word to known keywords that have a special meaning in the dialect.
|
||||
// Returns `None if no match is found.
|
||||
/// Tries to parse an expression by matching the specified word to known keywords that have a special meaning in the dialect.
|
||||
/// Returns `None if no match is found.
|
||||
fn parse_expr_prefix_by_reserved_word(
|
||||
&mut self,
|
||||
w: &Word,
|
||||
|
@ -1203,7 +1211,7 @@ impl<'a> Parser<'a> {
|
|||
}
|
||||
Keyword::STRUCT if self.dialect.supports_struct_literal() => {
|
||||
let struct_expr = self.parse_struct_literal()?;
|
||||
Ok(Some(self.parse_compound_field_access(struct_expr, vec![])?))
|
||||
Ok(Some(struct_expr))
|
||||
}
|
||||
Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => {
|
||||
let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?;
|
||||
|
@ -1216,35 +1224,16 @@ impl<'a> Parser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// Tries to parse an expression by a word that is not known to have a special meaning in the dialect.
|
||||
/// Tries to parse an expression by a word that is not known to have a special meaning in the dialect.
|
||||
fn parse_expr_prefix_by_unreserved_word(
|
||||
&mut self,
|
||||
w: &Word,
|
||||
w_span: Span,
|
||||
) -> Result<Expr, ParserError> {
|
||||
match self.peek_token().token {
|
||||
Token::Period => self.parse_compound_field_access(
|
||||
Expr::Identifier(w.clone().into_ident(w_span)),
|
||||
vec![],
|
||||
),
|
||||
Token::LParen => {
|
||||
Token::LParen if !self.peek_outer_join_operator() => {
|
||||
let id_parts = vec![w.clone().into_ident(w_span)];
|
||||
if let Some(expr) = self.parse_outer_join_expr(&id_parts) {
|
||||
Ok(expr)
|
||||
} else {
|
||||
let mut expr = self.parse_function(ObjectName::from(id_parts))?;
|
||||
// consume all period if it's a method chain
|
||||
expr = self.try_parse_method(expr)?;
|
||||
let fields = vec![];
|
||||
self.parse_compound_field_access(expr, fields)
|
||||
}
|
||||
}
|
||||
Token::LBracket if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect) =>
|
||||
{
|
||||
let ident = Expr::Identifier(w.clone().into_ident(w_span));
|
||||
let mut fields = vec![];
|
||||
self.parse_multi_dim_subscript(&mut fields)?;
|
||||
self.parse_compound_field_access(ident, fields)
|
||||
self.parse_function(ObjectName::from(id_parts))
|
||||
}
|
||||
// string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html
|
||||
Token::SingleQuotedString(_)
|
||||
|
@ -1453,25 +1442,7 @@ impl<'a> Parser<'a> {
|
|||
}
|
||||
};
|
||||
self.expect_token(&Token::RParen)?;
|
||||
let expr = self.try_parse_method(expr)?;
|
||||
if !self.consume_token(&Token::Period) {
|
||||
Ok(expr)
|
||||
} else {
|
||||
let tok = self.next_token();
|
||||
let key = match tok.token {
|
||||
Token::Word(word) => word.into_ident(tok.span),
|
||||
_ => {
|
||||
return parser_err!(
|
||||
format!("Expected identifier, found: {tok}"),
|
||||
tok.span.start
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok(Expr::CompositeAccess {
|
||||
expr: Box::new(expr),
|
||||
key,
|
||||
})
|
||||
}
|
||||
Ok(expr)
|
||||
}
|
||||
Token::Placeholder(_) | Token::Colon | Token::AtSign => {
|
||||
self.prev_token();
|
||||
|
@ -1484,8 +1455,6 @@ impl<'a> Parser<'a> {
|
|||
_ => self.expected_at("an expression", next_token_index),
|
||||
}?;
|
||||
|
||||
let expr = self.try_parse_method(expr)?;
|
||||
|
||||
if self.parse_keyword(Keyword::COLLATE) {
|
||||
Ok(Expr::Collate {
|
||||
expr: Box::new(expr),
|
||||
|
@ -1499,62 +1468,72 @@ impl<'a> Parser<'a> {
|
|||
/// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`.
|
||||
/// If all the fields are `Expr::Identifier`s, return an [Expr::CompoundIdentifier] instead.
|
||||
/// If only the root exists, return the root.
|
||||
/// If self supports [Dialect::supports_partiql], it will fall back when occurs [Token::LBracket] for JsonAccess parsing.
|
||||
pub fn parse_compound_field_access(
|
||||
/// Parses compound expressions which may be delimited by period
|
||||
/// or bracket notation.
|
||||
/// For example: `a.b.c`, `a.b[1]`.
|
||||
pub fn parse_compound_expr(
|
||||
&mut self,
|
||||
root: Expr,
|
||||
mut chain: Vec<AccessExpr>,
|
||||
) -> Result<Expr, ParserError> {
|
||||
let mut ending_wildcard: Option<TokenWithSpan> = None;
|
||||
let mut ending_lbracket = false;
|
||||
while self.consume_token(&Token::Period) {
|
||||
let next_token = self.next_token();
|
||||
match next_token.token {
|
||||
Token::Word(w) => {
|
||||
let expr = Expr::Identifier(w.into_ident(next_token.span));
|
||||
chain.push(AccessExpr::Dot(expr));
|
||||
if self.peek_token().token == Token::LBracket {
|
||||
if self.dialect.supports_partiql() {
|
||||
self.next_token();
|
||||
ending_lbracket = true;
|
||||
break;
|
||||
loop {
|
||||
if self.consume_token(&Token::Period) {
|
||||
let next_token = self.peek_token_ref();
|
||||
match &next_token.token {
|
||||
Token::Mul => {
|
||||
// Postgres explicitly allows funcnm(tablenm.*) and the
|
||||
// function array_agg traverses this control flow
|
||||
if dialect_of!(self is PostgreSqlDialect) {
|
||||
ending_wildcard = Some(self.next_token());
|
||||
} else {
|
||||
self.parse_multi_dim_subscript(&mut chain)?
|
||||
// Put back the consumed `.` tokens before exiting.
|
||||
// If this expression is being parsed in the
|
||||
// context of a projection, then the `.*` could imply
|
||||
// a wildcard expansion. For example:
|
||||
// `SELECT STRUCT('foo').* FROM T`
|
||||
self.prev_token(); // .
|
||||
}
|
||||
}
|
||||
}
|
||||
Token::Mul => {
|
||||
// Postgres explicitly allows funcnm(tablenm.*) and the
|
||||
// function array_agg traverses this control flow
|
||||
if dialect_of!(self is PostgreSqlDialect) {
|
||||
ending_wildcard = Some(next_token);
|
||||
} else {
|
||||
// Put back the consumed .* tokens before exiting.
|
||||
// If this expression is being parsed in the
|
||||
// context of a projection, then this could imply
|
||||
// a wildcard expansion. For example:
|
||||
// `SELECT STRUCT('foo').* FROM T`
|
||||
self.prev_token(); // *
|
||||
self.prev_token(); // .
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
Token::SingleQuotedString(s) => {
|
||||
let expr = Expr::Identifier(Ident::with_quote('\'', s));
|
||||
chain.push(AccessExpr::Dot(expr));
|
||||
}
|
||||
_ => {
|
||||
return self.expected("an identifier or a '*' after '.'", next_token);
|
||||
break;
|
||||
}
|
||||
Token::SingleQuotedString(s) => {
|
||||
let expr =
|
||||
Expr::Identifier(Ident::with_quote_and_span('\'', next_token.span, s));
|
||||
chain.push(AccessExpr::Dot(expr));
|
||||
self.advance_token(); // The consumed string
|
||||
}
|
||||
// Fallback to parsing an arbitrary expression.
|
||||
_ => match self.parse_subexpr(self.dialect.prec_value(Precedence::Period))? {
|
||||
// If we get back a compound field access or identifier,
|
||||
// we flatten the nested expression.
|
||||
// For example if the current root is `foo`
|
||||
// and we get back a compound identifier expression `bar.baz`
|
||||
// The full expression should be `foo.bar.baz` (i.e.
|
||||
// a root with an access chain with 2 entries) and not
|
||||
// `foo.(bar.baz)` (i.e. a root with an access chain with
|
||||
// 1 entry`).
|
||||
Expr::CompoundFieldAccess { root, access_chain } => {
|
||||
chain.push(AccessExpr::Dot(*root));
|
||||
chain.extend(access_chain);
|
||||
}
|
||||
Expr::CompoundIdentifier(parts) => chain
|
||||
.extend(parts.into_iter().map(Expr::Identifier).map(AccessExpr::Dot)),
|
||||
expr => {
|
||||
chain.push(AccessExpr::Dot(expr));
|
||||
}
|
||||
},
|
||||
}
|
||||
} else if !self.dialect.supports_partiql()
|
||||
&& self.peek_token_ref().token == Token::LBracket
|
||||
{
|
||||
self.parse_multi_dim_subscript(&mut chain)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if dialect supports partiql, we need to go back one Token::LBracket for the JsonAccess parsing
|
||||
if self.dialect.supports_partiql() && ending_lbracket {
|
||||
self.prev_token();
|
||||
}
|
||||
|
||||
let tok_index = self.get_current_index();
|
||||
if let Some(wildcard_token) = ending_wildcard {
|
||||
if !Self::is_all_ident(&root, &chain) {
|
||||
return self.expected("an identifier or a '*' after '.'", self.peek_token());
|
||||
|
@ -1563,32 +1542,112 @@ impl<'a> Parser<'a> {
|
|||
ObjectName::from(Self::exprs_to_idents(root, chain)?),
|
||||
AttachedToken(wildcard_token),
|
||||
))
|
||||
} else if self.peek_token().token == Token::LParen {
|
||||
} else if self.maybe_parse_outer_join_operator() {
|
||||
if !Self::is_all_ident(&root, &chain) {
|
||||
// consume LParen
|
||||
self.next_token();
|
||||
return self.expected("an identifier or a '*' after '.'", self.peek_token());
|
||||
return self.expected_at("column identifier before (+)", tok_index);
|
||||
};
|
||||
let id_parts = Self::exprs_to_idents(root, chain)?;
|
||||
if let Some(expr) = self.parse_outer_join_expr(&id_parts) {
|
||||
Ok(expr)
|
||||
let expr = if chain.is_empty() {
|
||||
root
|
||||
} else {
|
||||
self.parse_function(ObjectName::from(id_parts))
|
||||
}
|
||||
} else if chain.is_empty() {
|
||||
Ok(root)
|
||||
Expr::CompoundIdentifier(Self::exprs_to_idents(root, chain)?)
|
||||
};
|
||||
Ok(Expr::OuterJoin(expr.into()))
|
||||
} else {
|
||||
if Self::is_all_ident(&root, &chain) {
|
||||
return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents(
|
||||
root, chain,
|
||||
)?));
|
||||
Self::build_compound_expr(root, chain)
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines a root expression and access chain to form
|
||||
/// a compound expression. Which may be a [Expr::CompoundFieldAccess]
|
||||
/// or other special cased expressions like [Expr::CompoundIdentifier],
|
||||
/// [Expr::OuterJoin].
|
||||
fn build_compound_expr(
|
||||
root: Expr,
|
||||
mut access_chain: Vec<AccessExpr>,
|
||||
) -> Result<Expr, ParserError> {
|
||||
if access_chain.is_empty() {
|
||||
return Ok(root);
|
||||
}
|
||||
|
||||
if Self::is_all_ident(&root, &access_chain) {
|
||||
return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents(
|
||||
root,
|
||||
access_chain,
|
||||
)?));
|
||||
}
|
||||
|
||||
// Flatten qualified function calls.
|
||||
// For example, the expression `a.b.c.foo(1,2,3)` should
|
||||
// represent a function called `a.b.c.foo`, rather than
|
||||
// a composite expression.
|
||||
if matches!(root, Expr::Identifier(_))
|
||||
&& matches!(
|
||||
access_chain.last(),
|
||||
Some(AccessExpr::Dot(Expr::Function(_)))
|
||||
)
|
||||
&& access_chain
|
||||
.iter()
|
||||
.rev()
|
||||
.skip(1) // All except the Function
|
||||
.all(|access| matches!(access, AccessExpr::Dot(Expr::Identifier(_))))
|
||||
{
|
||||
let Some(AccessExpr::Dot(Expr::Function(mut func))) = access_chain.pop() else {
|
||||
return parser_err!("expected function expression", root.span().start);
|
||||
};
|
||||
|
||||
let compound_func_name = [root]
|
||||
.into_iter()
|
||||
.chain(access_chain.into_iter().flat_map(|access| match access {
|
||||
AccessExpr::Dot(expr) => Some(expr),
|
||||
_ => None,
|
||||
}))
|
||||
.flat_map(|expr| match expr {
|
||||
Expr::Identifier(ident) => Some(ident),
|
||||
_ => None,
|
||||
})
|
||||
.map(ObjectNamePart::Identifier)
|
||||
.chain(func.name.0)
|
||||
.collect::<Vec<_>>();
|
||||
func.name = ObjectName(compound_func_name);
|
||||
|
||||
return Ok(Expr::Function(func));
|
||||
}
|
||||
|
||||
// Flatten qualified outer join expressions.
|
||||
// For example, the expression `T.foo(+)` should
|
||||
// represent an outer join on the column name `T.foo`
|
||||
// rather than a composite expression.
|
||||
if access_chain.len() == 1
|
||||
&& matches!(
|
||||
access_chain.last(),
|
||||
Some(AccessExpr::Dot(Expr::OuterJoin(_)))
|
||||
)
|
||||
{
|
||||
let Some(AccessExpr::Dot(Expr::OuterJoin(inner_expr))) = access_chain.pop() else {
|
||||
return parser_err!("expected (+) expression", root.span().start);
|
||||
};
|
||||
|
||||
if !Self::is_all_ident(&root, &[]) {
|
||||
return parser_err!("column identifier before (+)", root.span().start);
|
||||
};
|
||||
|
||||
let token_start = root.span().start;
|
||||
let mut idents = Self::exprs_to_idents(root, vec![])?;
|
||||
match *inner_expr {
|
||||
Expr::CompoundIdentifier(suffix) => idents.extend(suffix),
|
||||
Expr::Identifier(suffix) => idents.push(suffix),
|
||||
_ => {
|
||||
return parser_err!("column identifier before (+)", token_start);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Expr::CompoundFieldAccess {
|
||||
root: Box::new(root),
|
||||
access_chain: chain,
|
||||
})
|
||||
return Ok(Expr::OuterJoin(Expr::CompoundIdentifier(idents).into()));
|
||||
}
|
||||
|
||||
Ok(Expr::CompoundFieldAccess {
|
||||
root: Box::new(root),
|
||||
access_chain,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if the root is an identifier and all fields are identifiers.
|
||||
|
@ -1625,20 +1684,23 @@ impl<'a> Parser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Try to parse OuterJoin expression `(+)`
|
||||
fn parse_outer_join_expr(&mut self, id_parts: &[Ident]) -> Option<Expr> {
|
||||
if dialect_of!(self is SnowflakeDialect | MsSqlDialect)
|
||||
&& self.consume_tokens(&[Token::LParen, Token::Plus, Token::RParen])
|
||||
{
|
||||
Some(Expr::OuterJoin(Box::new(
|
||||
match <[Ident; 1]>::try_from(id_parts.to_vec()) {
|
||||
Ok([ident]) => Expr::Identifier(ident),
|
||||
Err(parts) => Expr::CompoundIdentifier(parts),
|
||||
},
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
/// Returns true if the next tokens indicate the outer join operator `(+)`.
|
||||
fn peek_outer_join_operator(&mut self) -> bool {
|
||||
if !self.dialect.supports_outer_join_operator() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let [maybe_lparen, maybe_plus, maybe_rparen] = self.peek_tokens_ref();
|
||||
Token::LParen == maybe_lparen.token
|
||||
&& Token::Plus == maybe_plus.token
|
||||
&& Token::RParen == maybe_rparen.token
|
||||
}
|
||||
|
||||
/// If the next tokens indicates the outer join operator `(+)`, consume
|
||||
/// the tokens and return true.
|
||||
fn maybe_parse_outer_join_operator(&mut self) -> bool {
|
||||
self.dialect.supports_outer_join_operator()
|
||||
&& self.consume_tokens(&[Token::LParen, Token::Plus, Token::RParen])
|
||||
}
|
||||
|
||||
pub fn parse_utility_options(&mut self) -> Result<Vec<UtilityOption>, ParserError> {
|
||||
|
@ -1688,41 +1750,6 @@ impl<'a> Parser<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Parses method call expression
|
||||
fn try_parse_method(&mut self, expr: Expr) -> Result<Expr, ParserError> {
|
||||
if !self.dialect.supports_methods() {
|
||||
return Ok(expr);
|
||||
}
|
||||
let method_chain = self.maybe_parse(|p| {
|
||||
let mut method_chain = Vec::new();
|
||||
while p.consume_token(&Token::Period) {
|
||||
let tok = p.next_token();
|
||||
let name = match tok.token {
|
||||
Token::Word(word) => word.into_ident(tok.span),
|
||||
_ => return p.expected("identifier", tok),
|
||||
};
|
||||
let func = match p.parse_function(ObjectName::from(vec![name]))? {
|
||||
Expr::Function(func) => func,
|
||||
_ => return p.expected("function", p.peek_token()),
|
||||
};
|
||||
method_chain.push(func);
|
||||
}
|
||||
if !method_chain.is_empty() {
|
||||
Ok(method_chain)
|
||||
} else {
|
||||
p.expected("function", p.peek_token())
|
||||
}
|
||||
})?;
|
||||
if let Some(method_chain) = method_chain {
|
||||
Ok(Expr::Method(Method {
|
||||
expr: Box::new(expr),
|
||||
method_chain,
|
||||
}))
|
||||
} else {
|
||||
Ok(expr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to parse the body of an [ODBC function] call.
|
||||
/// i.e. without the enclosing braces
|
||||
///
|
||||
|
@ -3281,21 +3308,9 @@ impl<'a> Parser<'a> {
|
|||
op: UnaryOperator::PGPostfixFactorial,
|
||||
expr: Box::new(expr),
|
||||
})
|
||||
} else if Token::LBracket == *tok {
|
||||
if dialect_of!(self is PostgreSqlDialect | DuckDbDialect | GenericDialect | ClickHouseDialect | BigQueryDialect)
|
||||
{
|
||||
let mut chain = vec![];
|
||||
// back to LBracket
|
||||
self.prev_token();
|
||||
self.parse_multi_dim_subscript(&mut chain)?;
|
||||
self.parse_compound_field_access(expr, chain)
|
||||
} else if self.dialect.supports_partiql() {
|
||||
self.prev_token();
|
||||
self.parse_json_access(expr)
|
||||
} else {
|
||||
parser_err!("Array subscripting is not supported", tok.span.start)
|
||||
}
|
||||
} else if dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == *tok {
|
||||
} else if Token::LBracket == *tok && self.dialect.supports_partiql()
|
||||
|| (dialect_of!(self is SnowflakeDialect | GenericDialect) && Token::Colon == *tok)
|
||||
{
|
||||
self.prev_token();
|
||||
self.parse_json_access(expr)
|
||||
} else {
|
||||
|
@ -3605,6 +3620,26 @@ impl<'a> Parser<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Returns references to the `N` next non-whitespace tokens
|
||||
/// that have not yet been processed.
|
||||
///
|
||||
/// See [`Self::peek_tokens`] for an example.
|
||||
pub fn peek_tokens_ref<const N: usize>(&self) -> [&TokenWithSpan; N] {
|
||||
let mut index = self.index;
|
||||
core::array::from_fn(|_| loop {
|
||||
let token = self.tokens.get(index);
|
||||
index += 1;
|
||||
if let Some(TokenWithSpan {
|
||||
token: Token::Whitespace(_),
|
||||
span: _,
|
||||
}) = token
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break token.unwrap_or(&EOF_TOKEN);
|
||||
})
|
||||
}
|
||||
|
||||
/// Return nth non-whitespace token that has not yet been processed
|
||||
pub fn peek_nth_token(&self, n: usize) -> TokenWithSpan {
|
||||
self.peek_nth_token_ref(n).clone()
|
||||
|
|
|
@ -73,6 +73,23 @@ fn parse_numeric_literal_underscore() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_function_object_name() {
|
||||
let select = verified_only_select("SELECT a.b.c.d(1, 2, 3) FROM T");
|
||||
let Expr::Function(func) = expr_from_projection(&select.projection[0]) else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(
|
||||
ObjectName::from(
|
||||
["a", "b", "c", "d"]
|
||||
.into_iter()
|
||||
.map(Ident::new)
|
||||
.collect::<Vec<_>>()
|
||||
),
|
||||
func.name,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_insert_values() {
|
||||
let row = vec![
|
||||
|
@ -936,6 +953,44 @@ fn parse_select_distinct_tuple() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_outer_join_operator() {
|
||||
let dialects = all_dialects_where(|d| d.supports_outer_join_operator());
|
||||
|
||||
let select = dialects.verified_only_select("SELECT 1 FROM T WHERE a = b (+)");
|
||||
assert_eq!(
|
||||
select.selection,
|
||||
Some(Expr::BinaryOp {
|
||||
left: Box::new(Expr::Identifier(Ident::new("a"))),
|
||||
op: BinaryOperator::Eq,
|
||||
right: Box::new(Expr::OuterJoin(Box::new(Expr::Identifier(Ident::new("b")))))
|
||||
})
|
||||
);
|
||||
|
||||
let select = dialects.verified_only_select("SELECT 1 FROM T WHERE t1.c1 = t2.c2.d3 (+)");
|
||||
assert_eq!(
|
||||
select.selection,
|
||||
Some(Expr::BinaryOp {
|
||||
left: Box::new(Expr::CompoundIdentifier(vec![
|
||||
Ident::new("t1"),
|
||||
Ident::new("c1")
|
||||
])),
|
||||
op: BinaryOperator::Eq,
|
||||
right: Box::new(Expr::OuterJoin(Box::new(Expr::CompoundIdentifier(vec![
|
||||
Ident::new("t2"),
|
||||
Ident::new("c2"),
|
||||
Ident::new("d3"),
|
||||
]))))
|
||||
})
|
||||
);
|
||||
|
||||
let res = dialects.parse_sql_statements("SELECT 1 FROM T WHERE 1 = 2 (+)");
|
||||
assert_eq!(
|
||||
ParserError::ParserError("Expected: column identifier before (+), found: 2".to_string()),
|
||||
res.unwrap_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_select_distinct_on() {
|
||||
let sql = "SELECT DISTINCT ON (album_id) name FROM track ORDER BY album_id, milliseconds";
|
||||
|
@ -12623,68 +12678,76 @@ fn test_try_convert() {
|
|||
|
||||
#[test]
|
||||
fn parse_method_select() {
|
||||
let dialects = all_dialects_where(|d| d.supports_methods());
|
||||
let _ = dialects.verified_only_select(
|
||||
let _ = verified_only_select(
|
||||
"SELECT LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T",
|
||||
);
|
||||
let _ = dialects.verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T");
|
||||
let _ = dialects
|
||||
.verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T");
|
||||
let _ = verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T");
|
||||
let _ = verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T");
|
||||
|
||||
// `CONVERT` support
|
||||
let dialects = all_dialects_where(|d| {
|
||||
d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value()
|
||||
});
|
||||
let dialects =
|
||||
all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value());
|
||||
let _ = dialects.verified_only_select("SELECT CONVERT(XML, '<Book>abc</Book>').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_method_expr() {
|
||||
let dialects = all_dialects_where(|d| d.supports_methods());
|
||||
let expr = dialects
|
||||
.verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')");
|
||||
let expr =
|
||||
verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')");
|
||||
match expr {
|
||||
Expr::Method(Method { expr, method_chain }) => {
|
||||
assert!(matches!(*expr, Expr::Function(_)));
|
||||
Expr::CompoundFieldAccess { root, access_chain } => {
|
||||
assert!(matches!(*root, Expr::Function(_)));
|
||||
assert!(matches!(
|
||||
method_chain[..],
|
||||
[Function { .. }, Function { .. }]
|
||||
access_chain[..],
|
||||
[
|
||||
AccessExpr::Dot(Expr::Function(_)),
|
||||
AccessExpr::Dot(Expr::Function(_))
|
||||
]
|
||||
));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let expr = dialects.verified_expr(
|
||||
|
||||
let expr = verified_expr(
|
||||
"(SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')",
|
||||
);
|
||||
match expr {
|
||||
Expr::Method(Method { expr, method_chain }) => {
|
||||
assert!(matches!(*expr, Expr::Subquery(_)));
|
||||
assert!(matches!(method_chain[..], [Function { .. }]));
|
||||
Expr::CompoundFieldAccess { root, access_chain } => {
|
||||
assert!(matches!(*root, Expr::Subquery(_)));
|
||||
assert!(matches!(
|
||||
access_chain[..],
|
||||
[AccessExpr::Dot(Expr::Function(_))]
|
||||
));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let expr = dialects.verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')");
|
||||
let expr = verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')");
|
||||
match expr {
|
||||
Expr::Method(Method { expr, method_chain }) => {
|
||||
assert!(matches!(*expr, Expr::Cast { .. }));
|
||||
assert!(matches!(method_chain[..], [Function { .. }]));
|
||||
Expr::CompoundFieldAccess { root, access_chain } => {
|
||||
assert!(matches!(*root, Expr::Cast { .. }));
|
||||
assert!(matches!(
|
||||
access_chain[..],
|
||||
[AccessExpr::Dot(Expr::Function(_))]
|
||||
));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// `CONVERT` support
|
||||
let dialects = all_dialects_where(|d| {
|
||||
d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value()
|
||||
});
|
||||
let dialects =
|
||||
all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value());
|
||||
let expr = dialects.verified_expr(
|
||||
"CONVERT(XML, '<Book>abc</Book>').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')",
|
||||
);
|
||||
match expr {
|
||||
Expr::Method(Method { expr, method_chain }) => {
|
||||
assert!(matches!(*expr, Expr::Convert { .. }));
|
||||
Expr::CompoundFieldAccess { root, access_chain } => {
|
||||
assert!(matches!(*root, Expr::Convert { .. }));
|
||||
assert!(matches!(
|
||||
method_chain[..],
|
||||
[Function { .. }, Function { .. }]
|
||||
access_chain[..],
|
||||
[
|
||||
AccessExpr::Dot(Expr::Function(_)),
|
||||
AccessExpr::Dot(Expr::Function(_))
|
||||
]
|
||||
));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
|
|
|
@ -2991,38 +2991,45 @@ fn parse_json_table_is_not_reserved() {
|
|||
fn test_composite_value() {
|
||||
let sql = "SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9";
|
||||
let select = pg().verified_only_select(sql);
|
||||
|
||||
let Expr::CompoundFieldAccess { root, access_chain } =
|
||||
expr_from_projection(&select.projection[0])
|
||||
else {
|
||||
unreachable!("expected projection: got {:?}", &select.projection[0]);
|
||||
};
|
||||
assert_eq!(
|
||||
SelectItem::UnnamedExpr(Expr::CompositeAccess {
|
||||
key: Ident::new("name"),
|
||||
expr: Box::new(Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![
|
||||
Ident::new("on_hand"),
|
||||
Ident::new("item")
|
||||
]))))
|
||||
}),
|
||||
select.projection[0]
|
||||
root.as_ref(),
|
||||
&Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![
|
||||
Ident::new("on_hand"),
|
||||
Ident::new("item")
|
||||
])))
|
||||
);
|
||||
assert_eq!(
|
||||
access_chain.as_slice(),
|
||||
&[AccessExpr::Dot(Expr::Identifier(Ident::new("name")))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
select.selection,
|
||||
Some(Expr::BinaryOp {
|
||||
left: Box::new(Expr::CompositeAccess {
|
||||
key: Ident::new("price"),
|
||||
expr: Box::new(Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![
|
||||
select.selection.as_ref().unwrap(),
|
||||
&Expr::BinaryOp {
|
||||
left: Box::new(Expr::CompoundFieldAccess {
|
||||
root: Expr::Nested(Box::new(Expr::CompoundIdentifier(vec![
|
||||
Ident::new("on_hand"),
|
||||
Ident::new("item")
|
||||
]))))
|
||||
])))
|
||||
.into(),
|
||||
access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("price")))]
|
||||
}),
|
||||
op: BinaryOperator::Gt,
|
||||
right: Box::new(Expr::Value(number("9")))
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
let sql = "SELECT (information_schema._pg_expandarray(ARRAY['i', 'i'])).n";
|
||||
let select = pg().verified_only_select(sql);
|
||||
assert_eq!(
|
||||
SelectItem::UnnamedExpr(Expr::CompositeAccess {
|
||||
key: Ident::new("n"),
|
||||
expr: Box::new(Expr::Nested(Box::new(Expr::Function(Function {
|
||||
&Expr::CompoundFieldAccess {
|
||||
root: Box::new(Expr::Nested(Box::new(Expr::Function(Function {
|
||||
name: ObjectName::from(vec![
|
||||
Ident::new("information_schema"),
|
||||
Ident::new("_pg_expandarray")
|
||||
|
@ -3046,9 +3053,10 @@ fn test_composite_value() {
|
|||
filter: None,
|
||||
over: None,
|
||||
within_group: vec![],
|
||||
}))))
|
||||
}),
|
||||
select.projection[0]
|
||||
})))),
|
||||
access_chain: vec![AccessExpr::Dot(Expr::Identifier(Ident::new("n")))],
|
||||
},
|
||||
expr_from_projection(&select.projection[0])
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue