Skip to content

Commit d51ee1c

Browse files
authored
feat: add support for the XMLTABLE table function (#2458)
* feat: add support for the XMLTABLE table function XMLTABLE (SQL/XML standard, supported by PostgreSQL and Oracle) turns an XML document into a relational row set. It is the XML counterpart of JSON_TABLE and shares its PASSING/COLUMNS structure, so it mirrors the existing JsonTableFunction without the JSON-specific clauses. Adds an XmlTableFunction AST node plus a grammar branch in the TableFunction dispatch covering: - a row XPath expression - an optional PASSING clause (value [AS name], ...) - an optional COLUMNS clause with FOR ORDINALITY, data type, PATH and DEFAULT per column Round-trips through parse/deparse and is wired into the expression visitor, deparser, table-names finder and validator. Fixes #2326 Signed-off-by: 付典 <fudianchn@gmail.com> * Drop redundant no-arg constructors flagged by Codacy Each inner class declared an empty public no-arg constructor as its only constructor, which is redundant — the compiler generates an equivalent default constructor (Codacy PMD UnnecessaryConstructor). Removed both; the grammar and tests that instantiate them with `new ...Clause()` are unaffected. Verified locally: full build + XmlTableTest pass on JDK 17; javap confirms the default constructor is still synthesized. Refs #2326 Signed-off-by: 付典 <fudianchn@gmail.com> --------- Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 5bec712 commit d51ee1c

8 files changed

Lines changed: 449 additions & 3 deletions

File tree

src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,14 @@ default void visit(JsonTableFunction jsonTableFunction) {
659659
this.visit(jsonTableFunction, null);
660660
}
661661

662+
default <S> T visit(XmlTableFunction xmlTableFunction, S context) {
663+
return visit((Function) xmlTableFunction, context);
664+
}
665+
666+
default void visit(XmlTableFunction xmlTableFunction) {
667+
this.visit(xmlTableFunction, null);
668+
}
669+
662670
<S> T visit(ConnectByRootOperator connectByRootOperator, S context);
663671

664672
default void visit(ConnectByRootOperator connectByRootOperator) {

src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,11 @@ public <S> T visit(JsonTableFunction jsonTableFunction, S context) {
758758
return visitExpressions(jsonTableFunction, context, jsonTableFunction.getAllExpressions());
759759
}
760760

761+
@Override
762+
public <S> T visit(XmlTableFunction xmlTableFunction, S context) {
763+
return visitExpressions(xmlTableFunction, context, xmlTableFunction.getAllExpressions());
764+
}
765+
761766
@Override
762767
public <S> T visit(ConnectByRootOperator connectByRootOperator, S context) {
763768
return connectByRootOperator.getColumn().accept(this, context);
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.expression;
11+
12+
import java.io.Serializable;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
16+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
17+
import net.sf.jsqlparser.statement.create.table.ColDataType;
18+
19+
/**
20+
* Represents an {@code XMLTABLE} table function (SQL/XML standard, supported by PostgreSQL and
21+
* Oracle), which turns an XML document into a relational row set, e.g.:
22+
*
23+
* <pre>
24+
* XMLTABLE('//ROWS/ROW'
25+
* PASSING data
26+
* COLUMNS id int PATH '@id',
27+
* ordinality FOR ORDINALITY,
28+
* country_id text PATH 'COUNTRY_ID',
29+
* size float PATH 'SIZE' DEFAULT 0)
30+
* </pre>
31+
*
32+
* It is the XML counterpart of {@link JsonTableFunction} and mirrors its structure, without the
33+
* JSON-specific wrapper/error/plan clauses.
34+
*/
35+
public class XmlTableFunction extends Function {
36+
37+
private Expression rowPathExpression;
38+
private final List<XmlTablePassingClause> passingClauses = new ArrayList<>();
39+
private final List<XmlTableColumnDefinition> columnDefinitions = new ArrayList<>();
40+
41+
public static class XmlTablePassingClause extends ASTNodeAccessImpl implements Serializable {
42+
private Expression valueExpression;
43+
private String name;
44+
45+
public Expression getValueExpression() {
46+
return valueExpression;
47+
}
48+
49+
public XmlTablePassingClause setValueExpression(Expression valueExpression) {
50+
this.valueExpression = valueExpression;
51+
return this;
52+
}
53+
54+
public String getName() {
55+
return name;
56+
}
57+
58+
public XmlTablePassingClause setName(String name) {
59+
this.name = name;
60+
return this;
61+
}
62+
63+
public void collectExpressions(List<Expression> expressions) {
64+
if (valueExpression != null) {
65+
expressions.add(valueExpression);
66+
}
67+
}
68+
69+
@Override
70+
public String toString() {
71+
return valueExpression + (name != null ? " AS " + name : "");
72+
}
73+
}
74+
75+
public static class XmlTableColumnDefinition extends ASTNodeAccessImpl implements Serializable {
76+
private String columnName;
77+
private boolean forOrdinality;
78+
private ColDataType dataType;
79+
private Expression pathExpression;
80+
private Expression defaultExpression;
81+
82+
public String getColumnName() {
83+
return columnName;
84+
}
85+
86+
public XmlTableColumnDefinition setColumnName(String columnName) {
87+
this.columnName = columnName;
88+
return this;
89+
}
90+
91+
public boolean isForOrdinality() {
92+
return forOrdinality;
93+
}
94+
95+
public XmlTableColumnDefinition setForOrdinality(boolean forOrdinality) {
96+
this.forOrdinality = forOrdinality;
97+
return this;
98+
}
99+
100+
public ColDataType getDataType() {
101+
return dataType;
102+
}
103+
104+
public XmlTableColumnDefinition setDataType(ColDataType dataType) {
105+
this.dataType = dataType;
106+
return this;
107+
}
108+
109+
public Expression getPathExpression() {
110+
return pathExpression;
111+
}
112+
113+
public XmlTableColumnDefinition setPathExpression(Expression pathExpression) {
114+
this.pathExpression = pathExpression;
115+
return this;
116+
}
117+
118+
public Expression getDefaultExpression() {
119+
return defaultExpression;
120+
}
121+
122+
public XmlTableColumnDefinition setDefaultExpression(Expression defaultExpression) {
123+
this.defaultExpression = defaultExpression;
124+
return this;
125+
}
126+
127+
public void collectExpressions(List<Expression> expressions) {
128+
if (pathExpression != null) {
129+
expressions.add(pathExpression);
130+
}
131+
if (defaultExpression != null) {
132+
expressions.add(defaultExpression);
133+
}
134+
}
135+
136+
@Override
137+
public String toString() {
138+
StringBuilder builder = new StringBuilder(columnName);
139+
if (forOrdinality) {
140+
builder.append(" FOR ORDINALITY");
141+
return builder.toString();
142+
}
143+
if (dataType != null) {
144+
builder.append(" ").append(dataType);
145+
}
146+
if (pathExpression != null) {
147+
builder.append(" PATH ").append(pathExpression);
148+
}
149+
if (defaultExpression != null) {
150+
builder.append(" DEFAULT ").append(defaultExpression);
151+
}
152+
return builder.toString();
153+
}
154+
}
155+
156+
public Expression getRowPathExpression() {
157+
return rowPathExpression;
158+
}
159+
160+
public XmlTableFunction setRowPathExpression(Expression rowPathExpression) {
161+
this.rowPathExpression = rowPathExpression;
162+
return this;
163+
}
164+
165+
public List<XmlTablePassingClause> getPassingClauses() {
166+
return passingClauses;
167+
}
168+
169+
public XmlTableFunction addPassingClause(XmlTablePassingClause passingClause) {
170+
passingClauses.add(passingClause);
171+
return this;
172+
}
173+
174+
public List<XmlTableColumnDefinition> getColumnDefinitions() {
175+
return columnDefinitions;
176+
}
177+
178+
public XmlTableFunction addColumnDefinition(XmlTableColumnDefinition columnDefinition) {
179+
columnDefinitions.add(columnDefinition);
180+
return this;
181+
}
182+
183+
public List<Expression> getAllExpressions() {
184+
List<Expression> expressions = new ArrayList<>();
185+
if (rowPathExpression != null) {
186+
expressions.add(rowPathExpression);
187+
}
188+
for (XmlTablePassingClause passingClause : passingClauses) {
189+
passingClause.collectExpressions(expressions);
190+
}
191+
for (XmlTableColumnDefinition columnDefinition : columnDefinitions) {
192+
columnDefinition.collectExpressions(expressions);
193+
}
194+
return expressions;
195+
}
196+
197+
@Override
198+
public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
199+
return expressionVisitor.visit(this, context);
200+
}
201+
202+
@Override
203+
public String toString() {
204+
StringBuilder builder = new StringBuilder("XMLTABLE(");
205+
builder.append(rowPathExpression);
206+
if (!passingClauses.isEmpty()) {
207+
builder.append(" PASSING ");
208+
boolean first = true;
209+
for (XmlTablePassingClause passingClause : passingClauses) {
210+
if (!first) {
211+
builder.append(", ");
212+
}
213+
builder.append(passingClause);
214+
first = false;
215+
}
216+
}
217+
if (!columnDefinitions.isEmpty()) {
218+
builder.append(" COLUMNS ");
219+
boolean first = true;
220+
for (XmlTableColumnDefinition columnDefinition : columnDefinitions) {
221+
if (!first) {
222+
builder.append(", ");
223+
}
224+
builder.append(columnDefinition);
225+
first = false;
226+
}
227+
}
228+
builder.append(")");
229+
return builder.toString();
230+
}
231+
}

src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -768,17 +768,20 @@ public <S> Void visit(AnalyticExpression analytic, S context) {
768768
if (analytic.getWindowElement() != null) {
769769
if (analytic.getWindowElement().getRange() != null) {
770770
if (analytic.getWindowElement().getRange().getStart() != null
771-
&& analytic.getWindowElement().getRange().getStart().getExpression() != null) {
771+
&& analytic.getWindowElement().getRange().getStart()
772+
.getExpression() != null) {
772773
analytic.getWindowElement().getRange().getStart().getExpression().accept(this,
773774
context);
774775
}
775776
if (analytic.getWindowElement().getRange().getEnd() != null
776-
&& analytic.getWindowElement().getRange().getEnd().getExpression() != null) {
777+
&& analytic.getWindowElement().getRange().getEnd()
778+
.getExpression() != null) {
777779
analytic.getWindowElement().getRange().getEnd().getExpression().accept(this,
778780
context);
779781
}
780782
}
781-
if (analytic.getWindowElement().getOffset() != null && analytic.getWindowElement().getOffset().getExpression() != null) {
783+
if (analytic.getWindowElement().getOffset() != null
784+
&& analytic.getWindowElement().getOffset().getExpression() != null) {
782785
analytic.getWindowElement().getOffset().getExpression().accept(this, context);
783786
}
784787

@@ -1792,6 +1795,16 @@ public <S> Void visit(JsonTableFunction expression, S context) {
17921795
return null;
17931796
}
17941797

1798+
@Override
1799+
public <S> Void visit(XmlTableFunction expression, S context) {
1800+
for (Expression xmlExpression : expression.getAllExpressions()) {
1801+
if (xmlExpression != null) {
1802+
xmlExpression.accept(this, context);
1803+
}
1804+
}
1805+
return null;
1806+
}
1807+
17951808
@Override
17961809
public <S> Void visit(ConnectByRootOperator connectByRootOperator, S context) {
17971810
connectByRootOperator.getColumn().accept(this, context);

src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import net.sf.jsqlparser.expression.JsonExpression;
4646
import net.sf.jsqlparser.expression.JsonFunction;
4747
import net.sf.jsqlparser.expression.JsonTableFunction;
48+
import net.sf.jsqlparser.expression.XmlTableFunction;
4849
import net.sf.jsqlparser.expression.KeepExpression;
4950
import net.sf.jsqlparser.expression.KeyExpression;
5051
import net.sf.jsqlparser.expression.LambdaExpression;
@@ -1659,6 +1660,12 @@ public <S> StringBuilder visit(JsonTableFunction expression, S context) {
16591660
return builder;
16601661
}
16611662

1663+
@Override
1664+
public <S> StringBuilder visit(XmlTableFunction expression, S context) {
1665+
builder.append(expression);
1666+
return builder;
1667+
}
1668+
16621669
@Override
16631670
public <S> StringBuilder visit(ConnectByRootOperator connectByRootOperator, S context) {
16641671
builder.append("CONNECT_BY_ROOT ");

src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import net.sf.jsqlparser.expression.JsonExpression;
4040
import net.sf.jsqlparser.expression.JsonFunction;
4141
import net.sf.jsqlparser.expression.JsonTableFunction;
42+
import net.sf.jsqlparser.expression.XmlTableFunction;
4243
import net.sf.jsqlparser.expression.KeepExpression;
4344
import net.sf.jsqlparser.expression.KeyExpression;
4445
import net.sf.jsqlparser.expression.LambdaExpression;
@@ -1051,6 +1052,14 @@ public <S> Void visit(JsonTableFunction expression, S context) {
10511052
return null;
10521053
}
10531054

1055+
@Override
1056+
public <S> Void visit(XmlTableFunction expression, S context) {
1057+
for (Expression xmlExpression : expression.getAllExpressions()) {
1058+
validateOptionalExpression(xmlExpression, this);
1059+
}
1060+
return null;
1061+
}
1062+
10541063
@Override
10551064
public <S> Void visit(ConnectByRootOperator connectByRootOperator, S context) {
10561065
connectByRootOperator.getColumn().accept(this, context);

0 commit comments

Comments
 (0)