Skip to content

Commit cc391af

Browse files
authored
Traverse table functions with the configured expression visitor (#2766)
Visit all FROM function expressions with their original context, share the supplied expression visitor through SELECT traversal, and remove the duplicate feature-analysis workaround. Cover JSON_TABLE, ROWS FROM, nested expressions, and once-only purity checks. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 6b67189 commit cc391af

4 files changed

Lines changed: 158 additions & 28 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java‎

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@
107107
import net.sf.jsqlparser.statement.select.SelectItemVisitorAdapter;
108108
import net.sf.jsqlparser.statement.select.SelectVisitor;
109109
import net.sf.jsqlparser.statement.select.SelectVisitorAdapter;
110-
import net.sf.jsqlparser.statement.select.TableFunction;
111110
import net.sf.jsqlparser.statement.select.TableStatement;
112111
import net.sf.jsqlparser.statement.select.WithItem;
113112
import net.sf.jsqlparser.statement.show.ShowIndexStatement;
@@ -175,7 +174,7 @@ private StatementFeatureVisitor(Analysis analysis) {
175174
*/
176175
private static SelectVisitorAdapter<Void> wire(Analysis analysis) {
177176
FeatureExpressionVisitor expressions = new FeatureExpressionVisitor(analysis);
178-
FeatureFromItemVisitor fromItems = new FeatureFromItemVisitor(analysis);
177+
FromItemVisitorAdapter<Void> fromItems = new FromItemVisitorAdapter<>();
179178

180179
FeatureSelectVisitor selects = new FeatureSelectVisitor(analysis, expressions,
181180
new PivotVisitorAdapter<>(expressions),
@@ -1027,30 +1026,6 @@ public <S> Void visit(TranscodingFunction expression, S context) {
10271026
}
10281027
}
10291028

1030-
static final class FeatureFromItemVisitor extends FromItemVisitorAdapter<Void> {
1031-
private final Analysis analysis;
1032-
1033-
FeatureFromItemVisitor(Analysis analysis) {
1034-
this.analysis = analysis;
1035-
}
1036-
1037-
/**
1038-
* The inherited implementation is empty, so table functions escape the purity check.
1039-
* {@link TableFunction#getFunctions()} also covers {@code ROWS FROM (..)}, where
1040-
* {@link TableFunction#getFunction()} is null.
1041-
*/
1042-
@Override
1043-
public <S> Void visit(TableFunction tableFunction, S context) {
1044-
List<Function> functions = tableFunction.getFunctions();
1045-
if (functions != null) {
1046-
for (Function function : functions) {
1047-
function.accept(analysis.expressions, context);
1048-
}
1049-
}
1050-
return super.visit(tableFunction, context);
1051-
}
1052-
}
1053-
10541029
@Override
10551030
public <S> Void visit(CreateRole statement, S context) {
10561031
return schemaOnly();

‎src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import net.sf.jsqlparser.expression.Expression;
1515
import net.sf.jsqlparser.expression.ExpressionVisitor;
1616
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
17+
import net.sf.jsqlparser.expression.Function;
1718
import net.sf.jsqlparser.schema.Column;
1819
import net.sf.jsqlparser.schema.Table;
1920
import net.sf.jsqlparser.statement.imprt.Import;
@@ -117,7 +118,11 @@ public <S> T visit(LateralSubSelect lateralSubSelect, S context) {
117118

118119
@Override
119120
public <S> T visit(TableFunction tableFunction, S context) {
120-
121+
if (tableFunction.getFunctions() != null) {
122+
for (Function function : tableFunction.getFunctions()) {
123+
function.accept(expressionVisitor, context);
124+
}
125+
}
121126
return null;
122127
}
123128

‎src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public SelectVisitorAdapter(ExpressionVisitor<T> expressionVisitor) {
6464
this.expressionVisitor = expressionVisitor;
6565
this.pivotVisitor = new PivotVisitorAdapter<>();
6666
this.selectItemVisitor = new SelectItemVisitorAdapter<>(this.expressionVisitor);
67-
this.fromItemVisitor = new FromItemVisitorAdapter<>();
67+
this.fromItemVisitor = new FromItemVisitorAdapter<>(this, this.expressionVisitor);
6868
}
6969

7070
@Override
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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.assertFalse;
14+
import static org.junit.jupiter.api.Assertions.assertNull;
15+
import static org.junit.jupiter.api.Assertions.assertSame;
16+
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import net.sf.jsqlparser.JSQLParserException;
20+
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
21+
import net.sf.jsqlparser.expression.Function;
22+
import net.sf.jsqlparser.expression.JsonTableFunction;
23+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
24+
import net.sf.jsqlparser.schema.Column;
25+
import net.sf.jsqlparser.statement.StatementFeatureVisitor;
26+
import net.sf.jsqlparser.statement.StatementFeatures;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.ValueSource;
30+
31+
class FromItemVisitorAdapterTest {
32+
33+
@Test
34+
void visitsEveryRowsFromFunctionWithTheSameContext() throws JSQLParserException {
35+
PlainSelect select = parse("SELECT * FROM ROWS FROM (f(source_col), g(other_col)) "
36+
+ "WITH ORDINALITY AS rows");
37+
RecordingExpressions expressions = new RecordingExpressions();
38+
39+
select.getFromItem().accept(new FromItemVisitorAdapter<>(expressions),
40+
expressions.context);
41+
42+
assertEquals(List.of("f", "g"), expressions.functions);
43+
assertEquals(List.of("source_col", "other_col"), expressions.columns);
44+
}
45+
46+
@ParameterizedTest
47+
@ValueSource(strings = {
48+
"SELECT * FROM f(g(source_col))",
49+
"SELECT * FROM (SELECT * FROM f(g(source_col))) nested",
50+
"SELECT * FROM base JOIN f(g(source_col)) AS derived ON 1 = 1"
51+
})
52+
void selectAdapterUsesTheSuppliedExpressionVisitorForTableFunctions(String sql)
53+
throws JSQLParserException {
54+
RecordingExpressions expressions = new RecordingExpressions();
55+
SelectVisitorAdapter<Void> visitor = new SelectVisitorAdapter<>(expressions);
56+
57+
parse(sql).accept(visitor, expressions.context);
58+
59+
assertEquals(List.of("f", "g"), expressions.functions);
60+
assertEquals(List.of("source_col"), expressions.columns);
61+
}
62+
63+
@Test
64+
void dispatchesJsonTableAndVisitsItsNestedExpressions() throws JSQLParserException {
65+
PlainSelect select = parse("SELECT * FROM JSON_TABLE(payload, '$' "
66+
+ "PASSING filter_expr AS filter COLUMNS ("
67+
+ "id INT PATH '$.id' DEFAULT fallback_value ON EMPTY, "
68+
+ "NESTED PATH '$.items[*]' COLUMNS ("
69+
+ "value INT PATH '$.value' DEFAULT nested_default ON ERROR))) AS jt");
70+
RecordingExpressions expressions = new RecordingExpressions();
71+
72+
select.getFromItem().accept(new FromItemVisitorAdapter<>(expressions),
73+
expressions.context);
74+
75+
assertEquals(1, expressions.jsonTables.size());
76+
assertSame(((TableFunction) select.getFromItem()).getFunction(),
77+
expressions.jsonTables.get(0));
78+
assertEquals(List.of("payload", "filter_expr", "fallback_value", "nested_default"),
79+
expressions.columns);
80+
}
81+
82+
@Test
83+
void traversesSubqueriesInFunctionArgumentsThroughTheConfiguredVisitorChain()
84+
throws JSQLParserException {
85+
RecordingExpressions expressions = new RecordingExpressions();
86+
SelectVisitorAdapter<Void> visitor = new SelectVisitorAdapter<>(expressions);
87+
expressions.setSelectVisitor(visitor);
88+
89+
parse("SELECT * FROM f((SELECT selected_col FROM g(argument_col)))")
90+
.accept(visitor, expressions.context);
91+
92+
assertEquals(List.of("f", "g"), expressions.functions);
93+
assertEquals(List.of("selected_col", "argument_col"), expressions.columns);
94+
}
95+
96+
@Test
97+
void emptyTableFunctionDoesNotInvokeTheExpressionVisitor() {
98+
RecordingExpressions expressions = new RecordingExpressions();
99+
TableFunction tableFunction = new TableFunction((Function) null);
100+
101+
assertNull(tableFunction.accept(new FromItemVisitorAdapter<>(expressions),
102+
expressions.context));
103+
assertEquals(List.of(), expressions.functions);
104+
}
105+
106+
@Test
107+
void featureAnalysisChecksEachTableFunctionOnce() throws JSQLParserException {
108+
List<String> checkedFunctions = new ArrayList<>();
109+
StatementFeatures features = StatementFeatureVisitor.analyse(
110+
CCJSqlParserUtil.parse("SELECT * FROM ROWS FROM (f(g(1)), h(2))"), name -> {
111+
checkedFunctions.add(name);
112+
return true;
113+
});
114+
115+
assertEquals(List.of("f", "g", "h"), checkedFunctions);
116+
assertFalse(features.mayModifyData());
117+
}
118+
119+
private static PlainSelect parse(String sql) throws JSQLParserException {
120+
return ((Select) CCJSqlParserUtil.parse(sql)).getPlainSelect();
121+
}
122+
123+
private static final class RecordingExpressions extends ExpressionVisitorAdapter<Void> {
124+
private final Object context = new Object();
125+
private final List<String> columns = new ArrayList<>();
126+
private final List<String> functions = new ArrayList<>();
127+
private final List<JsonTableFunction> jsonTables = new ArrayList<>();
128+
129+
@Override
130+
public <S> Void visit(Column column, S context) {
131+
assertSame(this.context, context);
132+
columns.add(column.getColumnName());
133+
return super.visit(column, context);
134+
}
135+
136+
@Override
137+
public <S> Void visit(Function function, S context) {
138+
assertSame(this.context, context);
139+
functions.add(function.getName());
140+
return super.visit(function, context);
141+
}
142+
143+
@Override
144+
public <S> Void visit(JsonTableFunction function, S context) {
145+
assertSame(this.context, context);
146+
jsonTables.add(function);
147+
return super.visit(function, context);
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)