This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 62eaee62 Add support for MSSQL's `XQuery` methods (#1500)
62eaee62 is described below

commit 62eaee62dc12fe001992650bf5b330f065b92c07
Author: gaoqiangz <[email protected]>
AuthorDate: Fri Nov 15 04:32:57 2024 +0800

    Add support for MSSQL's `XQuery` methods (#1500)
    
    Co-authored-by: Ifeanyi Ubah <[email protected]>
---
 src/ast/mod.rs            | 39 ++++++++++++++++++++++++++
 src/dialect/mod.rs        |  9 ++++++
 src/dialect/mssql.rs      |  4 +++
 src/parser/mod.rs         | 39 ++++++++++++++++++++++++++
 tests/sqlparser_common.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 161 insertions(+)

diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 505386fb..b0ac6bc4 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>.<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>`,
@@ -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<Expr>,
+    // always non-empty
+    pub method_chain: Vec<Function>,
+}
+
+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 a732aa5a..ee3fd471 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 8aab0bc8..39ce9c12 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 4115bbc9..a66a627b 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<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.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<Expr, 
ParserError> {
         self.expect_token(&Token::LParen)?;
 
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index daf65edf..4fdbf7d5 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, 
'<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)')");
+    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, '<Book>abc</Book>').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


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to