Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for MSSQL's XQuery methods #1500

Merged
merged 21 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,21 @@ pub enum Expr {
},
/// Scalar function call e.g. `LEFT(foo, 5)`
Function(Function),
/// Arbitrary expr method call
///
/// Syntax:
///
/// `<arbitrary-expr>.<arbitrary-expr>.<arbitrary-expr>...`
///
/// > `arbitrary-expr` can be any expression including a function call.
gaoqiangz marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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)')
gaoqiangz marked this conversation as resolved.
Show resolved Hide resolved
/// ```
Method(Method),
iffyio marked this conversation as resolved.
Show resolved Hide resolved
/// `CASE [<operand>] WHEN <condition> THEN <result> ... [ELSE <result>] END`
///
/// Note we only recognize a complete single expression as `<condition>`,
Expand Down Expand Up @@ -1464,6 +1479,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,
Expand Down Expand Up @@ -5593,6 +5609,21 @@ 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>,
pub method: Function,
}
gaoqiangz marked this conversation as resolved.
Show resolved Hide resolved

impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}.{}", self.expr, self.method,)
}
}

#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
Expand Down
9 changes: 9 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ impl Dialect for MsSqlDialect {
fn supports_try_convert(&self) -> bool {
true
}

fn supports_methods(&self) -> bool {
true
}
}
84 changes: 66 additions & 18 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,7 +1012,7 @@ impl<'a> Parser<'a> {
}

let next_token = self.next_token();
let expr = match next_token.token {
let mut expr = match next_token.token {
Token::Word(w) => match w.keyword {
Keyword::TRUE | Keyword::FALSE | Keyword::NULL => {
self.prev_token();
Expand Down Expand Up @@ -1258,23 +1258,27 @@ impl<'a> Parser<'a> {
}
};
self.expect_token(&Token::RParen)?;
if !self.consume_token(&Token::Period) {
if let Some(expr) = self.try_parse_method(&expr)? {
Ok(expr)
} else {
let tok = self.next_token();
let key = match tok.token {
Token::Word(word) => word.to_ident(),
_ => {
return parser_err!(
format!("Expected identifier, found: {tok}"),
tok.location
)
}
};
Ok(Expr::CompositeAccess {
expr: Box::new(expr),
key,
})
if !self.consume_token(&Token::Period) {
Ok(expr)
} else {
gaoqiangz marked this conversation as resolved.
Show resolved Hide resolved
let tok = self.next_token();
let key = match tok.token {
Token::Word(word) => word.to_ident(),
_ => {
return parser_err!(
format!("Expected identifier, found: {tok}"),
tok.location
)
}
};
Ok(Expr::CompositeAccess {
expr: Box::new(expr),
key,
})
}
}
}
Token::Placeholder(_) | Token::Colon | Token::AtSign => {
Expand All @@ -1288,6 +1292,11 @@ impl<'a> Parser<'a> {
_ => self.expected("an expression", next_token),
}?;

// parse method calls
if let Some(method) = self.try_parse_method(&expr)? {
expr = method;
}

if self.parse_keyword(Keyword::COLLATE) {
Ok(Expr::Collate {
expr: Box::new(expr),
Expand Down Expand Up @@ -1345,6 +1354,45 @@ impl<'a> Parser<'a> {
})
}

fn try_parse_method(&mut self, expr: &Expr) -> Result<Option<Expr>, ParserError> {
gaoqiangz marked this conversation as resolved.
Show resolved Hide resolved
if !self.dialect.supports_methods() {
return Ok(None);
}
self.maybe_parse(|p| {
let mut method = None;
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()),
};
match method.take() {
Some(expr) => {
method = Some(Expr::Method(Method {
expr: Box::new(expr),
method: func,
}));
}
None => {
method = Some(Expr::Method(Method {
expr: Box::new(expr.clone()),
method: func,
}))
}
}
}
if let Some(method) = method {
Ok(method)
} else {
p.expected("method", p.peek_token())
}
})
}

pub fn parse_function(&mut self, name: ObjectName) -> Result<Expr, ParserError> {
self.expect_token(&Token::LParen)?;

Expand Down Expand Up @@ -3532,9 +3580,9 @@ impl<'a> Parser<'a> {
}

/// Run a parser method `f`, reverting back to the current position if unsuccessful.
pub fn maybe_parse<T, F>(&mut self, mut f: F) -> Result<Option<T>, ParserError>
pub fn maybe_parse<T, F>(&mut self, f: F) -> Result<Option<T>, ParserError>
where
F: FnMut(&mut Parser) -> Result<T, ParserError>,
F: FnOnce(&mut Parser) -> Result<T, ParserError>,
gaoqiangz marked this conversation as resolved.
Show resolved Hide resolved
{
let index = self.index;
match f(self) {
Expand Down
76 changes: 76 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11387,6 +11387,82 @@ 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: Function { .. },
}) => match *expr {
Expr::Method(Method {
expr,
method: Function { .. },
}) if matches!(*expr, Expr::Function(_)) => {}
_ => unreachable!(),
},
_ => 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: Function { .. },
}) if matches!(*expr, Expr::Subquery(_)) => {}
_ => unreachable!(),
}
let expr = dialects.verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')");
match expr {
Expr::Method(Method {
expr,
method: Function { .. },
}) if matches!(*expr, Expr::Cast { .. }) => {}
_ => 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: Function { .. },
}) => match *expr {
Expr::Method(Method {
expr,
method: Function { .. },
}) if matches!(*expr, Expr::Convert { .. }) => {}
_ => unreachable!(),
},
_ => unreachable!(),
}
}

#[test]
fn test_show_dbs_schemas_tables_views() {
verified_stmt("SHOW DATABASES");
Expand Down