diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 505386fbf..b0ac6bc41 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -808,6 +808,23 @@ pub enum Expr { }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), + /// Arbitrary expr method call + /// + /// Syntax: + /// + /// `.....` + /// + /// > `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,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') + /// ``` + /// + /// (mssql): + Method(Method), /// `CASE [] WHEN THEN ... [ELSE ] END` /// /// Note we only recognize a complete single expression as ``, @@ -1464,6 +1481,7 @@ impl fmt::Display for Expr { write!(f, " '{}'", &value::escape_single_quote_string(value)) } Expr::Function(fun) => write!(f, "{fun}"), + Expr::Method(method) => write!(f, "{method}"), Expr::Case { operand, conditions, @@ -5609,6 +5627,27 @@ impl fmt::Display for FunctionArgumentClause { } } +/// A method call +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct Method { + pub expr: Box, + // always non-empty + pub method_chain: Vec, +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}.{}", + self.expr, + display_separated(&self.method_chain, ".") + ) + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a732aa5a0..ee3fd4714 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -279,6 +279,15 @@ 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. /// diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 8aab0bc8a..39ce9c125 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -62,4 +62,8 @@ impl Dialect for MsSqlDialect { fn supports_boolean_literals(&self) -> bool { false } + + fn supports_methods(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4115bbc9a..a66a627bc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1317,6 +1317,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 { @@ -1346,6 +1347,9 @@ impl<'a> Parser<'a> { } _ => self.expected("an expression", next_token), }?; + + let expr = self.try_parse_method(expr)?; + if self.parse_keyword(Keyword::COLLATE) { Ok(Expr::Collate { expr: Box::new(expr), @@ -1403,6 +1407,41 @@ impl<'a> Parser<'a> { }) } + /// Parses method call expression + fn try_parse_method(&mut self, expr: Expr) -> Result { + 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.to_ident(), + _ => return p.expected("identifier", tok), + }; + let func = match p.parse_function(ObjectName(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) + } + } + pub fn parse_function(&mut self, name: ObjectName) -> Result { self.expect_token(&Token::LParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index daf65edf1..4fdbf7d51 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11403,6 +11403,76 @@ fn test_try_convert() { dialects.verified_expr("TRY_CONVERT('foo', VARCHAR(MAX))"); } +#[test] +fn parse_method_select() { + let dialects = all_dialects_where(|d| d.supports_methods()); + let _ = dialects.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"); + + // `CONVERT` support + let dialects = all_dialects_where(|d| { + d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() + }); + let _ = dialects.verified_only_select("SELECT CONVERT(XML, 'abc').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)')"); + match expr { + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Function(_))); + assert!(matches!( + method_chain[..], + [Function { .. }, Function { .. }] + )); + } + _ => unreachable!(), + } + let expr = dialects.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 { .. }])); + } + _ => unreachable!(), + } + let expr = dialects.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 { .. }])); + } + _ => unreachable!(), + } + + // `CONVERT` support + let dialects = all_dialects_where(|d| { + d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() + }); + let expr = dialects.verified_expr( + "CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')", + ); + match expr { + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Convert { .. })); + assert!(matches!( + method_chain[..], + [Function { .. }, Function { .. }] + )); + } + _ => unreachable!(), + } +} + #[test] fn test_show_dbs_schemas_tables_views() { // These statements are parsed the same by all dialects