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
24 changes: 22 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8023,6 +8023,11 @@ 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)`
Comment thread
LucaCappelletti94 marked this conversation as resolved.
///
/// [PostgreSQL](https://www.postgresql.org/docs/current/datatype-xml.html#DATATYPE-XML-CREATING)
Space,
}

impl fmt::Display for FunctionArgOperator {
Expand All @@ -8033,6 +8038,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 +8081,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
25 changes: 24 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2531,6 +2531,23 @@ impl<'a> Parser<'a> {
})
}

/// Parse the argument list of `XMLPARSE({ DOCUMENT | CONTENT } value)`,
/// including the closing parenthesis. The mode word becomes the name of the
/// single argument, with no operator between it and the value.
fn parse_xmlparse_argument_list(&mut self) -> Result<FunctionArgumentList, ParserError> {
let arg = FunctionArg::Named {
name: self.parse_identifier()?,
arg: FunctionArgExpr::Expr(self.parse_expr()?),
operator: FunctionArgOperator::Space,
};
self.expect_token(&Token::RParen)?;
Ok(FunctionArgumentList {
duplicate_treatment: None,
args: vec![arg],
clauses: 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> {
self.parse_function_call(name).map(Expr::Function)
Expand All @@ -2556,7 +2573,13 @@ impl<'a> Parser<'a> {
});
}

let mut args = self.parse_function_argument_list()?;
let mut args = if self.dialect.supports_xml_expressions()
&& Self::is_simple_unquoted_object_name(&name, "xmlparse")
{
self.parse_xmlparse_argument_list()?
} else {
self.parse_function_argument_list()?
};
let mut parameters = FunctionArguments::None;
// ClickHouse aggregations support parametric functions like `HISTOGRAM(0.5, 0.6)(x, y)`
// which (0.5, 0.6) is a parameter to the function.
Expand Down
50 changes: 50 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19501,6 +19501,56 @@ 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 needs both a mode word and a value.
assert!(dialects
.parse_sql_statements("SELECT xmlparse('<a/>')")
.is_err());

// Going through the ordinary function-call path, XMLPARSE accepts the same
// trailing clauses as any other function.
dialects.verified_stmt("SELECT xmlparse(document x) FILTER (WHERE y > 1)");
dialects.verified_stmt("SELECT xmlparse(document x) OVER (PARTITION BY y)");

// 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