From fbec089bf8b6062f7629bde7ef402c1463600d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Mon, 17 Aug 2026 23:21:38 +0800 Subject: [PATCH] fix: complete TablesNamesFinder traversal for piped queries, DML side clauses and analytic functions (#2478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Silently missed tables or threw for: piped queries (visit(FromQuery) was empty, now implements PipeOperatorVisitor), DELETE WITH lists (CTE tables lost, CTE alias leaked as phantom table), MERGE ON condition and WHEN operations (now implements MergeOperationVisitor), INSERT SET / ON DUPLICATE KEY UPDATE / ON CONFLICT actions and OUTPUT / RETURNING clauses of INSERT / UPDATE / DELETE, data modifying CTEs (ClassCastException in WithItem.getSelect(), dispatch any ParenthesedStatement instead) and analytic functions with function level ORDER BY (NPE), window ORDER BY or FILTER clause. Signed-off-by: 付典 --- .../sf/jsqlparser/util/TablesNamesFinder.java | 277 +++++++++++++++++- .../util/TablesNamesFinderTest.java | 130 +++++++- 2 files changed, 402 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 720e16254..3e15be9a7 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -76,6 +76,7 @@ import net.sf.jsqlparser.statement.PurgeObjectType; import net.sf.jsqlparser.statement.PurgeStatement; import net.sf.jsqlparser.statement.ResetStatement; +import net.sf.jsqlparser.statement.ReturningClause; import net.sf.jsqlparser.statement.RollbackStatement; import net.sf.jsqlparser.statement.SavepointStatement; import net.sf.jsqlparser.statement.SessionStatement; @@ -92,6 +93,7 @@ import net.sf.jsqlparser.statement.alter.AlterSystemStatement; import net.sf.jsqlparser.statement.alter.RenameTableStatement; import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; +import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; import net.sf.jsqlparser.statement.create.database.CreateDatabase; @@ -111,12 +113,38 @@ import net.sf.jsqlparser.statement.grant.Grant; import net.sf.jsqlparser.statement.imprt.Import; import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.insert.InsertConflictAction; +import net.sf.jsqlparser.statement.insert.InsertDuplicateAction; import net.sf.jsqlparser.statement.insert.OracleMultiInsertBranch; import net.sf.jsqlparser.statement.insert.OracleMultiInsertClause; import net.sf.jsqlparser.statement.insert.ParenthesedInsert; import net.sf.jsqlparser.statement.lock.LockStatement; import net.sf.jsqlparser.statement.merge.Merge; +import net.sf.jsqlparser.statement.merge.MergeDelete; +import net.sf.jsqlparser.statement.merge.MergeInsert; +import net.sf.jsqlparser.statement.merge.MergeOperation; +import net.sf.jsqlparser.statement.merge.MergeOperationVisitor; +import net.sf.jsqlparser.statement.merge.MergeUpdate; +import net.sf.jsqlparser.statement.piped.AggregatePipeOperator; +import net.sf.jsqlparser.statement.piped.AsPipeOperator; +import net.sf.jsqlparser.statement.piped.CallPipeOperator; +import net.sf.jsqlparser.statement.piped.DropPipeOperator; +import net.sf.jsqlparser.statement.piped.ExtendPipeOperator; import net.sf.jsqlparser.statement.piped.FromQuery; +import net.sf.jsqlparser.statement.piped.JoinPipeOperator; +import net.sf.jsqlparser.statement.piped.LimitPipeOperator; +import net.sf.jsqlparser.statement.piped.OrderByPipeOperator; +import net.sf.jsqlparser.statement.piped.PipeOperator; +import net.sf.jsqlparser.statement.piped.PipeOperatorVisitor; +import net.sf.jsqlparser.statement.piped.PivotPipeOperator; +import net.sf.jsqlparser.statement.piped.RenamePipeOperator; +import net.sf.jsqlparser.statement.piped.SelectPipeOperator; +import net.sf.jsqlparser.statement.piped.SetOperationPipeOperator; +import net.sf.jsqlparser.statement.piped.SetPipeOperator; +import net.sf.jsqlparser.statement.piped.TableSamplePipeOperator; +import net.sf.jsqlparser.statement.piped.UnPivotPipeOperator; +import net.sf.jsqlparser.statement.piped.WherePipeOperator; +import net.sf.jsqlparser.statement.piped.WindowPipeOperator; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.select.AllColumns; import net.sf.jsqlparser.statement.select.AllTableColumns; @@ -156,7 +184,8 @@ @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.UncommentedEmptyMethodBody"}) public class TablesNamesFinder implements SelectVisitor, FromItemVisitor, ExpressionVisitor, - SelectItemVisitor, StatementVisitor { + SelectItemVisitor, StatementVisitor, MergeOperationVisitor, + PipeOperatorVisitor { private Set tables; private boolean allowColumnProcessing = false; @@ -277,9 +306,8 @@ public Void visit(WithItem withItem, S context) { if (withItem.getAlias() != null) { otherItemNames.add(withItem.getAlias().getName()); } - if (withItem.getSelect() != null) { - withItem.getSelect().accept((SelectVisitor) this, context); - } + // dispatch any ParenthesedStatement payload (Select, Delete, Update, Insert) + withItem.accept((StatementVisitor) this, context); return null; } @@ -768,7 +796,15 @@ public Void visit(AnalyticExpression analytic, S context) { if (analytic.getKeep() != null) { analytic.getKeep().accept(this, context); } + if (analytic.getFilterExpression() != null) { + analytic.getFilterExpression().accept(this, context); + } if (analytic.getFuncOrderBy() != null) { + for (OrderByElement element : analytic.getFuncOrderBy()) { + element.getExpression().accept(this, context); + } + } + if (analytic.getOrderByElements() != null) { for (OrderByElement element : analytic.getOrderByElements()) { element.getExpression().accept(this, context); } @@ -852,9 +888,143 @@ public void visit(TableStatement tableStatement) { @Override public Void visit(FromQuery fromQuery, S context) { + List> withItemsList = fromQuery.getWithItemsList(); + if (withItemsList != null && !withItemsList.isEmpty()) { + for (WithItem withItem : withItemsList) { + withItem.accept((SelectVisitor) this, context); + } + } + if (fromQuery.getFromItem() != null) { + fromQuery.getFromItem().accept(this, context); + } + for (PipeOperator pipeOperator : fromQuery.getPipeOperators()) { + pipeOperator.accept(this, null); + } + return null; + } + + @Override + public Void visit(AggregatePipeOperator aggregate, Void context) { + for (SelectItem selectItem : aggregate.getSelectItems()) { + selectItem.accept(this, context); + } + for (SelectItem groupItem : aggregate.getGroupItems()) { + groupItem.accept(this, context); + } return null; } + @Override + public Void visit(AsPipeOperator as, Void context) { + if (as.getAlias() != null) { + otherItemNames.add(as.getAlias().getName()); + } + return null; + } + + @Override + public Void visit(CallPipeOperator call, Void context) { + visit(call.getTableFunction(), context); + return null; + } + + @Override + public Void visit(DropPipeOperator drop, Void context) { + drop.getColumns().accept(this, context); + return null; + } + + @Override + public Void visit(ExtendPipeOperator extend, Void context) { + return visit((SelectPipeOperator) extend, context); + } + + @Override + public Void visit(JoinPipeOperator joinPipeOperator, Void context) { + visitJoins(List.of(joinPipeOperator.getJoin()), context); + return null; + } + + @Override + public Void visit(LimitPipeOperator limit, Void context) { + limit.getLimitExpression().accept(this, context); + if (limit.getOffsetExpression() != null) { + limit.getOffsetExpression().accept(this, context); + } + return null; + } + + @Override + public Void visit(OrderByPipeOperator orderBy, Void context) { + for (OrderByElement element : orderBy.getOrderByElements()) { + element.getExpression().accept(this, context); + } + return null; + } + + @Override + public Void visit(PivotPipeOperator pivot, Void context) { + pivot.getAggregateExpression().accept(this, context); + for (SelectItem pivotColumn : pivot.getPivotColumns()) { + pivotColumn.accept(this, context); + } + return null; + } + + @Override + public Void visit(RenamePipeOperator rename, Void context) { + return visit((SelectPipeOperator) rename, context); + } + + @Override + public Void visit(SelectPipeOperator select, Void context) { + for (SelectItem selectItem : select.getSelectItems()) { + selectItem.accept(this, context); + } + return null; + } + + @Override + public Void visit(SetPipeOperator set, Void context) { + for (UpdateSet updateSet : set.getUpdateSets()) { + updateSet.getColumns().accept(this, context); + updateSet.getValues().accept(this, context); + } + return null; + } + + @Override + public Void visit(TableSamplePipeOperator tableSample, Void context) { + return null; + } + + @Override + public Void visit(SetOperationPipeOperator setOperation, Void context) { + for (ParenthesedSelect select : setOperation.getSelects()) { + select.accept((SelectVisitor) this, context); + } + return null; + } + + @Override + public Void visit(UnPivotPipeOperator unPivot, Void context) { + for (SelectItem pivotColumn : unPivot.getPivotColumns()) { + pivotColumn.accept(this, context); + } + return null; + } + + @Override + public Void visit(WherePipeOperator where, Void context) { + where.getExpression().accept(this, context); + return null; + } + + @Override + public Void visit(WindowPipeOperator window, Void context) { + return visit((SelectPipeOperator) window, context); + } + @Override public Void visit(DateUnitExpression dateUnitExpression, S context) { return null; @@ -988,6 +1158,11 @@ public Void visit(MySQLGroupConcat groupConcat, S context) { @Override public Void visit(Delete delete, S context) { + if (delete.getWithItemsList() != null) { + for (WithItem withItem : delete.getWithItemsList()) { + withItem.accept((SelectVisitor) this, context); + } + } visit(delete.getTable(), context); if (delete.getUsingFromItemList() != null) { @@ -1001,6 +1176,8 @@ public Void visit(Delete delete, S context) { if (delete.getWhere() != null) { delete.getWhere().accept(this, context); } + visitOutputClause(delete.getOutputClause(), context); + visitReturningClause(delete.getReturningClause(), context); return null; } @@ -1058,6 +1235,8 @@ public Void visit(Update update, S context) { if (update.getWhere() != null) { update.getWhere().accept(this, context); } + visitOutputClause(update.getOutputClause(), context); + visitReturningClause(update.getReturningClause(), context); return null; } @@ -1096,12 +1275,55 @@ public Void visit(Insert insert, S context) { withItem.accept((SelectVisitor) this, context); } } + if (insert.getSetUpdateSets() != null) { + visitUpdateSets(insert.getSetUpdateSets(), context); + } + if (insert.getDuplicateAction() != null) { + visitInsertAction(insert.getDuplicateAction(), context); + } + if (insert.getConflictAction() != null) { + visitInsertAction(insert.getConflictAction(), context); + } + visitOutputClause(insert.getOutputClause(), context); + visitReturningClause(insert.getReturningClause(), context); if (insert.getSelect() != null) { visit(insert.getSelect(), context); } return null; } + private void visitInsertAction(InsertDuplicateAction action, S context) { + visitUpdateSets(action.getUpdateSets(), context); + if (action.getWhereExpression() != null) { + action.getWhereExpression().accept(this, context); + } + } + + private void visitInsertAction(InsertConflictAction action, S context) { + visitUpdateSets(action.getUpdateSets(), context); + if (action.getWhereExpression() != null) { + action.getWhereExpression().accept(this, context); + } + } + + @Override + public Void visitOutputClause(OutputClause outputClause, S context) { + if (outputClause != null && outputClause.getSelectItemList() != null) { + for (SelectItem selectItem : outputClause.getSelectItemList()) { + selectItem.accept(this, context); + } + } + return null; + } + + private void visitReturningClause(ReturningClause returningClause, S context) { + if (returningClause != null) { + for (SelectItem selectItem : returningClause) { + selectItem.accept(this, context); + } + } + } + @Override public Void visit(ParenthesedInsert insert, S context) { return visit(insert.getInsert(), context); @@ -1315,6 +1537,53 @@ public Void visit(Merge merge, S context) { if (merge.getFromItem() != null) { merge.getFromItem().accept(this, context); } + + if (merge.getOnCondition() != null) { + merge.getOnCondition().accept(this, context); + } + + if (merge.getOperations() != null) { + for (MergeOperation operation : merge.getOperations()) { + operation.accept(this, context); + } + } + return null; + } + + @Override + public Void visit(MergeDelete mergeDelete, S context) { + if (mergeDelete.getAndPredicate() != null) { + mergeDelete.getAndPredicate().accept(this, context); + } + return null; + } + + @Override + public Void visit(MergeInsert mergeInsert, S context) { + if (mergeInsert.getAndPredicate() != null) { + mergeInsert.getAndPredicate().accept(this, context); + } + if (mergeInsert.getValues() != null) { + mergeInsert.getValues().accept(this, context); + } + if (mergeInsert.getWhereCondition() != null) { + mergeInsert.getWhereCondition().accept(this, context); + } + return null; + } + + @Override + public Void visit(MergeUpdate mergeUpdate, S context) { + if (mergeUpdate.getAndPredicate() != null) { + mergeUpdate.getAndPredicate().accept(this, context); + } + visitUpdateSets(mergeUpdate.getUpdateSets(), context); + if (mergeUpdate.getWhereCondition() != null) { + mergeUpdate.getWhereCondition().accept(this, context); + } + if (mergeUpdate.getDeleteWhereCondition() != null) { + mergeUpdate.getDeleteWhereCondition().accept(this, context); + } return null; } diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java index 8581a0e59..53a33e988 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTest.java @@ -773,10 +773,138 @@ void testJsonTable() throws JSQLParserException { @Test void testWindowExpressionWithNoRangeAndNoOffsetDoesNotThrowException() { - String sqlStr = "SELECT c, SUM(COUNT(*)) OVER (ORDER BY c ASC ROWS UNBOUNDED PRECEDING) FROM tbl GROUP BY c"; + String sqlStr = + "SELECT c, SUM(COUNT(*)) OVER (ORDER BY c ASC ROWS UNBOUNDED PRECEDING) FROM tbl GROUP BY c"; assertThatCode(() -> TablesNamesFinder.findTables(sqlStr)) .doesNotThrowAnyException(); } + @Test + void testPipedQuery() throws JSQLParserException { + String sqlStr = "FROM MY_TABLE1\n" + + "|> WHERE id IN (SELECT id FROM MY_TABLE2)\n" + + "|> LEFT JOIN (SELECT item, id FROM MY_TABLE3) AS t3 ON t3.item = item\n" + + "|> UNION ALL (SELECT * FROM MY_TABLE4)\n" + + "|> SELECT item"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3", "MY_TABLE4"); + } + + @Test + void testDeleteWithWithItemList() throws JSQLParserException { + String sqlStr = + "WITH cte AS (SELECT * FROM MY_TABLE2) DELETE FROM MY_TABLE1 WHERE id IN (SELECT id FROM cte)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testMergeOnConditionAndOperations() throws JSQLParserException { + String sqlStr = "MERGE INTO MY_TABLE1 t USING MY_TABLE2 s " + + "ON t.id IN (SELECT id FROM MY_TABLE3) " + + "WHEN MATCHED AND t.v > (SELECT MIN(v) FROM MY_TABLE4) THEN UPDATE SET t.v = (SELECT MAX(v) FROM MY_TABLE5) " + + "WHEN NOT MATCHED THEN INSERT (v) VALUES (1)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2", "MY_TABLE3", "MY_TABLE4", "MY_TABLE5"); + } + + @Test + void testInsertWithSetUpdateSets() throws JSQLParserException { + String sqlStr = "INSERT INTO MY_TABLE1 SET a = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithOnDuplicateKeyUpdate() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) ON DUPLICATE KEY UPDATE b = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithOnConflictDoUpdate() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) ON CONFLICT (a) DO UPDATE SET b = (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testWithDataModifyingCte() throws JSQLParserException { + String sqlStr = + "WITH del AS (DELETE FROM MY_TABLE2) INSERT INTO MY_TABLE1 SELECT * FROM del"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testUpdateWithOutputClause() throws JSQLParserException { + String sqlStr = + "UPDATE MY_TABLE1 SET a = 1 OUTPUT (SELECT x FROM MY_TABLE2) INTO @tv"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testUpdateWithReturningClause() throws JSQLParserException { + String sqlStr = "UPDATE MY_TABLE1 SET a = 1 RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testInsertWithReturningClause() throws JSQLParserException { + String sqlStr = + "INSERT INTO MY_TABLE1 (a) VALUES (1) RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testDeleteWithReturningClause() throws JSQLParserException { + String sqlStr = "DELETE FROM MY_TABLE1 RETURNING (SELECT x FROM MY_TABLE2)"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testAnalyticFunctionsWithFunctionOrderBy() throws JSQLParserException { + String sqlStr = + "SELECT string_agg(name, ',' ORDER BY id) OVER (PARTITION BY grp) FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1"); + } + + @Test + void testAnalyticFunctionsWithWindowOrderBy() throws JSQLParserException { + String sqlStr = + "SELECT SUM(v) OVER (ORDER BY (SELECT MAX(k) FROM MY_TABLE2)) FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + + @Test + void testAnalyticFunctionsWithFilterClause() throws JSQLParserException { + String sqlStr = + "SELECT SUM(v) FILTER (WHERE id IN (SELECT id FROM MY_TABLE2)) OVER () FROM MY_TABLE1"; + + assertThat(TablesNamesFinder.findTables(sqlStr)).containsExactlyInAnyOrder("MY_TABLE1", + "MY_TABLE2"); + } + }