Skip to content

Commit db3e7a8

Browse files
committed
Traverse table functions with the configured expression visitor
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 cdbaf1a commit db3e7a8

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)