Replace Method and CompositeAccess with CompoundFieldAccess (#1716)

This commit is contained in:
Ifeanyi Ubah 2025-02-19 18:49:42 +01:00 committed by GitHub
parent c75a992621
commit 3e90a18f6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 348 additions and 264 deletions

View file

@ -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,

View file

@ -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(),
}
}
}

View file

@ -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,

View file

@ -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
}

View file

@ -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,

View file

@ -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
}

View file

@ -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()

View file

@ -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!(),

View file

@ -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])
);
}