Skip to content

Commit 73bd3c4

Browse files
authored
Support structured XMLFOREST arguments with aliases (#2619)
1 parent 537f3a6 commit 73bd3c4

6 files changed

Lines changed: 212 additions & 1 deletion

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.util.function.Consumer;
13+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
14+
15+
/** An expression with an alias, such as an XMLFOREST argument. */
16+
public class AliasedExpression extends ASTNodeAccessImpl implements Expression {
17+
private Expression expression;
18+
private Alias alias;
19+
20+
public AliasedExpression(Expression expression, Alias alias) {
21+
this.expression = expression;
22+
this.alias = alias;
23+
}
24+
25+
public Expression getExpression() {
26+
return expression;
27+
}
28+
29+
public void setExpression(Expression expression) {
30+
this.expression = expression;
31+
}
32+
33+
public Alias getAlias() {
34+
return alias;
35+
}
36+
37+
public void setAlias(Alias alias) {
38+
this.alias = alias;
39+
}
40+
41+
@Override
42+
public <T, S> T accept(ExpressionVisitor<T> visitor, S context) {
43+
return visitor.visit(this, context);
44+
}
45+
46+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
47+
expressionPrinter.accept(expression);
48+
if (alias != null) {
49+
builder.append(alias);
50+
}
51+
return builder;
52+
}
53+
54+
@Override
55+
public String toString() {
56+
StringBuilder builder = new StringBuilder();
57+
return appendTo(builder, builder::append).toString();
58+
}
59+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@
7272
import net.sf.jsqlparser.statement.update.UpdateSet;
7373

7474
public interface ExpressionVisitor<T> {
75+
/** Visits the value of an aliased expression by default, preserving existing visitors. */
76+
default <S> T visit(AliasedExpression expression, S context) {
77+
return expression.getExpression().accept(this, context);
78+
}
79+
80+
default void visit(AliasedExpression expression) {
81+
visit(expression, null);
82+
}
83+
7584

7685
default <S> T visit(ExecuteArgument argument, S context) {
7786
return argument.getExpression().accept(this, context);

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
*/
1010
package net.sf.jsqlparser.util;
1111

12+
import net.sf.jsqlparser.expression.AliasedExpression;
13+
1214
import net.sf.jsqlparser.statement.oracle.OracleBlock;
1315
import net.sf.jsqlparser.statement.oracle.OracleAssignment;
1416
import net.sf.jsqlparser.statement.oracle.OracleNullStatement;
@@ -2435,6 +2437,11 @@ public void visit(IfElseStatement ifElseStatement) {
24352437
StatementVisitor.super.visit(ifElseStatement);
24362438
}
24372439

2440+
@Override
2441+
public <S> Void visit(AliasedExpression expression, S context) {
2442+
return expression.getExpression().accept(this, context);
2443+
}
2444+
24382445
@Override
24392446
public <S> Void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter, S context) {
24402447
oracleNamedFunctionParameter.getExpression().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
@@ -9,6 +9,8 @@
99
*/
1010
package net.sf.jsqlparser.util.deparser;
1111

12+
import net.sf.jsqlparser.expression.AliasedExpression;
13+
1214
import static java.util.stream.Collectors.joining;
1315

1416
import java.util.Iterator;
@@ -169,6 +171,11 @@ public ExpressionDeParser(SelectVisitor<StringBuilder> selectVisitor, StringBuil
169171
this.orderByDeParser = orderByDeParser;
170172
}
171173

174+
@Override
175+
public <S> StringBuilder visit(AliasedExpression expression, S context) {
176+
return expression.appendTo(builder, value -> value.accept(this, context));
177+
}
178+
172179
@Override
173180
public <S> StringBuilder visit(Addition addition, S context) {
174181
deparse(addition, " + ", null);

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
794794
}
795795
// only a bare leading keyword: an expression must follow
796796
Token next = getToken(2);
797-
if (next.kind == EOF || ")".equals(next.image) || ",".equals(next.image)
797+
if (next.kind == EOF || next.kind == K_AS || ")".equals(next.image) || ",".equals(next.image)
798798
|| ".".equals(next.image) || "(".equals(next.image)) {
799799
return;
800800
}
@@ -11575,6 +11575,7 @@ ExpressionList FunctionArgumentList(Function retval):
1157511575
Token extraKeywordToken;
1157611576
}
1157711577
{
11578+
{ if ("XMLFOREST".equalsIgnoreCase(retval.getName())) { return XmlForestArguments(); } }
1157811579
[ LOOKAHEAD(2) extraKeywordToken = <K_TABLE> { retval.setExtraKeyword(extraKeywordToken.image); } ]
1157911580
expressionList=ExpressionList()
1158011581
[ orderByList = OrderByElements() { retval.setOrderByElements(orderByList); } ]
@@ -11593,6 +11594,29 @@ ExpressionList FunctionArgumentList(Function retval):
1159311594
}
1159411595
}
1159511596

11597+
/** XMLFOREST keeps expressions in the ordinary function parameter list. */
11598+
ExpressionList<Expression> XmlForestArguments():
11599+
{
11600+
ExpressionList<Expression> arguments = new ExpressionList<Expression>();
11601+
Expression expression;
11602+
}
11603+
{
11604+
expression=XmlForestArgument() { arguments.add(expression); }
11605+
( "," expression=XmlForestArgument() { arguments.add(expression); } )*
11606+
{ return arguments; }
11607+
}
11608+
11609+
Expression XmlForestArgument():
11610+
{
11611+
Expression expression;
11612+
String name;
11613+
}
11614+
{
11615+
expression=Expression()
11616+
[ <K_AS> name=RelObjectName() { expression = new AliasedExpression(expression, new Alias(name, true)); } ]
11617+
{ return expression; }
11618+
}
11619+
1159611620
XMLSerializeExpr XMLSerializeExpr(): {
1159711621
XMLSerializeExpr result = new XMLSerializeExpr();
1159811622
Expression expression;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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.statement.select;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertThrows;
14+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
15+
import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNotAllowed;
16+
import java.util.ArrayList;
17+
import java.util.Arrays;
18+
import java.util.List;
19+
import java.util.Set;
20+
import net.sf.jsqlparser.JSQLParserException;
21+
import net.sf.jsqlparser.expression.Alias;
22+
import net.sf.jsqlparser.expression.AliasedExpression;
23+
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
24+
import net.sf.jsqlparser.expression.Function;
25+
import net.sf.jsqlparser.expression.LongValue;
26+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
27+
import net.sf.jsqlparser.parser.feature.Feature;
28+
import net.sf.jsqlparser.schema.Column;
29+
import net.sf.jsqlparser.util.TablesNamesFinder;
30+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
31+
import net.sf.jsqlparser.util.deparser.SelectDeParser;
32+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
33+
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
34+
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.params.ParameterizedTest;
36+
import org.junit.jupiter.params.provider.ValueSource;
37+
38+
class XmlForestTest {
39+
@ParameterizedTest
40+
@ValueSource(strings = {"XMLFOREST(name AS \"Name\")", "xmlforest(a, b AS item)",
41+
"XMLFOREST(1 + 2 AS total, COALESCE(a, 'default') AS \"Value\")",
42+
"XMLFOREST(XMLFOREST(a AS inner_name) AS outer_name)",
43+
"XMLFOREST(name a)", "XMLFOREST(name)", "XMLPARSE(CONTENT a)"})
44+
void parsesAndRoundTripsNamedAndUnnamedArguments(String expression) throws Exception {
45+
var statement = assertSqlCanBeParsedAndDeparsed("SELECT " + expression + " FROM t");
46+
assertEquals(statement.toString(), CCJSqlParserUtil.parse(statement.toString()).toString());
47+
}
48+
49+
@Test
50+
void aliasIsStructuredAndCanBeEditedWithoutReparsingSql() throws Exception {
51+
var statement =
52+
(PlainSelect) CCJSqlParserUtil.parse("SELECT XMLFOREST(name AS \"Name\") FROM t");
53+
Function function = (Function) statement.getSelectItem(0).getExpression();
54+
AliasedExpression argument = (AliasedExpression) function.getParameters().get(0);
55+
assertEquals("name", ((Column) argument.getExpression()).getColumnName());
56+
assertEquals("\"Name\"", argument.getAlias().getName());
57+
argument.setExpression(new LongValue(7));
58+
argument.setAlias(new Alias("value", true));
59+
assertEquals("SELECT XMLFOREST(7 AS value) FROM t", statement.toString());
60+
StringBuilder output = new StringBuilder();
61+
statement.accept(new StatementDeParser(output), null);
62+
assertEquals(statement.toString(), output.toString());
63+
}
64+
65+
@Test
66+
void customVisitorReachesValuesWithContextAndDeparserKeepsAliases() throws Exception {
67+
var statement = (PlainSelect) CCJSqlParserUtil
68+
.parse("SELECT XMLFOREST(1 + 2 AS total, 3 AS n) FROM t");
69+
List<Long> values = new ArrayList<>();
70+
statement.getSelectItem(0).getExpression().accept(new ExpressionVisitorAdapter<Void>() {
71+
@Override
72+
public <S> Void visit(LongValue value, S context) {
73+
assertEquals("context", context);
74+
values.add(value.getValue());
75+
return null;
76+
}
77+
}, "context");
78+
assertEquals(Arrays.asList(1L, 2L, 3L), values);
79+
StringBuilder output = new StringBuilder();
80+
ExpressionDeParser expressions = new ExpressionDeParser() {
81+
@Override
82+
public <S> StringBuilder visit(LongValue value, S context) {
83+
return getBuilder().append(value.getValue() + 100);
84+
}
85+
};
86+
statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null);
87+
assertEquals("SELECT XMLFOREST(101 + 102 AS total, 103 AS n) FROM t", output.toString());
88+
}
89+
90+
@Test
91+
void traversalAndValidationReachTheUnderlyingArgument() throws Exception {
92+
assertEquals(Set.of("outer_t", "inner_t"), TablesNamesFinder.findTables(
93+
"SELECT XMLFOREST((SELECT a FROM inner_t) AS item) FROM outer_t"));
94+
validateNotAllowed("SELECT a FROM t WHERE XMLFOREST(? AS item) IS NULL", 1, 1,
95+
new FeaturesAllowed().add(FeaturesAllowed.SELECT).remove(Feature.jdbcParameter),
96+
Feature.jdbcParameter);
97+
}
98+
99+
@ParameterizedTest
100+
@ValueSource(strings = {"SELECT XMLFOREST(a AS) FROM t", "SELECT XMLFOREST(a AS b.c) FROM t",
101+
"SELECT f(a AS b) FROM t"})
102+
void rejectsMissingAliasesAndKeepsOrdinaryFunctionArgumentsStrict(String sql) {
103+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
104+
}
105+
}

0 commit comments

Comments
 (0)