Skip to content

Add support for XMLTABLE #1817

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

Merged
merged 3 commits into from
Apr 23, 2025
Merged
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
3 changes: 2 additions & 1 deletion src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ pub use self::query::{
TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier,
TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion,
TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values,
WildcardAdditionalOptions, With, WithFill,
WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, XmlPassingArgument,
XmlPassingClause, XmlTableColumn, XmlTableColumnOption,
};

pub use self::trigger::{
Expand Down
186 changes: 186 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,37 @@ pub enum TableFactor {
symbols: Vec<SymbolDefinition>,
alias: Option<TableAlias>,
},
/// The `XMLTABLE` table-valued function.
/// Part of the SQL standard, supported by PostgreSQL, Oracle, and DB2.
///
/// <https://www.postgresql.org/docs/15/functions-xml.html#FUNCTIONS-XML-PROCESSING>
///
/// ```sql
/// 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'
/// );
/// ````
XmlTable {
/// Optional XMLNAMESPACES clause (empty if not present)
namespaces: Vec<XmlNamespaceDefinition>,
/// The row-generating XPath expression.
row_expression: Expr,
/// The PASSING clause specifying the document expression.
passing: XmlPassingClause,
/// The columns to be extracted from each generated row.
columns: Vec<XmlTableColumn>,
/// The alias for the table.
alias: Option<TableAlias>,
},
}

/// The table sample modifier options
Expand Down Expand Up @@ -1936,6 +1967,31 @@ impl fmt::Display for TableFactor {
}
Ok(())
}
TableFactor::XmlTable {
row_expression,
passing,
columns,
alias,
namespaces,
} => {
write!(f, "XMLTABLE(")?;
if !namespaces.is_empty() {
write!(
f,
"XMLNAMESPACES({}), ",
display_comma_separated(namespaces)
)?;
}
write!(
f,
"{row_expression}{passing} COLUMNS {columns})",
columns = display_comma_separated(columns)
)?;
if let Some(alias) = alias {
write!(f, " AS {alias}")?;
}
Ok(())
}
}
}
}
Expand Down Expand Up @@ -3082,3 +3138,133 @@ pub enum UpdateTableFromKind {
/// For Example: `UPDATE SET t1.name='aaa' FROM t1`
AfterSet(Vec<TableWithJoins>),
}

/// Defines the options for an XmlTable column: Named or ForOrdinality
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum XmlTableColumnOption {
/// A named column with a type, optional path, and default value.
NamedInfo {
/// The type of the column to be extracted.
r#type: DataType,
/// The path to the column to be extracted. If None, defaults to the column name.
path: Option<Expr>,
/// Default value if path does not match
default: Option<Expr>,
/// Whether the column is nullable (NULL=true, NOT NULL=false)
nullable: bool,
},
/// The FOR ORDINALITY marker
ForOrdinality,
}

/// A single column definition in XMLTABLE
///
/// ```sql
/// 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'
/// ```
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct XmlTableColumn {
/// The name of the column.
pub name: Ident,
/// Column options: type/path/default or FOR ORDINALITY
pub option: XmlTableColumnOption,
}

impl fmt::Display for XmlTableColumn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)?;
match &self.option {
XmlTableColumnOption::NamedInfo {
r#type,
path,
default,
nullable,
} => {
write!(f, " {}", r#type)?;
if let Some(p) = path {
write!(f, " PATH {}", p)?;
}
if let Some(d) = default {
write!(f, " DEFAULT {}", d)?;
}
if !*nullable {
write!(f, " NOT NULL")?;
}
Ok(())
}
XmlTableColumnOption::ForOrdinality => {
write!(f, " FOR ORDINALITY")
}
}
}
}

/// Argument passed in the XMLTABLE PASSING clause
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct XmlPassingArgument {
pub expr: Expr,
pub alias: Option<Ident>,
pub by_value: bool, // True if BY VALUE is specified
}

impl fmt::Display for XmlPassingArgument {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.by_value {
write!(f, "BY VALUE ")?;
}
write!(f, "{}", self.expr)?;
if let Some(alias) = &self.alias {
write!(f, " AS {}", alias)?;
}
Ok(())
}
}

/// The PASSING clause for XMLTABLE
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct XmlPassingClause {
pub arguments: Vec<XmlPassingArgument>,
}

impl fmt::Display for XmlPassingClause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.arguments.is_empty() {
write!(f, " PASSING {}", display_comma_separated(&self.arguments))?;
}
Ok(())
}
}

/// Represents a single XML namespace definition in the XMLNAMESPACES clause.
///
/// `namespace_uri AS namespace_name`
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct XmlNamespaceDefinition {
/// The namespace URI (a text expression).
pub uri: Expr,
/// The alias for the namespace (a simple identifier).
pub name: Ident,
}

impl fmt::Display for XmlNamespaceDefinition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} AS {}", self.uri, self.name)
}
}
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1909,6 +1909,7 @@ impl Spanned for TableFactor {
.chain(alias.as_ref().map(|alias| alias.span())),
),
TableFactor::JsonTable { .. } => Span::empty(),
TableFactor::XmlTable { .. } => Span::empty(),
TableFactor::Pivot {
table,
aggregate_functions,
Expand Down
3 changes: 3 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,7 @@ define_keywords!(
PARTITION,
PARTITIONED,
PARTITIONS,
PASSING,
PASSWORD,
PAST,
PATH,
Expand Down Expand Up @@ -989,6 +990,8 @@ define_keywords!(
WORK,
WRITE,
XML,
XMLNAMESPACES,
XMLTABLE,
XOR,
YEAR,
YEARS,
Expand Down
97 changes: 97 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11992,6 +11992,7 @@ impl<'a> Parser<'a> {
| TableFactor::Function { alias, .. }
| TableFactor::UNNEST { alias, .. }
| TableFactor::JsonTable { alias, .. }
| TableFactor::XmlTable { alias, .. }
| TableFactor::OpenJsonTable { alias, .. }
| TableFactor::TableFunction { alias, .. }
| TableFactor::Pivot { alias, .. }
Expand Down Expand Up @@ -12107,6 +12108,9 @@ impl<'a> Parser<'a> {
} else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[Token::LParen]) {
self.prev_token();
self.parse_open_json_table_factor()
} else if self.parse_keyword_with_tokens(Keyword::XMLTABLE, &[Token::LParen]) {
self.prev_token();
self.parse_xml_table_factor()
} else {
let name = self.parse_object_name(true)?;

Expand Down Expand Up @@ -12339,6 +12343,99 @@ impl<'a> Parser<'a> {
})
}

fn parse_xml_table_factor(&mut self) -> Result<TableFactor, ParserError> {
self.expect_token(&Token::LParen)?;
let namespaces = if self.parse_keyword(Keyword::XMLNAMESPACES) {
self.expect_token(&Token::LParen)?;
let namespaces = self.parse_comma_separated(Parser::parse_xml_namespace_definition)?;
self.expect_token(&Token::RParen)?;
self.expect_token(&Token::Comma)?;
namespaces
} else {
vec![]
};
let row_expression = self.parse_expr()?;
let passing = self.parse_xml_passing_clause()?;
self.expect_keyword_is(Keyword::COLUMNS)?;
let columns = self.parse_comma_separated(Parser::parse_xml_table_column)?;
self.expect_token(&Token::RParen)?;
let alias = self.maybe_parse_table_alias()?;
Ok(TableFactor::XmlTable {
namespaces,
row_expression,
passing,
columns,
alias,
})
}

fn parse_xml_namespace_definition(&mut self) -> Result<XmlNamespaceDefinition, ParserError> {
let uri = self.parse_expr()?;
self.expect_keyword_is(Keyword::AS)?;
let name = self.parse_identifier()?;
Ok(XmlNamespaceDefinition { uri, name })
}

fn parse_xml_table_column(&mut self) -> Result<XmlTableColumn, ParserError> {
let name = self.parse_identifier()?;

let option = if self.parse_keyword(Keyword::FOR) {
self.expect_keyword(Keyword::ORDINALITY)?;
XmlTableColumnOption::ForOrdinality
} else {
let r#type = self.parse_data_type()?;
let mut path = None;
let mut default = None;

if self.parse_keyword(Keyword::PATH) {
path = Some(self.parse_expr()?);
}

if self.parse_keyword(Keyword::DEFAULT) {
default = Some(self.parse_expr()?);
}

let not_null = self.parse_keywords(&[Keyword::NOT, Keyword::NULL]);
if !not_null {
// NULL is the default but can be specified explicitly
let _ = self.parse_keyword(Keyword::NULL);
}

XmlTableColumnOption::NamedInfo {
r#type,
path,
default,
nullable: !not_null,
}
};
Ok(XmlTableColumn { name, option })
}

fn parse_xml_passing_clause(&mut self) -> Result<XmlPassingClause, ParserError> {
let mut arguments = vec![];
if self.parse_keyword(Keyword::PASSING) {
loop {
let by_value =
self.parse_keyword(Keyword::BY) && self.expect_keyword(Keyword::VALUE).is_ok();
let expr = self.parse_expr()?;
let alias = if self.parse_keyword(Keyword::AS) {
Some(self.parse_identifier()?)
} else {
None
};
arguments.push(XmlPassingArgument {
expr,
alias,
by_value,
});
if !self.consume_token(&Token::Comma) {
break;
}
}
}
Ok(XmlPassingClause { arguments })
}

fn parse_match_recognize(&mut self, table: TableFactor) -> Result<TableFactor, ParserError> {
self.expect_token(&Token::LParen)?;

Expand Down
Loading