Skip to content

Commit e0a233a

Browse files
AndreasLanz01claude
andcommitted
fix: find tables in skipped expression children
- Visit aggregate ORDER BY, KEEP, HAVING, LIMIT, keyword arguments and attribute functions of Function and AnalyticExpression - Visit LIKE ESCAPE, AT TIME ZONE zones, JSON operator paths, IS UNKNOWN and MATCH ... AGAINST operands - Visit GROUP_CONCAT and JSON aggregate key, value, ORDER BY and OVER - Dispatch table functions dynamically so XMLTABLE PASSING is visited Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bdf8ac3 commit e0a233a

2 files changed

Lines changed: 182 additions & 27 deletions

File tree

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

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@
223223
import net.sf.jsqlparser.statement.select.Join;
224224
import net.sf.jsqlparser.statement.select.LateralSubSelect;
225225
import net.sf.jsqlparser.statement.select.LateralView;
226+
import net.sf.jsqlparser.statement.select.Limit;
226227
import net.sf.jsqlparser.statement.select.MatchRecognize;
227228
import net.sf.jsqlparser.statement.select.OrderByElement;
228229
import net.sf.jsqlparser.statement.select.ParenthesedFromItem;
@@ -701,9 +702,28 @@ public <S> Void visit(Function function, S context) {
701702
if (exprList != null) {
702703
visit(exprList, context);
703704
}
705+
// an attribute column names a field of the result, only an attribute function is visited
706+
if (function.getAttribute() != function.getAttributeColumn()) {
707+
visitExpression((Expression) function.getAttribute(), context);
708+
}
709+
visitOrderBy(function.getOrderByElements(), context);
710+
visitAggregateModifiers(function.getKeep(), function.getHavingClause(),
711+
function.getKeywordArguments(), function.getLimit(), context);
704712
return null;
705713
}
706714

715+
private <S> void visitAggregateModifiers(KeepExpression keep, Function.HavingClause having,
716+
List<Function.KeywordArgument> keywordArguments, Limit limit, S context) {
717+
visitExpression(keep, context);
718+
visitExpression(having, context);
719+
if (keywordArguments != null) {
720+
for (Function.KeywordArgument keywordArgument : keywordArguments) {
721+
visitExpression(keywordArgument.getExpression(), context);
722+
}
723+
}
724+
visitLimit(limit, context);
725+
}
726+
707727
@Override
708728
public <S> Void visit(GreaterThan greaterThan, S context) {
709729
visitBinaryExpression(greaterThan);
@@ -739,7 +759,8 @@ public <S> Void visit(ExcludesExpression excludesExpression, S context) {
739759

740760
@Override
741761
public <S> Void visit(FullTextSearch fullTextSearch, S context) {
742-
762+
visitExpressions(fullTextSearch.getMatchColumns(), context);
763+
visitExpression(fullTextSearch.getAgainstValue(), context);
743764
return null;
744765
}
745766

@@ -769,7 +790,7 @@ public <S> Void visit(IsJsonExpression expression, S context) {
769790

770791
@Override
771792
public <S> Void visit(IsUnknownExpression isUnknownExpression, S context) {
772-
793+
isUnknownExpression.getLeftExpression().accept(this, context);
773794
return null;
774795
}
775796

@@ -782,6 +803,7 @@ public <S> Void visit(JdbcParameter jdbcParameter, S context) {
782803
@Override
783804
public <S> Void visit(LikeExpression likeExpression, S context) {
784805
visitBinaryExpression(likeExpression);
806+
visitExpression(likeExpression.getEscape(), context);
785807
return null;
786808
}
787809

@@ -1029,9 +1051,8 @@ public <S> Void visit(AnalyticExpression analytic, S context) {
10291051
if (analytic.getOffset() != null) {
10301052
analytic.getOffset().accept(this, context);
10311053
}
1032-
if (analytic.getKeep() != null) {
1033-
analytic.getKeep().accept(this, context);
1034-
}
1054+
visitAggregateModifiers(analytic.getKeep(), analytic.getHavingClause(),
1055+
analytic.getKeywordArguments(), analytic.getLimit(), context);
10351056
if (analytic.getFilterExpression() != null) {
10361057
analytic.getFilterExpression().accept(this, context);
10371058
}
@@ -1048,29 +1069,24 @@ public <S> Void visit(AnalyticExpression analytic, S context) {
10481069
element.getExpression().accept(this, context);
10491070
}
10501071
}
1072+
visitWindowElement(analytic.getWindowElement(), context);
1073+
return null;
1074+
}
10511075

1052-
if (analytic.getWindowElement() != null) {
1053-
if (analytic.getWindowElement().getRange() != null) {
1054-
if (analytic.getWindowElement().getRange().getStart() != null
1055-
&& analytic.getWindowElement().getRange().getStart()
1056-
.getExpression() != null) {
1057-
analytic.getWindowElement().getRange().getStart().getExpression().accept(this,
1058-
context);
1076+
private <S> void visitWindowElement(WindowElement windowElement, S context) {
1077+
if (windowElement != null) {
1078+
if (windowElement.getRange() != null) {
1079+
if (windowElement.getRange().getStart() != null) {
1080+
visitExpression(windowElement.getRange().getStart().getExpression(), context);
10591081
}
1060-
if (analytic.getWindowElement().getRange().getEnd() != null
1061-
&& analytic.getWindowElement().getRange().getEnd()
1062-
.getExpression() != null) {
1063-
analytic.getWindowElement().getRange().getEnd().getExpression().accept(this,
1064-
context);
1082+
if (windowElement.getRange().getEnd() != null) {
1083+
visitExpression(windowElement.getRange().getEnd().getExpression(), context);
10651084
}
10661085
}
1067-
if (analytic.getWindowElement().getOffset() != null
1068-
&& analytic.getWindowElement().getOffset().getExpression() != null) {
1069-
analytic.getWindowElement().getOffset().getExpression().accept(this, context);
1086+
if (windowElement.getOffset() != null) {
1087+
visitExpression(windowElement.getOffset().getExpression(), context);
10701088
}
1071-
10721089
}
1073-
return null;
10741090
}
10751091

10761092
@Override
@@ -1365,6 +1381,9 @@ public <S> Void visit(JsonExpression jsonExpr, S context) {
13651381
if (jsonExpr.getExpression() != null) {
13661382
jsonExpr.getExpression().accept(this, context);
13671383
}
1384+
for (Expression ident : jsonExpr.getIdents()) {
1385+
visitExpression(ident, context);
1386+
}
13681387
return null;
13691388
}
13701389

@@ -1436,13 +1455,14 @@ public <S> Void visit(NumericBind numericBind, S context) {
14361455

14371456
@Override
14381457
public <S> Void visit(KeepExpression keepExpression, S context) {
1439-
1458+
visitOrderBy(keepExpression.getOrderByElements(), context);
14401459
return null;
14411460
}
14421461

14431462
@Override
14441463
public <S> Void visit(MySQLGroupConcat groupConcat, S context) {
1445-
1464+
visitExpressions(groupConcat.getExpressionList(), context);
1465+
visitOrderBy(groupConcat.getOrderByElements(), context);
14461466
return null;
14471467
}
14481468

@@ -2001,7 +2021,8 @@ public <S> Void visit(OracleHint hint, S context) {
20012021
public <S> Void visit(TableFunction tableFunction, S context) {
20022022
if (tableFunction.getFunctions() != null) {
20032023
for (var function : tableFunction.getFunctions()) {
2004-
visit(function, null);
2024+
// dispatch dynamically, XMLTABLE and JSON_TABLE are Function subclasses
2025+
function.accept(this, context);
20052026
}
20062027
}
20072028
return null;
@@ -2450,6 +2471,9 @@ public void visit(CreateSynonym createSynonym) {
24502471
@Override
24512472
public <S> Void visit(TimezoneExpression timezoneExpression, S context) {
24522473
timezoneExpression.getLeftExpression().accept(this, context);
2474+
for (Expression timezone : timezoneExpression.getTimezoneExpressions()) {
2475+
visitExpression(timezone, context);
2476+
}
24532477
return null;
24542478
}
24552479

@@ -2491,11 +2515,21 @@ public <S> Void visit(JsonAggregateFunction expression, S context) {
24912515
if (expr != null) {
24922516
expr.accept(this, context);
24932517
}
2518+
if (expression.getKey() instanceof Expression) {
2519+
((Expression) expression.getKey()).accept(this, context);
2520+
}
2521+
if (expression.getValue() instanceof Expression) {
2522+
((Expression) expression.getValue()).accept(this, context);
2523+
}
2524+
visitOrderBy(expression.getExpressionOrderByElements(), context);
24942525

24952526
expr = expression.getFilterExpression();
24962527
if (expr != null) {
24972528
expr.accept(this, context);
24982529
}
2530+
visitExpressions(expression.getPartitionExpressionList(), context);
2531+
visitOrderBy(expression.getOrderByElements(), context);
2532+
visitWindowElement(expression.getWindowElement(), context);
24992533
return null;
25002534
}
25012535

‎src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTraversalTest.java‎

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import net.sf.jsqlparser.JSQLParserException;
1919
import net.sf.jsqlparser.expression.ArrayExpression;
2020
import net.sf.jsqlparser.expression.Expression;
21+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
2122
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
2223
import net.sf.jsqlparser.schema.Table;
2324
import net.sf.jsqlparser.statement.Statement;
@@ -30,6 +31,8 @@
3031
import org.junit.jupiter.params.provider.ValueSource;
3132

3233
class TablesNamesFinderTraversalTest {
34+
private static final String SUBQUERY = "(SELECT max(x) FROM t2)";
35+
3336
@ParameterizedTest
3437
@MethodSource("nestedTables")
3538
void findsTablesInNestedChildren(String sql, List<String> expected) throws JSQLParserException {
@@ -98,7 +101,110 @@ private static Stream<Arguments> nestedTables() {
98101
tables("UPSERT INTO t1 VALUES (1)", "t1"),
99102
tables("SELECT POSITION('x' IN 'xx') FROM t1", "t1"),
100103
tables("IF a>1 DELETE FROM t3", "t3"),
101-
tables("SELECT sum(x) OVER (ORDER BY (SELECT a FROM t2)) FROM t1", "t1", "t2"));
104+
tables("SELECT sum(x) OVER (ORDER BY (SELECT a FROM t2)) FROM t1", "t1", "t2"),
105+
tables("SELECT max(a) KEEP (DENSE_RANK FIRST ORDER BY (SELECT x FROM t2)) FROM t1",
106+
"t1", "t2"),
107+
tables("SELECT max(a) KEEP (DENSE_RANK FIRST ORDER BY (SELECT x FROM t2)) OVER (PARTITION BY b) FROM t1",
108+
"t1", "t2"),
109+
tables("SELECT array_agg(a LIMIT (SELECT x FROM t2)) FROM t1", "t1", "t2"),
110+
tables("SELECT any_value(a HAVING MAX (SELECT x FROM t2)) FROM t1", "t1", "t2"),
111+
tables("SELECT any_value(a HAVING MAX (SELECT x FROM t2)) OVER () FROM t1", "t1",
112+
"t2"),
113+
tables("SELECT group_concat(a SEPARATOR (SELECT x FROM t2)) FROM t1", "t1", "t2"),
114+
tables("SELECT group_concat(a ORDER BY (SELECT x FROM t2)) FROM t1", "t1", "t2"),
115+
tables("SELECT f(a).g((SELECT x FROM t2)) FROM t1", "t1", "t2"),
116+
tables("SELECT a FROM t1 WHERE (SELECT x FROM t2) IS UNKNOWN", "t1", "t2"),
117+
tables("SELECT a FROM t1 WHERE MATCH (a) AGAINST ((SELECT x FROM t2))", "t1",
118+
"t2"),
119+
tables("SELECT JSON_OBJECTAGG(KEY a VALUE (SELECT x FROM t2)) FROM t1", "t1",
120+
"t2"),
121+
tables("SELECT JSON_ARRAYAGG(a ORDER BY (SELECT x FROM t2)) FROM t1", "t1", "t2"),
122+
tables("SELECT JSON_ARRAYAGG(a) OVER (PARTITION BY (SELECT x FROM t2) ORDER BY (SELECT y FROM t3)) FROM t1",
123+
"t1", "t2", "t3"),
124+
tables("SELECT JSON_ARRAYAGG(a) OVER (ORDER BY b ROWS (SELECT x FROM t2) PRECEDING) FROM t1",
125+
"t1", "t2"),
126+
tables("SELECT * FROM XMLTABLE('/r' PASSING (SELECT x FROM t2) COLUMNS a int PATH 'a') x",
127+
"t2"),
128+
tables("SELECT * FROM JSON_TABLE((SELECT x FROM t2), '$' COLUMNS (a int PATH '$.a')) j",
129+
"t2"),
130+
tables("SELECT * FROM generate_series(1, (SELECT max(x) FROM t2)) g", "t2"));
131+
}
132+
133+
@ParameterizedTest
134+
@MethodSource("postgresTables")
135+
void findsTablesInPostgresExpressionChildren(String sql, List<String> expected)
136+
throws JSQLParserException {
137+
Statement statement = CCJSqlParserUtil.parse(sql,
138+
parser -> parser.withDialect(Dialect.POSTGRESQL));
139+
assertThat(new TablesNamesFinder<>().getTables(statement))
140+
.containsExactlyInAnyOrderElementsOf(expected);
141+
assertThat(new TablesNamesFinder<>().getTablesOrOtherSources(statement))
142+
.containsAll(expected);
143+
}
144+
145+
private static Stream<Arguments> postgresTables() {
146+
return Stream.of(
147+
// aggregate ORDER BY inside the call
148+
tables("SELECT string_agg(a, ',' ORDER BY " + SUBQUERY + ") FROM t1", "t1",
149+
"t2"),
150+
tables("SELECT array_agg(a ORDER BY " + SUBQUERY + ") FROM t1", "t1",
151+
"t2"),
152+
// LIKE ... ESCAPE
153+
tables("SELECT a FROM t1 WHERE a LIKE 'a' ESCAPE " + SUBQUERY, "t1",
154+
"t2"),
155+
// AT TIME ZONE, zone operand
156+
tables("SELECT now() AT TIME ZONE " + SUBQUERY, "t2"),
157+
// JSON operators, right operand
158+
tables("SELECT j -> " + SUBQUERY + " FROM t1", "t1", "t2"),
159+
tables("SELECT j ->> " + SUBQUERY + " FROM t1", "t1", "t2"),
160+
tables("SELECT j #> ARRAY[" + SUBQUERY + "] FROM t1", "t1", "t2"),
161+
tables("SELECT j #>> ARRAY[" + SUBQUERY + "] FROM t1", "t1", "t2"),
162+
// array slice bounds and subscript
163+
tables("SELECT a[1:(SELECT count(*)::int FROM t2)] FROM t1", "t1",
164+
"t2"),
165+
tables("SELECT a[(SELECT count(*)::int FROM t2):2] FROM t1", "t1",
166+
"t2"),
167+
tables("SELECT a[(SELECT count(*)::int FROM t2)] FROM t1", "t1",
168+
"t2"),
169+
// positions that were already traversed
170+
tables("SELECT f(" + SUBQUERY + ") FROM t1", "t1", "t2"),
171+
tables("SELECT count(*) FILTER (WHERE a = " + SUBQUERY + ") FROM t1", "t1",
172+
"t2"),
173+
tables("SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY " + SUBQUERY
174+
+ ") FROM t1", "t1", "t2"),
175+
tables("SELECT sum(a) OVER (PARTITION BY " + SUBQUERY + ") FROM t1", "t1",
176+
"t2"),
177+
tables("SELECT sum(a) OVER (ORDER BY " + SUBQUERY + ") FROM t1", "t1",
178+
"t2"),
179+
tables("SELECT sum(a) OVER (ORDER BY b ROWS BETWEEN " + SUBQUERY
180+
+ " PRECEDING AND CURRENT ROW) FROM t1", "t1", "t2"),
181+
tables("SELECT a FROM t1 LIMIT " + SUBQUERY, "t1", "t2"),
182+
tables("SELECT a FROM t1 OFFSET " + SUBQUERY, "t1", "t2"),
183+
tables("SELECT a FROM t1 FETCH FIRST " + SUBQUERY + " ROWS ONLY", "t1",
184+
"t2"),
185+
tables("SELECT a FROM t1 WHERE a = ANY (SELECT x FROM t2)", "t1",
186+
"t2"),
187+
tables("SELECT a FROM t1 WHERE a > ALL (SELECT x FROM t2)", "t1",
188+
"t2"),
189+
tables("SELECT a FROM t1 WHERE a = SOME (SELECT x FROM t2)", "t1",
190+
"t2"),
191+
tables("SELECT ARRAY(SELECT x FROM t2)", "t2"),
192+
tables("SELECT * FROM (VALUES (" + SUBQUERY + ")) v(x)", "t2"),
193+
tables("SELECT substring(a FROM " + SUBQUERY + ") FROM t1", "t1", "t2"),
194+
tables("SELECT trim(BOTH " + SUBQUERY + " FROM a) FROM t1", "t1", "t2"),
195+
tables("SELECT position(" + SUBQUERY + " IN a) FROM t1", "t1", "t2"),
196+
tables("SELECT overlay(a PLACING " + SUBQUERY + " FROM 1) FROM t1", "t1",
197+
"t2"),
198+
tables("SELECT CASE WHEN a = 1 THEN " + SUBQUERY + " END FROM t1", "t1",
199+
"t2"),
200+
tables("SELECT CAST(" + SUBQUERY + " AS text) FROM t1", "t1", "t2"),
201+
tables("SELECT " + SUBQUERY + "::text FROM t1", "t1", "t2"),
202+
tables("SELECT count(*) FROM t1 GROUP BY " + SUBQUERY, "t1", "t2"),
203+
tables("SELECT count(*) FROM t1 GROUP BY GROUPING SETS ((" + SUBQUERY + "))", "t1",
204+
"t2"),
205+
tables("SELECT DISTINCT ON (" + SUBQUERY + ") a FROM t1", "t1", "t2"),
206+
tables("SELECT * FROM t1, LATERAL (SELECT x FROM t2) p", "t1",
207+
"t2"));
102208
}
103209

104210
private static Arguments tables(String sql, String... names) {
@@ -122,7 +228,12 @@ private static Stream<Arguments> expressionTables() {
122228
tables("f()[OFFSET((SELECT x FROM t2)):OFFSET((SELECT y FROM t3))]", "t2", "t3"),
123229
tables("POSITION('x' IN (SELECT a FROM t2))", "t2"),
124230
tables("sum(x) OVER (PARTITION BY (SELECT a FROM t2))", "t2"),
125-
tables("a[1]"), tables("f()[1]"), tables("f()[:2]"), tables("f()[:]"));
231+
tables("string_agg(a, ',' ORDER BY (SELECT x FROM t2))", "t2"),
232+
tables("a LIKE 'a' ESCAPE (SELECT x FROM t2)", "t2"),
233+
tables("now() AT TIME ZONE (SELECT x FROM t2)", "t2"),
234+
tables("(SELECT x FROM t2) IS UNKNOWN", "t2"),
235+
tables("a[1]"), tables("f()[1]"), tables("f()[:2]"), tables("f()[:]"),
236+
tables("f(a).b.c"));
126237
}
127238

128239
@ParameterizedTest
@@ -164,6 +275,16 @@ void visitsNewChildrenWithoutRepeatingExistingChildren() throws JSQLParserExcept
164275
assertThat(finder.visits).isEqualTo(Map.of("t1", 1, "t2", 1, "t3", 1, "t4", 1, "t5", 1));
165276
}
166277

278+
@Test
279+
void visitsAggregateChildrenOnce() throws JSQLParserException {
280+
CountingFinder finder = new CountingFinder();
281+
Statement statement = CCJSqlParserUtil.parse(
282+
"SELECT max(a) KEEP (DENSE_RANK FIRST ORDER BY (SELECT x FROM t2)) OVER (PARTITION BY (SELECT y FROM t3)), string_agg(a, ',' ORDER BY (SELECT z FROM t4)) FROM t1");
283+
assertThat(finder.getTables(statement)).containsExactlyInAnyOrder("t1", "t2", "t3",
284+
"t4");
285+
assertThat(finder.visits).isEqualTo(Map.of("t1", 1, "t2", 1, "t3", 1, "t4", 1));
286+
}
287+
167288
@Test
168289
void resetsStateBetweenStatementAndExpressionVisits() throws JSQLParserException {
169290
TablesNamesFinder<?> finder = new TablesNamesFinder<>();

0 commit comments

Comments
 (0)