Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
}

default <S> T visitLimit(Limit limit, S context) {
if (limit != null && !limit.isLimitNull() && !limit.isLimitAll()) {

Check warning on line 104 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 104 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isLimitNull() in Limit has been deprecated

Check warning on line 104 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 104 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isLimitNull() in Limit has been deprecated

Check warning on line 104 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 104 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isLimitNull() in Limit has been deprecated
if (limit.getOffset() != null) {
limit.getOffset().accept(this, context);
}
Expand Down Expand Up @@ -569,6 +569,15 @@

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

default <S> T visit(MethodCallExpression methodCall, S context) {
methodCall.getExpression().accept(this, context);
return methodCall.getMethod().accept(this, context);
}

default void visit(MethodCallExpression methodCall) {
this.visit(methodCall, null);
}

default void visit(RowGetExpression rowGetExpression) {
this.visit(rowGetExpression, null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.expression;

import java.util.function.Consumer;
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;

/** A method applied to an expression, such as a SQL Server XML subquery result. */
public class MethodCallExpression extends ASTNodeAccessImpl implements Expression {
private Expression expression;
private Function method;

public MethodCallExpression(Expression expression, Function method) {
this.expression = expression;
this.method = method;
}

public Expression getExpression() {
return expression;
}

public void setExpression(Expression expression) {
this.expression = expression;
}

public Function getMethod() {
return method;
}

public void setMethod(Function method) {
this.method = method;
}

public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> printer) {
printer.accept(expression);
builder.append('.');
printer.accept(method);
return builder;
}

@Override
public <T, S> T accept(ExpressionVisitor<T> visitor, S context) {
return visitor.visit(this, context);
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
return appendTo(builder, value -> builder.append(value)).toString();
}
}
5 changes: 5 additions & 0 deletions src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,11 @@ public <S> Void visit(RowGetExpression rowGetExpression, S context) {
return null;
}

@Override
public <S> Void visit(MethodCallExpression methodCall, S context) {
return ExpressionVisitor.super.visit(methodCall, context);
}

@Override
public <S> Void visit(HexValue hexValue, S context) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import net.sf.jsqlparser.expression.RangeExpression;
import net.sf.jsqlparser.expression.RowConstructor;
import net.sf.jsqlparser.expression.RowGetExpression;
import net.sf.jsqlparser.expression.MethodCallExpression;
import net.sf.jsqlparser.expression.SignedExpression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.StructType;
Expand Down Expand Up @@ -1471,6 +1472,11 @@ public <S> StringBuilder visit(RowGetExpression rowGetExpression, S context) {
return null;
}

@Override
public <S> StringBuilder visit(MethodCallExpression methodCall, S context) {
return methodCall.appendTo(builder, expression -> expression.accept(this, context));
}

@Override
public <S> StringBuilder visit(OracleHint hint, S context) {
builder.append(hint.toString());
Expand Down
14 changes: 10 additions & 4 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -9221,10 +9221,17 @@ Expression NavigationStep(Expression base):
{
Expression step = null;
String nm = null;
Function method;
}
{
(
"." nm=RelObjectNameExt() { step = new RowGetExpression(base, nm); }
"."
(
LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect)) && isFunctionAhead() })
method=Function() { step = new MethodCallExpression(base, method); }
|
nm=RelObjectNameExt() { step = new RowGetExpression(base, nm); }
)
|
step = ArrayExpression(base)
)
Expand All @@ -9242,7 +9249,6 @@ Expression PrimaryExpression() #PrimaryExpression:
Token token = null;
Token sign = null;
Token adjacentToken = null;
String tmp = "";
ColDataType type = null;
boolean not = false;
boolean exclamationMarkNot = false;
Expand Down Expand Up @@ -9395,8 +9401,8 @@ Expression PrimaryExpression() #PrimaryExpression:
"." "*"
{ retval = new FunctionAllColumns(unwrapParenthesedFunction(retval)); } ]

// RowGet Expressions
( LOOKAHEAD(2) "." tmp=RelObjectName() { retval = new RowGetExpression(retval, tmp); } )*
// Share field and method navigation with the other primary expressions.
( LOOKAHEAD(".") nxt=NavigationStep(retval) { retval = nxt; } )*
)
)

Expand Down
7 changes: 7 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,13 @@ uses the existing ``Update`` model's ``fromItem`` and ``joins`` properties.
Table discovery and metadata validation recognize a target alias declared in
that FROM clause. Other dialects retain the existing FROM-after-SET syntax.

``Dialect.SQLSERVER`` supports methods on expression results, including
``(SELECT ... FOR XML PATH(''), TYPE).value('.', 'varchar(max)')``.
``MethodCallExpression`` exposes the receiver expression and a ``Function``
containing the method name and arguments. Field access and method calls share
the navigation grammar; expression visitors and deparsers traverse both the
receiver and method arguments. XQuery strings remain string literals.

``Dialect.POSTGRESQL`` enables ``DO [LANGUAGE name] code [LANGUAGE name]``,
with the language clause allowed once, before or after the body.
``DoStatement.getCode()`` is a ``StringValue`` that preserves the literal's
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.select;

import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.MethodCallExpression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.parser.feature.Feature;
import net.sf.jsqlparser.parser.feature.FeatureConfiguration;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.StatementVisitorAdapter;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import net.sf.jsqlparser.util.validation.Validation;
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class SqlServerXmlMethodTest {
private static final String SIMPLE =
"SELECT (SELECT body FROM docs FOR XML PATH (''), TYPE).value('.', 'varchar(max)')";

private static PlainSelect parse(String sql) throws Exception {
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.SQLSERVER));
}

@Test
void parsesOriginalStuffQueryIssue386() throws Exception {
String sql = "SELECT (STUFF((SELECT '|' + person_name FROM person "
+ "JOIN person_group ON person.person_id = person_group.person_id "
+ "WHERE person_group.group_id = 1 FOR XML PATH(''), TYPE)"
+ ".value('.', 'varchar(max)'), 1, 1, '')) AS person_name";
Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true,
p -> p.withDialect(Dialect.SQLSERVER));
assertEquals(Set.of("person", "person_group"),
new TablesNamesFinder().getTables(statement));
assertEquals(statement.toString(), parse(statement.toString()).toString());
}

@ParameterizedTest
@ValueSource(strings = {"value('.', 'varchar(max)')", "query('/root')", "exist('/root')",
"query('/root').value('.', 'int')"})
void preservesMethodsAndChains(String method) throws Exception {
String sql = "SELECT (SELECT body FROM docs FOR XML PATH(''), TYPE)." + method;
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true,
p -> p.withDialect(Dialect.SQLSERVER));
MethodCallExpression call = (MethodCallExpression) select.getSelectItem(0).getExpression();
assertInstanceOf(ParenthesedSelect.class, call.getExpression());
assertEquals(method.substring(0, method.indexOf('(')), call.getMethod().getName());
assertEquals(select.toString(), parse(select.toString()).toString());
assertEquals(Set.of("docs"), new TablesNamesFinder().getTables((Statement) select));
}

@Test
void visitsReceiverAndMethodArgumentsWithContext() throws Exception {
List<String> values = new ArrayList<>();
ExpressionVisitorAdapter<Void> expressions = new ExpressionVisitorAdapter<Void>() {
@Override
public <S> Void visit(StringValue value, S context) {
assertEquals("context", context);
values.add(value.getValue());
return null;
}
};
SelectVisitorAdapter<Void> selects = new SelectVisitorAdapter<>(expressions);
expressions.setSelectVisitor(selects);
parse(SIMPLE).accept(new StatementVisitorAdapter<>(selects), "context");
assertTrue(values.contains("."));
assertTrue(values.contains("varchar(max)"));
assertEquals(1, values.stream().filter("."::equals).count());
}

@Test
void editsAndDeparsesMethodArguments() throws Exception {
PlainSelect select = parse(SIMPLE);
MethodCallExpression call = (MethodCallExpression) select.getSelectItem(0).getExpression();
call.setMethod(new Function("query", new StringValue("'/root'")));
StringBuilder output = new StringBuilder();
ExpressionDeParser expressions = new ExpressionDeParser() {
@Override
public <S> StringBuilder visit(StringValue value, S context) {
return getBuilder().append("'changed'");
}
};
select.accept(new StatementDeParser(expressions, new SelectDeParser(), output));
assertTrue(output.toString().endsWith(".query('changed')"));
assertTrue(select.toString().endsWith(".query('/root')"));
assertEquals(output.toString(), parse(output.toString()).toString());
}

@Test
void gatesDialectAndRetainsRowNavigation() throws Exception {
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(SIMPLE));
for (Dialect dialect : Dialect.values()) {
if (dialect != Dialect.SQLSERVER) {
assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse(SIMPLE, p -> p.withDialect(dialect)));
}
}
assertSqlCanBeParsedAndDeparsed("SELECT (row_value).field FROM t");
assertSqlCanBeParsedAndDeparsed("SELECT (row_value).field COLLATE en_US FROM t");
assertThrows(JSQLParserException.class,
() -> parse(SIMPLE.substring(0, SIMPLE.length() - 1)));
assertEquals(2, CCJSqlParserUtil.parseStatements(SIMPLE + "; SELECT 1;",
p -> p.withDialect(Dialect.SQLSERVER)).size());
}

@Test
void validatesMethodAsFunction() {
String sql = "SELECT 1 WHERE " + SIMPLE.substring("SELECT ".length()) + " = 'value'";
FeatureConfiguration config = new FeatureConfiguration().setValue(Feature.dialect,
Dialect.SQLSERVER.name());
assertTrue(new Validation(config, List.of(new FeaturesAllowed(Feature.values())), sql)
.validate().isEmpty());
assertFalse(new Validation(config,
List.of(new FeaturesAllowed(Feature.values()).remove(Feature.function)), sql)
.validate().isEmpty());
}
}
Loading