diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 447c6aba2..d1eac1029 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -107,7 +107,6 @@ import net.sf.jsqlparser.statement.select.SelectItemVisitorAdapter; import net.sf.jsqlparser.statement.select.SelectVisitor; import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; -import net.sf.jsqlparser.statement.select.TableFunction; import net.sf.jsqlparser.statement.select.TableStatement; import net.sf.jsqlparser.statement.select.WithItem; import net.sf.jsqlparser.statement.show.ShowIndexStatement; @@ -175,7 +174,7 @@ private StatementFeatureVisitor(Analysis analysis) { */ private static SelectVisitorAdapter wire(Analysis analysis) { FeatureExpressionVisitor expressions = new FeatureExpressionVisitor(analysis); - FeatureFromItemVisitor fromItems = new FeatureFromItemVisitor(analysis); + FromItemVisitorAdapter fromItems = new FromItemVisitorAdapter<>(); FeatureSelectVisitor selects = new FeatureSelectVisitor(analysis, expressions, new PivotVisitorAdapter<>(expressions), @@ -1027,30 +1026,6 @@ public Void visit(TranscodingFunction expression, S context) { } } - static final class FeatureFromItemVisitor extends FromItemVisitorAdapter { - private final Analysis analysis; - - FeatureFromItemVisitor(Analysis analysis) { - this.analysis = analysis; - } - - /** - * The inherited implementation is empty, so table functions escape the purity check. - * {@link TableFunction#getFunctions()} also covers {@code ROWS FROM (..)}, where - * {@link TableFunction#getFunction()} is null. - */ - @Override - public Void visit(TableFunction tableFunction, S context) { - List functions = tableFunction.getFunctions(); - if (functions != null) { - for (Function function : functions) { - function.accept(analysis.expressions, context); - } - } - return super.visit(tableFunction, context); - } - } - @Override public Void visit(CreateRole statement, S context) { return schemaOnly(); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java index 704c22f25..157582acb 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.imprt.Import; @@ -117,7 +118,11 @@ public T visit(LateralSubSelect lateralSubSelect, S context) { @Override public T visit(TableFunction tableFunction, S context) { - + if (tableFunction.getFunctions() != null) { + for (Function function : tableFunction.getFunctions()) { + function.accept(expressionVisitor, context); + } + } return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java index da37acddd..8b6e65787 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -64,7 +64,7 @@ public SelectVisitorAdapter(ExpressionVisitor expressionVisitor) { this.expressionVisitor = expressionVisitor; this.pivotVisitor = new PivotVisitorAdapter<>(); this.selectItemVisitor = new SelectItemVisitorAdapter<>(this.expressionVisitor); - this.fromItemVisitor = new FromItemVisitorAdapter<>(); + this.fromItemVisitor = new FromItemVisitorAdapter<>(this, this.expressionVisitor); } @Override diff --git a/src/test/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapterTest.java b/src/test/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapterTest.java new file mode 100644 index 000000000..472222067 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapterTest.java @@ -0,0 +1,150 @@ +/*- + * #%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 org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.JsonTableFunction; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.StatementFeatureVisitor; +import net.sf.jsqlparser.statement.StatementFeatures; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class FromItemVisitorAdapterTest { + + @Test + void visitsEveryRowsFromFunctionWithTheSameContext() throws JSQLParserException { + PlainSelect select = parse("SELECT * FROM ROWS FROM (f(source_col), g(other_col)) " + + "WITH ORDINALITY AS rows"); + RecordingExpressions expressions = new RecordingExpressions(); + + select.getFromItem().accept(new FromItemVisitorAdapter<>(expressions), + expressions.context); + + assertEquals(List.of("f", "g"), expressions.functions); + assertEquals(List.of("source_col", "other_col"), expressions.columns); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM f(g(source_col))", + "SELECT * FROM (SELECT * FROM f(g(source_col))) nested", + "SELECT * FROM base JOIN f(g(source_col)) AS derived ON 1 = 1" + }) + void selectAdapterUsesTheSuppliedExpressionVisitorForTableFunctions(String sql) + throws JSQLParserException { + RecordingExpressions expressions = new RecordingExpressions(); + SelectVisitorAdapter visitor = new SelectVisitorAdapter<>(expressions); + + parse(sql).accept(visitor, expressions.context); + + assertEquals(List.of("f", "g"), expressions.functions); + assertEquals(List.of("source_col"), expressions.columns); + } + + @Test + void dispatchesJsonTableAndVisitsItsNestedExpressions() throws JSQLParserException { + PlainSelect select = parse("SELECT * FROM JSON_TABLE(payload, '$' " + + "PASSING filter_expr AS filter COLUMNS (" + + "id INT PATH '$.id' DEFAULT fallback_value ON EMPTY, " + + "NESTED PATH '$.items[*]' COLUMNS (" + + "value INT PATH '$.value' DEFAULT nested_default ON ERROR))) AS jt"); + RecordingExpressions expressions = new RecordingExpressions(); + + select.getFromItem().accept(new FromItemVisitorAdapter<>(expressions), + expressions.context); + + assertEquals(1, expressions.jsonTables.size()); + assertSame(((TableFunction) select.getFromItem()).getFunction(), + expressions.jsonTables.get(0)); + assertEquals(List.of("payload", "filter_expr", "fallback_value", "nested_default"), + expressions.columns); + } + + @Test + void traversesSubqueriesInFunctionArgumentsThroughTheConfiguredVisitorChain() + throws JSQLParserException { + RecordingExpressions expressions = new RecordingExpressions(); + SelectVisitorAdapter visitor = new SelectVisitorAdapter<>(expressions); + expressions.setSelectVisitor(visitor); + + parse("SELECT * FROM f((SELECT selected_col FROM g(argument_col)))") + .accept(visitor, expressions.context); + + assertEquals(List.of("f", "g"), expressions.functions); + assertEquals(List.of("selected_col", "argument_col"), expressions.columns); + } + + @Test + void emptyTableFunctionDoesNotInvokeTheExpressionVisitor() { + RecordingExpressions expressions = new RecordingExpressions(); + TableFunction tableFunction = new TableFunction((Function) null); + + assertNull(tableFunction.accept(new FromItemVisitorAdapter<>(expressions), + expressions.context)); + assertEquals(List.of(), expressions.functions); + } + + @Test + void featureAnalysisChecksEachTableFunctionOnce() throws JSQLParserException { + List checkedFunctions = new ArrayList<>(); + StatementFeatures features = StatementFeatureVisitor.analyse( + CCJSqlParserUtil.parse("SELECT * FROM ROWS FROM (f(g(1)), h(2))"), name -> { + checkedFunctions.add(name); + return true; + }); + + assertEquals(List.of("f", "g", "h"), checkedFunctions); + assertFalse(features.mayModifyData()); + } + + private static PlainSelect parse(String sql) throws JSQLParserException { + return ((Select) CCJSqlParserUtil.parse(sql)).getPlainSelect(); + } + + private static final class RecordingExpressions extends ExpressionVisitorAdapter { + private final Object context = new Object(); + private final List columns = new ArrayList<>(); + private final List functions = new ArrayList<>(); + private final List jsonTables = new ArrayList<>(); + + @Override + public Void visit(Column column, S context) { + assertSame(this.context, context); + columns.add(column.getColumnName()); + return super.visit(column, context); + } + + @Override + public Void visit(Function function, S context) { + assertSame(this.context, context); + functions.add(function.getName()); + return super.visit(function, context); + } + + @Override + public Void visit(JsonTableFunction function, S context) { + assertSame(this.context, context); + jsonTables.add(function); + return super.visit(function, context); + } + } +}