Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
22 changes: 20 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8023,6 +8023,9 @@ pub enum FunctionArgOperator {
Colon,
/// function(arg1 VALUE value1)
Value,
/// function(arg1 value1), with no operator between the name and the value,
/// as in PostgreSQL `XMLPARSE(DOCUMENT value)`
Space,
}

impl fmt::Display for FunctionArgOperator {
Expand All @@ -8033,6 +8036,7 @@ impl fmt::Display for FunctionArgOperator {
FunctionArgOperator::Assignment => f.write_str(":="),
FunctionArgOperator::Colon => f.write_str(":"),
FunctionArgOperator::Value => f.write_str("VALUE"),
FunctionArgOperator::Space => Ok(()),
}
}
}
Expand Down Expand Up @@ -8075,17 +8079,31 @@ impl fmt::Display for FunctionArg {
name,
arg,
operator,
} => write!(f, "{name} {operator} {arg}"),
} => fmt_named_function_arg(f, name, operator, arg),
FunctionArg::ExprNamed {
name,
arg,
operator,
} => write!(f, "{name} {operator} {arg}"),
} => fmt_named_function_arg(f, name, operator, arg),
FunctionArg::Unnamed(unnamed_arg) => write!(f, "{unnamed_arg}"),
}
}
}

/// `FunctionArgOperator::Space` has no token of its own, so the name and the
/// value are separated by a single space instead.
fn fmt_named_function_arg(
f: &mut fmt::Formatter,
name: &impl fmt::Display,
operator: &FunctionArgOperator,
arg: &FunctionArgExpr,
) -> fmt::Result {
match operator {
FunctionArgOperator::Space => write!(f, "{name} {arg}"),
_ => write!(f, "{name} {operator} {arg}"),
}
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
Expand Down
39 changes: 39 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2531,8 +2531,47 @@ impl<'a> Parser<'a> {
})
}

/// Parse `XMLPARSE({ DOCUMENT | CONTENT } value)`, whose mode keyword is
/// carried by the single argument as a name with no operator. Neither mode
/// word is reserved, so both arrive as plain words rather than keywords.
fn parse_xmlparse_call(&mut self, name: ObjectName) -> Result<Function, ParserError> {
self.expect_token(&Token::LParen)?;
let is_mode = matches!(&self.peek_token_ref().token, Token::Word(word)
if word.quote_style.is_none()
&& (word.value.eq_ignore_ascii_case("content")
|| word.value.eq_ignore_ascii_case("document")));
if !is_mode {
return self.expected_ref("CONTENT or DOCUMENT", self.peek_token_ref());
}
let arg = FunctionArg::Named {
name: self.parse_identifier()?,
arg: FunctionArgExpr::Expr(self.parse_expr()?),
operator: FunctionArgOperator::Space,
};
self.expect_token(&Token::RParen)?;
Ok(Function {
name,
uses_odbc_syntax: false,
parameters: FunctionArguments::None,
args: FunctionArguments::List(FunctionArgumentList {
duplicate_treatment: None,
args: vec![arg],
clauses: vec![],
}),
filter: None,
null_treatment: None,
over: None,
within_group: vec![],
})
}

/// Parse a function call expression named by `name` and return it as an `Expr`.
pub fn parse_function(&mut self, name: ObjectName) -> Result<Expr, ParserError> {
if self.dialect.supports_xml_expressions()
&& Self::is_simple_unquoted_object_name(&name, "xmlparse")
{
return self.parse_xmlparse_call(name).map(Expr::Function);
}
self.parse_function_call(name).map(Expr::Function)
}

Expand Down
45 changes: 45 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19487,6 +19487,51 @@ fn parse_aliased_function_args() {
.is_err());
}

#[test]
fn parse_xmlparse() {
let dialects = all_dialects_where(|d| d.supports_xml_expressions());

for (sql, mode) in [
("SELECT xmlparse(content '<a/>')", "content"),
("SELECT xmlparse(document '<a/>')", "document"),
] {
let select = dialects.verified_only_select(sql);
match expr_from_projection(&select.projection[0]) {
Expr::Function(Function {
name,
args: FunctionArguments::List(list),
..
}) => {
assert_eq!(name.to_string(), "xmlparse");
assert_eq!(
list.args,
vec![FunctionArg::Named {
name: Ident::new(mode),
arg: FunctionArgExpr::Expr(Expr::Value(
Value::SingleQuotedString("<a/>".to_string()).into()
)),
operator: FunctionArgOperator::Space,
}]
);
}
expr => panic!("expected an XMLPARSE function call, got {expr:?}"),
}
}

// XMLPARSE requires a CONTENT or DOCUMENT mode.
assert!(dialects
.parse_sql_statements("SELECT xmlparse('<a/>')")
.is_err());

// On dialects without XML support, `xmlparse` stays a regular function
// and the special `CONTENT <expr>` syntax is rejected.
let others = all_dialects_except(|d| d.supports_xml_expressions());
others.verified_only_select("SELECT xmlparse(1)");
assert!(others
.parse_sql_statements("SELECT xmlparse(content '<a/>')")
.is_err());
}

/// Regression test for the 2^N parse-time blowup in `parse_compound_expr` on
/// inputs like `IF a0.a1...aN.#`. The parse is run on a worker thread and the
/// main thread asserts that it reports back within a generous timeout. Post-fix
Expand Down
15 changes: 15 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4130,6 +4130,21 @@ fn parse_xmlforest_aliased_arguments() {
);
}

#[test]
fn parse_xmlparse() {
// The parser only distinguishes the two modes, so the corpus covers those
// plus a non-literal argument.
let statements = [
"SELECT XMLPARSE(CONTENT '')",
"SELECT XMLPARSE(CONTENT '<abc>x</abc>')",
"SELECT XMLPARSE(DOCUMENT '<abc>x</abc>')",
"SELECT XMLPARSE(DOCUMENT col || '</abc>')",
];
for sql in statements {
pg().verified_stmt(sql);
}
}

#[test]
fn parse_xml_typed_string() {
// xml '...' should parse as a TypedString on PostgreSQL and Generic
Expand Down
Loading