Skip to content

Commit 257e1c7

Browse files
committed
Support SQL Server XML methods on subquery results
1 parent eddb1fb commit 257e1c7

7 files changed

Lines changed: 234 additions & 4 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,15 @@ default void visit(RowConstructor<? extends Expression> rowConstructor) {
569569

570570
<S> T visit(RowGetExpression rowGetExpression, S context);
571571

572+
default <S> T visit(MethodCallExpression methodCall, S context) {
573+
methodCall.getExpression().accept(this, context);
574+
return methodCall.getMethod().accept(this, context);
575+
}
576+
577+
default void visit(MethodCallExpression methodCall) {
578+
this.visit(methodCall, null);
579+
}
580+
572581
default void visit(RowGetExpression rowGetExpression) {
573582
this.visit(rowGetExpression, null);
574583
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
/** A method applied to an expression, such as a SQL Server XML subquery result. */
16+
public class MethodCallExpression extends ASTNodeAccessImpl implements Expression {
17+
private Expression expression;
18+
private Function method;
19+
20+
public MethodCallExpression(Expression expression, Function method) {
21+
this.expression = expression;
22+
this.method = method;
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 Function getMethod() {
34+
return method;
35+
}
36+
37+
public void setMethod(Function method) {
38+
this.method = method;
39+
}
40+
41+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> printer) {
42+
printer.accept(expression);
43+
builder.append('.');
44+
printer.accept(method);
45+
return builder;
46+
}
47+
48+
@Override
49+
public <T, S> T accept(ExpressionVisitor<T> visitor, S context) {
50+
return visitor.visit(this, context);
51+
}
52+
53+
@Override
54+
public String toString() {
55+
StringBuilder builder = new StringBuilder();
56+
return appendTo(builder, value -> builder.append(value)).toString();
57+
}
58+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,6 +1773,11 @@ public <S> Void visit(RowGetExpression rowGetExpression, S context) {
17731773
return null;
17741774
}
17751775

1776+
@Override
1777+
public <S> Void visit(MethodCallExpression methodCall, S context) {
1778+
return ExpressionVisitor.super.visit(methodCall, context);
1779+
}
1780+
17761781
@Override
17771782
public <S> Void visit(HexValue hexValue, S context) {
17781783
return null;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import net.sf.jsqlparser.expression.RangeExpression;
6565
import net.sf.jsqlparser.expression.RowConstructor;
6666
import net.sf.jsqlparser.expression.RowGetExpression;
67+
import net.sf.jsqlparser.expression.MethodCallExpression;
6768
import net.sf.jsqlparser.expression.SignedExpression;
6869
import net.sf.jsqlparser.expression.StringValue;
6970
import net.sf.jsqlparser.expression.StructType;
@@ -1471,6 +1472,11 @@ public <S> StringBuilder visit(RowGetExpression rowGetExpression, S context) {
14711472
return null;
14721473
}
14731474

1475+
@Override
1476+
public <S> StringBuilder visit(MethodCallExpression methodCall, S context) {
1477+
return methodCall.appendTo(builder, expression -> expression.accept(this, context));
1478+
}
1479+
14741480
@Override
14751481
public <S> StringBuilder visit(OracleHint hint, S context) {
14761482
builder.append(hint.toString());

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8935,10 +8935,17 @@ Expression NavigationStep(Expression base):
89358935
{
89368936
Expression step = null;
89378937
String nm = null;
8938+
Function method;
89388939
}
89398940
{
89408941
(
8941-
"." nm=RelObjectNameExt() { step = new RowGetExpression(base, nm); }
8942+
"."
8943+
(
8944+
LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) && isFunctionAhead() })
8945+
method=Function() { step = new MethodCallExpression(base, method); }
8946+
|
8947+
nm=RelObjectNameExt() { step = new RowGetExpression(base, nm); }
8948+
)
89428949
|
89438950
step = ArrayExpression(base)
89448951
)
@@ -8956,7 +8963,6 @@ Expression PrimaryExpression() #PrimaryExpression:
89568963
Token token = null;
89578964
Token sign = null;
89588965
Token adjacentToken = null;
8959-
String tmp = "";
89608966
ColDataType type = null;
89618967
boolean not = false;
89628968
boolean exclamationMarkNot = false;
@@ -9109,8 +9115,8 @@ Expression PrimaryExpression() #PrimaryExpression:
91099115
"." "*"
91109116
{ retval = new FunctionAllColumns(unwrapParenthesedFunction(retval)); } ]
91119117

9112-
// RowGet Expressions
9113-
( LOOKAHEAD(2) "." tmp=RelObjectName() { retval = new RowGetExpression(retval, tmp); } )*
9118+
// Share field and method navigation with the other primary expressions.
9119+
( LOOKAHEAD(".") nxt=NavigationStep(retval) { retval = nxt; } )*
91149120
)
91159121
)
91169122

‎src/site/sphinx/usage.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,13 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
716716

717717
Features set explicitly *after* the preset win over it.
718718

719+
``Dialect.SQLSERVER`` supports methods on expression results, including
720+
``(SELECT ... FOR XML PATH(''), TYPE).value('.', 'varchar(max)')``.
721+
``MethodCallExpression`` exposes the receiver expression and a ``Function``
722+
containing the method name and arguments. Field access and method calls share
723+
the navigation grammar; expression visitors and deparsers traverse both the
724+
receiver and method arguments. XQuery strings remain string literals.
725+
719726
With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
720727
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
721728
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
13+
import static org.junit.jupiter.api.Assertions.*;
14+
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.Set;
18+
import net.sf.jsqlparser.JSQLParserException;
19+
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
20+
import net.sf.jsqlparser.expression.Function;
21+
import net.sf.jsqlparser.expression.MethodCallExpression;
22+
import net.sf.jsqlparser.expression.StringValue;
23+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
24+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
25+
import net.sf.jsqlparser.parser.feature.Feature;
26+
import net.sf.jsqlparser.parser.feature.FeatureConfiguration;
27+
import net.sf.jsqlparser.statement.Statement;
28+
import net.sf.jsqlparser.statement.StatementVisitorAdapter;
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.Validation;
34+
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
35+
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.params.ParameterizedTest;
37+
import org.junit.jupiter.params.provider.ValueSource;
38+
39+
class SqlServerXmlMethodTest {
40+
private static final String SIMPLE =
41+
"SELECT (SELECT body FROM docs FOR XML PATH (''), TYPE).value('.', 'varchar(max)')";
42+
43+
private static PlainSelect parse(String sql) throws Exception {
44+
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.SQLSERVER));
45+
}
46+
47+
@Test
48+
void parsesOriginalStuffQueryIssue386() throws Exception {
49+
String sql = "SELECT (STUFF((SELECT '|' + person_name FROM person "
50+
+ "JOIN person_group ON person.person_id = person_group.person_id "
51+
+ "WHERE person_group.group_id = 1 FOR XML PATH(''), TYPE)"
52+
+ ".value('.', 'varchar(max)'), 1, 1, '')) AS person_name";
53+
Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true,
54+
p -> p.withDialect(Dialect.SQLSERVER));
55+
assertEquals(Set.of("person", "person_group"),
56+
new TablesNamesFinder().getTables(statement));
57+
assertEquals(statement.toString(), parse(statement.toString()).toString());
58+
}
59+
60+
@ParameterizedTest
61+
@ValueSource(strings = {"value('.', 'varchar(max)')", "query('/root')", "exist('/root')",
62+
"query('/root').value('.', 'int')"})
63+
void preservesMethodsAndChains(String method) throws Exception {
64+
String sql = "SELECT (SELECT body FROM docs FOR XML PATH(''), TYPE)." + method;
65+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true,
66+
p -> p.withDialect(Dialect.SQLSERVER));
67+
MethodCallExpression call = (MethodCallExpression) select.getSelectItem(0).getExpression();
68+
assertInstanceOf(ParenthesedSelect.class, call.getExpression());
69+
assertEquals(method.substring(0, method.indexOf('(')), call.getMethod().getName());
70+
assertEquals(select.toString(), parse(select.toString()).toString());
71+
assertEquals(Set.of("docs"), new TablesNamesFinder().getTables((Statement) select));
72+
}
73+
74+
@Test
75+
void visitsReceiverAndMethodArgumentsWithContext() throws Exception {
76+
List<String> values = new ArrayList<>();
77+
ExpressionVisitorAdapter<Void> expressions = new ExpressionVisitorAdapter<Void>() {
78+
@Override
79+
public <S> Void visit(StringValue value, S context) {
80+
assertEquals("context", context);
81+
values.add(value.getValue());
82+
return null;
83+
}
84+
};
85+
SelectVisitorAdapter<Void> selects = new SelectVisitorAdapter<>(expressions);
86+
expressions.setSelectVisitor(selects);
87+
parse(SIMPLE).accept(new StatementVisitorAdapter<>(selects), "context");
88+
assertTrue(values.contains("."));
89+
assertTrue(values.contains("varchar(max)"));
90+
assertEquals(1, values.stream().filter("."::equals).count());
91+
}
92+
93+
@Test
94+
void editsAndDeparsesMethodArguments() throws Exception {
95+
PlainSelect select = parse(SIMPLE);
96+
MethodCallExpression call = (MethodCallExpression) select.getSelectItem(0).getExpression();
97+
call.setMethod(new Function("query", new StringValue("'/root'")));
98+
StringBuilder output = new StringBuilder();
99+
ExpressionDeParser expressions = new ExpressionDeParser() {
100+
@Override
101+
public <S> StringBuilder visit(StringValue value, S context) {
102+
return getBuilder().append("'changed'");
103+
}
104+
};
105+
select.accept(new StatementDeParser(expressions, new SelectDeParser(), output));
106+
assertTrue(output.toString().endsWith(".query('changed')"));
107+
assertTrue(select.toString().endsWith(".query('/root')"));
108+
assertEquals(output.toString(), parse(output.toString()).toString());
109+
}
110+
111+
@Test
112+
void gatesDialectAndRetainsRowNavigation() throws Exception {
113+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(SIMPLE));
114+
for (Dialect dialect : Dialect.values()) {
115+
if (dialect != Dialect.SQLSERVER) {
116+
assertThrows(JSQLParserException.class,
117+
() -> CCJSqlParserUtil.parse(SIMPLE, p -> p.withDialect(dialect)));
118+
}
119+
}
120+
assertSqlCanBeParsedAndDeparsed("SELECT (row_value).field FROM t");
121+
assertSqlCanBeParsedAndDeparsed("SELECT (row_value).field COLLATE en_US FROM t");
122+
assertThrows(JSQLParserException.class,
123+
() -> parse(SIMPLE.substring(0, SIMPLE.length() - 1)));
124+
assertEquals(2, CCJSqlParserUtil.parseStatements(SIMPLE + "; SELECT 1;",
125+
p -> p.withDialect(Dialect.SQLSERVER)).size());
126+
}
127+
128+
@Test
129+
void validatesMethodAsFunction() {
130+
String sql = "SELECT 1 WHERE " + SIMPLE.substring("SELECT ".length()) + " = 'value'";
131+
FeatureConfiguration config = new FeatureConfiguration().setValue(Feature.dialect,
132+
Dialect.SQLSERVER.name());
133+
assertTrue(new Validation(config, List.of(new FeaturesAllowed(Feature.values())), sql)
134+
.validate().isEmpty());
135+
assertFalse(new Validation(config,
136+
List.of(new FeaturesAllowed(Feature.values()).remove(Feature.function)), sql)
137+
.validate().isEmpty());
138+
}
139+
}

0 commit comments

Comments
 (0)