From ca876323626afc91b581fcd780a581d1f08af40c Mon Sep 17 00:00:00 2001 From: minleejae Date: Mon, 28 Sep 2026 17:23:53 +0900 Subject: [PATCH] fix(deparser): preserve WITH items for VALUES statements Signed-off-by: minleejae --- .../util/deparser/ExpressionDeParser.java | 5 +- .../util/deparser/SelectDeParser.java | 2 +- .../deparser/ValuesStatementDeParser.java | 15 +- .../util/deparser/ValuesWithDeParserTest.java | 130 ++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/util/deparser/ValuesWithDeParserTest.java diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index de8e6ad6e1..18c7b14726 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -149,6 +149,7 @@ import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectItem; import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.select.WithItem; @SuppressWarnings({"PMD.CyclomaticComplexity"}) @@ -726,7 +727,9 @@ protected void deparse(BinaryExpression binaryExpression, @Override public StringBuilder visit(Select select, S context) { if (selectVisitor != null) { - if (select.getWithItemsList() != null) { + // ValuesStatementDeParser owns WITH rendering when using SelectDeParser. + if (select.getWithItemsList() != null + && !(select instanceof Values && selectVisitor instanceof SelectDeParser)) { builder.append("WITH "); for (Iterator> iter = select.getWithItemsList().iterator(); iter .hasNext();) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 055e2a4766..31a3b9fa52 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -934,7 +934,7 @@ public StringBuilder visit(ParenthesedFromItem fromItem, S context) { @Override public StringBuilder visit(Values values, S context) { - new ValuesStatementDeParser(expressionVisitor, builder).deParse(values); + new ValuesStatementDeParser(expressionVisitor, this, builder).deParse(values, context); return builder; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java index eb39481d0b..8c8ffe967a 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java @@ -15,17 +15,30 @@ public class ValuesStatementDeParser extends AbstractDeParser { private final ExpressionVisitor expressionVisitor; + private final SelectDeParser selectDeParser; public ValuesStatementDeParser(ExpressionVisitor expressionVisitor, StringBuilder buffer) { + this(expressionVisitor, null, buffer); + } + + public ValuesStatementDeParser(ExpressionVisitor expressionVisitor, + SelectDeParser selectDeParser, StringBuilder buffer) { super(buffer); this.expressionVisitor = expressionVisitor; + this.selectDeParser = selectDeParser; } @Override public void deParse(Values values) { + deParse(values, null); + } + + public void deParse(Values values, S context) { + new DmlDeParserSupport(expressionVisitor, selectDeParser, builder) + .deparseWithItems(values.getWithItemsList(), context); builder.append("VALUES "); - values.getExpressions().accept(expressionVisitor, null); + values.getExpressions().accept(expressionVisitor, context); if (values.getAlias() != null) { builder.append(" ").append(values.getAlias()); } diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/ValuesWithDeParserTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/ValuesWithDeParserTest.java new file mode 100644 index 0000000000..5afe7b72e2 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/ValuesWithDeParserTest.java @@ -0,0 +1,130 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.util.deparser; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.select.Values; +import net.sf.jsqlparser.statement.select.WithItem; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ValuesWithDeParserTest { + private static Statement parse(String sql) throws Exception { + return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "WITH cte(foo) AS (VALUES (42)) VALUES ((SELECT foo FROM cte))", + "WITH a(n) AS (VALUES (1)), b(n) AS (VALUES (2)) " + + "VALUES ((SELECT n FROM a), (SELECT n FROM b))", + "WITH RECURSIVE c(n) AS (VALUES (1) UNION ALL " + + "SELECT n + 1 FROM c WHERE n < 3) VALUES ((SELECT max(n) FROM c))", + "WITH c(n) AS MATERIALIZED (VALUES (1)) VALUES ((SELECT n FROM c))", + "WITH c(n) AS NOT MATERIALIZED (VALUES (1)) VALUES ((SELECT n FROM c))", + "WITH c AS (DELETE FROM t RETURNING id) VALUES ((SELECT count(*) FROM c))" + }) + void preservesWithItemsInStatementAndDirectValuesDeparsers(String sql) throws Exception { + Values values = assertInstanceOf(Values.class, assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.POSTGRESQL))); + String original = values.toString(); + for (boolean direct : new boolean[] {false, true}) { + StringBuilder output = new StringBuilder(); + SelectDeParser selects = new SelectDeParser(); + ExpressionDeParser expressions = new ExpressionDeParser(selects, output); + if (direct) { + new ValuesStatementDeParser(expressions, output).deParse(values); + } else { + values.accept(new StatementDeParser(expressions, selects, output), null); + } + Values reparsed = assertInstanceOf(Values.class, parse(output.toString())); + assertEquals(original, reparsed.toString()); + assertEquals(values.getWithItemsList().size(), reparsed.getWithItemsList().size()); + assertEquals(original, values.toString()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"statement", "select", "expression", "direct", "directWithSelect"}) + void preservesCustomVisitorsAndContext(String entryPoint) throws Exception { + Values values = assertInstanceOf(Values.class, + parse("WITH first_cte AS (VALUES (7)), second_cte AS (VALUES (8)) VALUES (9)")); + String original = values.toString(); + Object expectedContext = new Object(); + StringBuilder output = new StringBuilder(); + List withItems = new ArrayList<>(); + List numbers = new ArrayList<>(); + SelectDeParser selects = new SelectDeParser() { + @Override + public StringBuilder visit(WithItem item, S context) { + assertSame(expectedContext, context); + withItems.add(item.getAlias().getName()); + return super.visit(item, context); + } + }; + ExpressionDeParser expressions = new ExpressionDeParser(selects, output) { + @Override + public StringBuilder visit(LongValue value, S context) { + assertSame(expectedContext, context); + numbers.add(value.getValue()); + return output.append(value.getValue() + 100); + } + }; + selects.setExpressionVisitor(expressions); + selects.setBuilder(output); + switch (entryPoint) { + case "statement": + values.accept(new StatementDeParser(expressions, selects, output), expectedContext); + break; + case "select": + values.accept((SelectVisitor) selects, expectedContext); + break; + case "expression": + values.accept((ExpressionVisitor) expressions, expectedContext); + break; + case "direct": + new ValuesStatementDeParser(expressions, output).deParse(values, expectedContext); + break; + default: + new ValuesStatementDeParser(expressions, selects, output) + .deParse(values, expectedContext); + } + assertEquals(List.of("first_cte", "second_cte"), withItems); + assertEquals(List.of(7L, 8L, 9L), numbers); + assertEquals(parse("WITH first_cte AS (VALUES (107)), " + + "second_cte AS (VALUES (108)) VALUES (109)").toString(), + parse(output.toString()).toString()); + assertEquals(original, values.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT (WITH c(n) AS (VALUES (1)) VALUES ((SELECT n FROM c)))", + "VALUES ((WITH c(n) AS (VALUES (1)) VALUES ((SELECT n FROM c))))", + "SELECT * FROM (WITH c(n) AS (VALUES (1)) VALUES ((SELECT n FROM c))) AS v(n)" + }) + void rendersNestedValuesWithItemsOnce(String sql) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), parse(output.toString()).toString()); + } +}