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]