Skip to content

Commit 2c78bb9

Browse files
Add xml '...' TypedString support for PostgreSQL
1 parent 72b295a commit 2c78bb9

File tree

6 files changed

+74
-1
lines changed

6 files changed

+74
-1
lines changed

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,8 @@ impl Dialect for GenericDialect {
292292
fn supports_cte_without_as(&self) -> bool {
293293
true
294294
}
295+
296+
fn supports_xml_expressions(&self) -> bool {
297+
true
298+
}
295299
}

src/dialect/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,6 +1681,17 @@ pub trait Dialect: Debug + Any {
16811681
fn supports_cte_without_as(&self) -> bool {
16821682
false
16831683
}
1684+
1685+
/// Returns true if the dialect supports XML-related expressions
1686+
/// such as `xml '<foo/>'` typed strings, XML functions like
1687+
/// `XMLCONCAT`, `XMLELEMENT`, etc.
1688+
///
1689+
/// When this returns false, `xml` is treated as a regular identifier.
1690+
///
1691+
/// [PostgreSQL](https://www.postgresql.org/docs/current/functions-xml.html)
1692+
fn supports_xml_expressions(&self) -> bool {
1693+
false
1694+
}
16841695
}
16851696

16861697
/// Operators for which precedence must be defined.

src/dialect/postgresql.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,8 @@ impl Dialect for PostgreSqlDialect {
310310
fn supports_comma_separated_trim(&self) -> bool {
311311
true
312312
}
313+
314+
fn supports_xml_expressions(&self) -> bool {
315+
true
316+
}
313317
}

src/parser/mod.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,16 @@ impl<'a> Parser<'a> {
16871687
}
16881688
}
16891689

1690+
/// Returns true if the given [ObjectName] is a single unquoted
1691+
/// identifier matching `expected` (case-insensitive).
1692+
fn is_simple_unquoted_object_name(name: &ObjectName, expected: &str) -> bool {
1693+
if let [ObjectNamePart::Identifier(ident)] = name.0.as_slice() {
1694+
ident.quote_style.is_none() && ident.value.eq_ignore_ascii_case(expected)
1695+
} else {
1696+
false
1697+
}
1698+
}
1699+
16901700
/// Parse an expression prefix.
16911701
pub fn parse_prefix(&mut self) -> Result<Expr, ParserError> {
16921702
// allow the dialect to override prefix parsing
@@ -1720,7 +1730,21 @@ impl<'a> Parser<'a> {
17201730
// so given `NOT 'a' LIKE 'b'`, we'd accept `NOT` as a possible custom data type
17211731
// name, resulting in `NOT 'a'` being recognized as a `TypedString` instead of
17221732
// an unary negation `NOT ('a' LIKE 'b')`. To solve this, we don't accept the
1723-
// `type 'string'` syntax for the custom data types at all.
1733+
// `type 'string'` syntax for the custom data types at all ...
1734+
//
1735+
// ... with the exception of `xml '...'` on dialects that support XML
1736+
// expressions, which is a valid PostgreSQL typed string literal.
1737+
DataType::Custom(ref name, ref modifiers)
1738+
if modifiers.is_empty()
1739+
&& Self::is_simple_unquoted_object_name(name, "xml")
1740+
&& parser.dialect.supports_xml_expressions() =>
1741+
{
1742+
Ok(Expr::TypedString(TypedString {
1743+
data_type: DataType::Custom(name.clone(), modifiers.clone()),
1744+
value: parser.parse_value()?,
1745+
uses_odbc_syntax: false,
1746+
}))
1747+
}
17241748
DataType::Custom(..) => parser_err!("dummy", loc),
17251749
// MySQL supports using the `BINARY` keyword as a cast to binary type.
17261750
DataType::Binary(..) if self.dialect.supports_binary_kw_as_cast() => {

tests/sqlparser_common.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18754,3 +18754,11 @@ fn test_wildcard_func_arg() {
1875418754
dialects.verified_expr("HASH(* EXCLUDE (col1))");
1875518755
dialects.verified_expr("HASH(* EXCLUDE (col1, col2))");
1875618756
}
18757+
18758+
#[test]
18759+
fn parse_non_pg_dialects_keep_xml_names_as_regular_identifiers() {
18760+
// On dialects that do NOT support XML expressions, bare `xml` should
18761+
// be treated as a regular column identifier, not a typed-string prefix.
18762+
let dialects = all_dialects_except(|d| d.supports_xml_expressions());
18763+
dialects.verified_only_select("SELECT xml FROM t");
18764+
}

tests/sqlparser_postgres.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3750,6 +3750,28 @@ fn parse_on_commit() {
37503750
pg_and_generic().verified_stmt("CREATE TEMPORARY TABLE table (COL INT) ON COMMIT DROP");
37513751
}
37523752

3753+
#[test]
3754+
fn parse_xml_typed_string() {
3755+
// xml '...' should parse as a TypedString on PostgreSQL and Generic
3756+
let sql = "SELECT xml '<foo/>'";
3757+
let select = pg_and_generic().verified_only_select(sql);
3758+
match expr_from_projection(&select.projection[0]) {
3759+
Expr::TypedString(TypedString {
3760+
data_type: DataType::Custom(name, modifiers),
3761+
value,
3762+
uses_odbc_syntax: false,
3763+
}) => {
3764+
assert_eq!(name.to_string(), "xml");
3765+
assert!(modifiers.is_empty());
3766+
assert_eq!(
3767+
value.value,
3768+
Value::SingleQuotedString("<foo/>".to_string())
3769+
);
3770+
}
3771+
other => panic!("Expected TypedString, got: {other:?}"),
3772+
}
3773+
}
3774+
37533775
fn pg() -> TestedDialects {
37543776
TestedDialects::new(vec![Box::new(PostgreSqlDialect {})])
37553777
}

0 commit comments

Comments
 (0)