Skip to content

Commit 82dff42

Browse files
committed
xmltable table-valued function
adds support for xmltable(...) see https://www.postgresql.org/docs/15/functions-xml.html#FUNCTIONS-XML-PROCESSING fixes apache#1816
1 parent 4a48729 commit 82dff42

File tree

6 files changed

+261
-1
lines changed

6 files changed

+261
-1
lines changed

Diff for: src/ast/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ pub use self::query::{
8181
TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier,
8282
TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion,
8383
TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values,
84-
WildcardAdditionalOptions, With, WithFill,
84+
WildcardAdditionalOptions, With, WithFill, XmlPassingArgument, XmlPassingClause,
85+
XmlTableColumn, XmlTableColumnOption,
8586
};
8687

8788
pub use self::trigger::{

Diff for: src/ast/query.rs

+153
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,36 @@ pub enum TableFactor {
12711271
symbols: Vec<SymbolDefinition>,
12721272
alias: Option<TableAlias>,
12731273
},
1274+
/// The `XMLTABLE` table-valued function.
1275+
/// Part of the SQL standard, supported by PostgreSQL, Oracle, and DB2.
1276+
///
1277+
/// <https://www.postgresql.org/docs/15/functions-xml.html#FUNCTIONS-XML-PROCESSING>
1278+
///
1279+
/// ```sql
1280+
/// SELECT xmltable.*
1281+
/// FROM xmldata,
1282+
/// XMLTABLE('//ROWS/ROW'
1283+
/// PASSING data
1284+
/// COLUMNS id int PATH '@id',
1285+
/// ordinality FOR ORDINALITY,
1286+
/// "COUNTRY_NAME" text,
1287+
/// country_id text PATH 'COUNTRY_ID',
1288+
/// size_sq_km float PATH 'SIZE[@unit = "sq_km"]',
1289+
/// size_other text PATH 'concat(SIZE[@unit!="sq_km"], " ", SIZE[@unit!="sq_km"]/@unit)',
1290+
/// premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'
1291+
/// );
1292+
/// ````
1293+
XmlTable {
1294+
// TODO: Add XMLNAMESPACES clause support
1295+
/// The row-generating XPath expression.
1296+
row_expression: Expr,
1297+
/// The PASSING clause specifying the document expression.
1298+
passing: XmlPassingClause,
1299+
/// The columns to be extracted from each generated row.
1300+
columns: Vec<XmlTableColumn>,
1301+
/// The alias for the table.
1302+
alias: Option<TableAlias>,
1303+
},
12741304
}
12751305

12761306
/// The table sample modifier options
@@ -1936,6 +1966,22 @@ impl fmt::Display for TableFactor {
19361966
}
19371967
Ok(())
19381968
}
1969+
TableFactor::XmlTable {
1970+
row_expression,
1971+
passing,
1972+
columns,
1973+
alias,
1974+
} => {
1975+
write!(
1976+
f,
1977+
"XMLTABLE({row_expression}{passing} COLUMNS {columns})",
1978+
columns = display_comma_separated(columns)
1979+
)?;
1980+
if let Some(alias) = alias {
1981+
write!(f, " AS {alias}")?;
1982+
}
1983+
Ok(())
1984+
}
19391985
}
19401986
}
19411987
}
@@ -3082,3 +3128,110 @@ pub enum UpdateTableFromKind {
30823128
/// For Example: `UPDATE SET t1.name='aaa' FROM t1`
30833129
AfterSet(Vec<TableWithJoins>),
30843130
}
3131+
3132+
/// Defines the options for an XmlTable column: Named or ForOrdinality
3133+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
3134+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
3135+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3136+
pub enum XmlTableColumnOption {
3137+
/// A named column with a type, optional path, and default value.
3138+
NamedInfo {
3139+
/// The type of the column to be extracted.
3140+
r#type: DataType,
3141+
/// The path to the column to be extracted. If None, defaults to the column name.
3142+
path: Option<Expr>,
3143+
/// Default value if path does not match
3144+
default: Option<Expr>,
3145+
// TODO: Add NULL ON EMPTY / ERROR handling if needed later
3146+
// TODO: Add NOT NULL / NULL constraints
3147+
},
3148+
/// The FOR ORDINALITY marker
3149+
ForOrdinality,
3150+
}
3151+
3152+
/// A single column definition in XMLTABLE
3153+
///
3154+
/// ```sql
3155+
/// COLUMNS
3156+
/// id int PATH '@id',
3157+
/// ordinality FOR ORDINALITY,
3158+
/// "COUNTRY_NAME" text,
3159+
/// country_id text PATH 'COUNTRY_ID',
3160+
/// size_sq_km float PATH 'SIZE[@unit = "sq_km"]',
3161+
/// size_other text PATH 'concat(SIZE[@unit!="sq_km"], " ", SIZE[@unit!="sq_km"]/@unit)',
3162+
/// premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'
3163+
/// ```
3164+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
3165+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
3166+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3167+
pub struct XmlTableColumn {
3168+
/// The name of the column.
3169+
pub name: Ident,
3170+
/// Column options: type/path/default or FOR ORDINALITY
3171+
pub option: XmlTableColumnOption,
3172+
}
3173+
3174+
impl fmt::Display for XmlTableColumn {
3175+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3176+
write!(f, "{}", self.name)?;
3177+
match &self.option {
3178+
XmlTableColumnOption::NamedInfo {
3179+
r#type,
3180+
path,
3181+
default,
3182+
} => {
3183+
write!(f, " {}", r#type)?;
3184+
if let Some(p) = path {
3185+
write!(f, " PATH {}", p)?;
3186+
}
3187+
if let Some(d) = default {
3188+
write!(f, " DEFAULT {}", d)?;
3189+
}
3190+
Ok(())
3191+
}
3192+
XmlTableColumnOption::ForOrdinality => {
3193+
write!(f, " FOR ORDINALITY")
3194+
}
3195+
}
3196+
}
3197+
}
3198+
3199+
/// Argument passed in the XMLTABLE PASSING clause
3200+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
3201+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
3202+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3203+
pub struct XmlPassingArgument {
3204+
pub expr: Expr,
3205+
pub alias: Option<Ident>,
3206+
pub by_value: bool, // True if BY VALUE is specified
3207+
}
3208+
3209+
impl fmt::Display for XmlPassingArgument {
3210+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3211+
if self.by_value {
3212+
write!(f, "BY VALUE ")?;
3213+
}
3214+
write!(f, "{}", self.expr)?;
3215+
if let Some(alias) = &self.alias {
3216+
write!(f, " AS {}", alias)?;
3217+
}
3218+
Ok(())
3219+
}
3220+
}
3221+
3222+
/// The PASSING clause for XMLTABLE
3223+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
3224+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
3225+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3226+
pub struct XmlPassingClause {
3227+
pub arguments: Vec<XmlPassingArgument>,
3228+
}
3229+
3230+
impl fmt::Display for XmlPassingClause {
3231+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3232+
if !self.arguments.is_empty() {
3233+
write!(f, " PASSING {}", display_comma_separated(&self.arguments))?;
3234+
}
3235+
Ok(())
3236+
}
3237+
}

Diff for: src/ast/spans.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,7 @@ impl Spanned for TableFactor {
19091909
.chain(alias.as_ref().map(|alias| alias.span())),
19101910
),
19111911
TableFactor::JsonTable { .. } => Span::empty(),
1912+
TableFactor::XmlTable { .. } => Span::empty(),
19121913
TableFactor::Pivot {
19131914
table,
19141915
aggregate_functions,

Diff for: src/keywords.rs

+2
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,7 @@ define_keywords!(
654654
PARTITION,
655655
PARTITIONED,
656656
PARTITIONS,
657+
PASSING,
657658
PASSWORD,
658659
PAST,
659660
PATH,
@@ -989,6 +990,7 @@ define_keywords!(
989990
WORK,
990991
WRITE,
991992
XML,
993+
XMLTABLE,
992994
XOR,
993995
YEAR,
994996
YEARS,

Diff for: src/parser/mod.rs

+75
Original file line numberDiff line numberDiff line change
@@ -11992,6 +11992,7 @@ impl<'a> Parser<'a> {
1199211992
| TableFactor::Function { alias, .. }
1199311993
| TableFactor::UNNEST { alias, .. }
1199411994
| TableFactor::JsonTable { alias, .. }
11995+
| TableFactor::XmlTable { alias, .. }
1199511996
| TableFactor::OpenJsonTable { alias, .. }
1199611997
| TableFactor::TableFunction { alias, .. }
1199711998
| TableFactor::Pivot { alias, .. }
@@ -12107,6 +12108,9 @@ impl<'a> Parser<'a> {
1210712108
} else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[Token::LParen]) {
1210812109
self.prev_token();
1210912110
self.parse_open_json_table_factor()
12111+
} else if self.parse_keyword_with_tokens(Keyword::XMLTABLE, &[Token::LParen]) {
12112+
self.prev_token();
12113+
self.parse_xml_table_factor()
1211012114
} else {
1211112115
let name = self.parse_object_name(true)?;
1211212116

@@ -12339,6 +12343,77 @@ impl<'a> Parser<'a> {
1233912343
})
1234012344
}
1234112345

12346+
fn parse_xml_table_factor(&mut self) -> Result<TableFactor, ParserError> {
12347+
self.expect_token(&Token::LParen)?;
12348+
let row_expression = self.parse_expr()?;
12349+
let passing = self.parse_xml_passing_clause()?;
12350+
self.expect_keyword_is(Keyword::COLUMNS)?;
12351+
let columns = self.parse_comma_separated(Parser::parse_xml_table_column)?;
12352+
self.expect_token(&Token::RParen)?;
12353+
let alias = self.maybe_parse_table_alias()?;
12354+
Ok(TableFactor::XmlTable {
12355+
row_expression,
12356+
passing,
12357+
columns,
12358+
alias,
12359+
})
12360+
}
12361+
12362+
fn parse_xml_table_column(&mut self) -> Result<XmlTableColumn, ParserError> {
12363+
let name = self.parse_identifier()?;
12364+
12365+
let option = if self.parse_keyword(Keyword::FOR) {
12366+
self.expect_keyword(Keyword::ORDINALITY)?;
12367+
XmlTableColumnOption::ForOrdinality
12368+
} else {
12369+
let r#type = self.parse_data_type()?;
12370+
let mut path = None;
12371+
let mut default = None;
12372+
12373+
if self.parse_keyword(Keyword::PATH) {
12374+
path = Some(self.parse_expr()?);
12375+
}
12376+
12377+
if self.parse_keyword(Keyword::DEFAULT) {
12378+
default = Some(self.parse_expr()?);
12379+
}
12380+
12381+
// TODO: Parse NOT NULL/NULL constraints
12382+
12383+
XmlTableColumnOption::NamedInfo {
12384+
r#type,
12385+
path,
12386+
default,
12387+
}
12388+
};
12389+
Ok(XmlTableColumn { name, option })
12390+
}
12391+
12392+
fn parse_xml_passing_clause(&mut self) -> Result<XmlPassingClause, ParserError> {
12393+
let mut arguments = vec![];
12394+
if self.parse_keyword(Keyword::PASSING) {
12395+
loop {
12396+
let by_value =
12397+
self.parse_keyword(Keyword::BY) && self.expect_keyword(Keyword::VALUE).is_ok();
12398+
let expr = self.parse_expr()?;
12399+
let alias = if self.parse_keyword(Keyword::AS) {
12400+
Some(self.parse_identifier()?)
12401+
} else {
12402+
None
12403+
};
12404+
arguments.push(XmlPassingArgument {
12405+
expr,
12406+
alias,
12407+
by_value,
12408+
});
12409+
if !self.consume_token(&Token::Comma) {
12410+
break;
12411+
}
12412+
}
12413+
}
12414+
Ok(XmlPassingClause { arguments })
12415+
}
12416+
1234212417
fn parse_match_recognize(&mut self, table: TableFactor) -> Result<TableFactor, ParserError> {
1234312418
self.expect_token(&Token::LParen)?;
1234412419

Diff for: tests/sqlparser_common.rs

+28
Original file line numberDiff line numberDiff line change
@@ -11729,6 +11729,34 @@ fn test_group_by_grouping_sets() {
1172911729
);
1173011730
}
1173111731

11732+
#[test]
11733+
fn test_xmltable() {
11734+
all_dialects()
11735+
.verified_only_select("SELECT * FROM XMLTABLE('/root' PASSING data COLUMNS element TEXT)");
11736+
11737+
// Minimal meaningful working example: returns a single row with a single column named y containing the value z
11738+
all_dialects().verified_only_select(
11739+
"SELECT y FROM XMLTABLE('/X' PASSING '<X><y>z</y></X>' COLUMNS y TEXT)",
11740+
);
11741+
11742+
// Test using subqueries
11743+
all_dialects().verified_only_select("SELECT y FROM XMLTABLE((SELECT '/X') PASSING (SELECT CAST('<X><y>z</y></X>' AS xml)) COLUMNS y TEXT PATH (SELECT 'y'))");
11744+
11745+
all_dialects().verified_only_select("SELECT * FROM XMLTABLE('/root/row' PASSING xmldata COLUMNS id INT PATH '@id', name TEXT PATH 'name/text()', value FLOAT PATH 'value')");
11746+
11747+
all_dialects().verified_only_select("SELECT * FROM XMLTABLE('//ROWS/ROW' PASSING data COLUMNS row_num FOR ORDINALITY, id INT PATH '@id', name TEXT PATH 'NAME' DEFAULT 'unnamed')");
11748+
11749+
// Example from https://www.postgresql.org/docs/15/functions-xml.html#FUNCTIONS-XML-PROCESSING
11750+
all_dialects().verified_only_select(
11751+
"SELECT xmltable.* FROM xmldata, XMLTABLE('//ROWS/ROW' PASSING data COLUMNS id INT PATH '@id', ordinality FOR ORDINALITY, \"COUNTRY_NAME\" TEXT, country_id TEXT PATH 'COUNTRY_ID', size_sq_km FLOAT PATH 'SIZE[@unit = \"sq_km\"]', size_other TEXT PATH 'concat(SIZE[@unit!=\"sq_km\"], \" \", SIZE[@unit!=\"sq_km\"]/@unit)', premier_name TEXT PATH 'PREMIER_NAME' DEFAULT 'not specified')"
11752+
);
11753+
11754+
// Example from DB2 docs without explicit PASSING clause: https://www.ibm.com/docs/en/db2/12.1.0?topic=xquery-simple-column-name-passing-xmlexists-xmlquery-xmltable
11755+
all_dialects().verified_only_select(
11756+
"SELECT X.* FROM T1, XMLTABLE('$CUSTLIST/customers/customerinfo' COLUMNS \"Cid\" BIGINT PATH '@Cid', \"Info\" XML PATH 'document{.}', \"History\" XML PATH 'NULL') AS X"
11757+
);
11758+
}
11759+
1173211760
#[test]
1173311761
fn test_match_recognize() {
1173411762
use MatchRecognizePattern::*;

0 commit comments

Comments
 (0)