From 444573cef65c28865730d74fdda3ea3d12eaf7b7 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 11 Sep 2026 08:17:44 +0900 Subject: [PATCH 1/2] Visit optional DISTINCT ON items without changing the AST --- .../select/SelectVisitorAdapter.java | 23 ++--- .../statement/DistinctVisitorTest.java | 88 +++++++++++++++++++ 2 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java 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 45c10a5a5..deab87f1f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.select; +import java.util.List; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.expression.Function; @@ -67,11 +68,7 @@ public SelectVisitorAdapter(ExpressionVisitor expressionVisitor) { @Override public T visitOutputClause(OutputClause outputClause, S context) { if (outputClause != null) { - if (outputClause.getSelectItemList() != null) { - for (SelectItem selectItem : outputClause.getSelectItemList()) { - selectItem.accept(selectItemVisitor, context); - } - } + visitSelectItems(outputClause.getSelectItemList(), context); if (outputClause.getTableVariable() != null) { outputClause.getTableVariable().accept(expressionVisitor, context); } @@ -86,6 +83,14 @@ public T visitOutputClause(OutputClause outputClause, S context) { return null; } + private void visitSelectItems(List> items, S context) { + if (items != null) { + for (SelectItem item : items) { + item.accept(selectItemVisitor, context); + } + } + } + public ExpressionVisitor getExpressionVisitor() { return expressionVisitor; } @@ -146,18 +151,14 @@ public T visit(PlainSelect plainSelect, S context) { visitWithItems(plainSelect.withItemsList, context); if (plainSelect.getDistinct() != null) { - for (SelectItem selectItem : plainSelect.getDistinct().getOnSelectItems()) { - selectItem.accept(selectItemVisitor, context); - } + visitSelectItems(plainSelect.getDistinct().getOnSelectItems(), context); } if (plainSelect.getTop() != null) { plainSelect.getTop().getExpression().accept(expressionVisitor, context); } - for (SelectItem selectItem : plainSelect.getSelectItems()) { - selectItem.accept(selectItemVisitor, context); - } + visitSelectItems(plainSelect.getSelectItems(), context); if (plainSelect.getMySqlSelectIntoClause() != null) { MySqlSelectIntoClause mySqlSelectIntoClause = plainSelect.getMySqlSelectIntoClause(); diff --git a/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java b/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java new file mode 100644 index 000000000..057b942e0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java @@ -0,0 +1,88 @@ +/*- + * #%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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.PivotVisitorAdapter; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.SelectItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DistinctVisitorTest { + @ParameterizedTest + @ValueSource(strings = {"SELECT DISTINCT id FROM foo", + "SELECT * FROM (SELECT DISTINCT id FROM foo) x", + "WITH x AS (SELECT DISTINCT id FROM foo) SELECT * FROM x", + "SELECT DISTINCT ON (id) id FROM foo", "SELECT ALL id FROM foo"}) + void analysisPreservesSelectSyntax(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + String before = statement.toString(); + StatementFeatures features = statement.getFeatures(); + assertThat(features.returnsResultSet()).isTrue(); + assertThat(features.mayModifyData()).isFalse(); + assertThat(features.getUnresolvedReferences()).isEmpty(); + statement.accept(new StatementVisitorAdapter<>(), null); + assertThat(statement.toString()).isEqualTo(before); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed), null); + assertThat(CCJSqlParserUtil.parse(deparsed.toString()).toString()).isEqualTo(before); + } + + @Test + void plainDistinctKeepsItsAbsentOnList() throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT DISTINCT id FROM foo"); + select.accept(new SelectVisitorAdapter<>(), null); + assertThat(select.getDistinct().getOnSelectItems()).isNull(); + assertThat(select.toString()).isEqualTo("SELECT DISTINCT id FROM foo"); + } + + @Test + void selectAndOutputItemsKeepCallbackOrderAndContext() throws JSQLParserException { + List seen = new ArrayList<>(); + Object marker = new Object(); + SelectItemVisitorAdapter items = new SelectItemVisitorAdapter() { + @Override + public Void visit(SelectItem item, S context) { + assertThat(context).isSameAs(marker); + seen.add(item.getExpression().toString()); + return null; + } + }; + SelectVisitorAdapter visitor = new SelectVisitorAdapter<>( + new ExpressionVisitorAdapter<>(), new PivotVisitorAdapter<>(), items, + new FromItemVisitorAdapter<>()); + Select select = (Select) CCJSqlParserUtil + .parse("SELECT DISTINCT ON (key_fn(id)) value_fn(id), id FROM foo"); + select.accept(visitor, marker); + assertThat(seen).containsExactly("key_fn(id)", "value_fn(id)", "id"); + assertThat(select.getFeatures().getUnresolvedReferences()) + .containsExactly("key_fn", "value_fn"); + + seen.clear(); + PlainSelect output = (PlainSelect) CCJSqlParserUtil.parse("SELECT a, b"); + visitor.visitOutputClause(new OutputClause(output.getSelectItems(), null, null, null), + marker); + assertThat(seen).containsExactly("a", "b"); + } +} From 9d9c25a6e3e03734ee66a410a2b57773809aa350 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Fri, 11 Sep 2026 08:25:31 +0900 Subject: [PATCH 2/2] Verify plain DISTINCT retains unproven function analysis --- .../sf/jsqlparser/statement/DistinctVisitorTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java b/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java index 057b942e0..6a4f7b40c 100644 --- a/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/DistinctVisitorTest.java @@ -57,6 +57,17 @@ void plainDistinctKeepsItsAbsentOnList() throws JSQLParserException { assertThat(select.toString()).isEqualTo("SELECT DISTINCT id FROM foo"); } + @ParameterizedTest + @ValueSource(strings = {"SELECT DISTINCT unproven(id) FROM foo", + "SELECT * FROM (SELECT DISTINCT unproven(id) FROM foo) x", + "WITH x AS (SELECT DISTINCT unproven(id) FROM foo) SELECT * FROM x"}) + void plainDistinctStillReportsUnprovenFunctions(String sql) throws JSQLParserException { + StatementFeatures features = CCJSqlParserUtil.parse(sql).getFeatures(n -> false); + assertThat(features.getUnresolvedReferences()).containsExactly("unproven"); + assertThat(features.mayModifyData()).isTrue(); + assertThat(features.modifiesData()).isFalse(); + } + @Test void selectAndOutputItemsKeepCallbackOrderAndContext() throws JSQLParserException { List seen = new ArrayList<>();