Skip to content

Commit ca87632

Browse files
committed
fix(deparser): preserve WITH items for VALUES statements
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 2b3bebe commit ca87632

4 files changed

Lines changed: 149 additions & 3 deletions

File tree

‎src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
import net.sf.jsqlparser.statement.select.Select;
150150
import net.sf.jsqlparser.statement.select.SelectItem;
151151
import net.sf.jsqlparser.statement.select.SelectVisitor;
152+
import net.sf.jsqlparser.statement.select.Values;
152153
import net.sf.jsqlparser.statement.select.WithItem;
153154

154155
@SuppressWarnings({"PMD.CyclomaticComplexity"})
@@ -726,7 +727,9 @@ protected <S> void deparse(BinaryExpression binaryExpression,
726727
@Override
727728
public <S> StringBuilder visit(Select select, S context) {
728729
if (selectVisitor != null) {
729-
if (select.getWithItemsList() != null) {
730+
// ValuesStatementDeParser owns WITH rendering when using SelectDeParser.
731+
if (select.getWithItemsList() != null
732+
&& !(select instanceof Values && selectVisitor instanceof SelectDeParser)) {
730733
builder.append("WITH ");
731734
for (Iterator<WithItem<?>> iter = select.getWithItemsList().iterator(); iter
732735
.hasNext();) {

‎src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -934,7 +934,7 @@ public <S> StringBuilder visit(ParenthesedFromItem fromItem, S context) {
934934

935935
@Override
936936
public <S> StringBuilder visit(Values values, S context) {
937-
new ValuesStatementDeParser(expressionVisitor, builder).deParse(values);
937+
new ValuesStatementDeParser(expressionVisitor, this, builder).deParse(values, context);
938938
return builder;
939939
}
940940

‎src/main/java/net/sf/jsqlparser/util/deparser/ValuesStatementDeParser.java‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,30 @@
1515
public class ValuesStatementDeParser extends AbstractDeParser<Values> {
1616

1717
private final ExpressionVisitor<StringBuilder> expressionVisitor;
18+
private final SelectDeParser selectDeParser;
1819

1920
public ValuesStatementDeParser(ExpressionVisitor<StringBuilder> expressionVisitor,
2021
StringBuilder buffer) {
22+
this(expressionVisitor, null, buffer);
23+
}
24+
25+
public ValuesStatementDeParser(ExpressionVisitor<StringBuilder> expressionVisitor,
26+
SelectDeParser selectDeParser, StringBuilder buffer) {
2127
super(buffer);
2228
this.expressionVisitor = expressionVisitor;
29+
this.selectDeParser = selectDeParser;
2330
}
2431

2532
@Override
2633
public void deParse(Values values) {
34+
deParse(values, null);
35+
}
36+
37+
public <S> void deParse(Values values, S context) {
38+
new DmlDeParserSupport(expressionVisitor, selectDeParser, builder)
39+
.deparseWithItems(values.getWithItemsList(), context);
2740
builder.append("VALUES ");
28-
values.getExpressions().accept(expressionVisitor, null);
41+
values.getExpressions().accept(expressionVisitor, context);
2942
if (values.getAlias() != null) {
3043
builder.append(" ").append(values.getAlias());
3144
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2019 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.util.deparser;
11+
12+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
13+
import static org.junit.jupiter.api.Assertions.*;
14+
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import net.sf.jsqlparser.expression.ExpressionVisitor;
18+
import net.sf.jsqlparser.expression.LongValue;
19+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
20+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
21+
import net.sf.jsqlparser.statement.Statement;
22+
import net.sf.jsqlparser.statement.select.SelectVisitor;
23+
import net.sf.jsqlparser.statement.select.Values;
24+
import net.sf.jsqlparser.statement.select.WithItem;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.ValueSource;
27+
28+
class ValuesWithDeParserTest {
29+
private static Statement parse(String sql) throws Exception {
30+
return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.POSTGRESQL));
31+
}
32+
33+
@ParameterizedTest
34+
@ValueSource(strings = {
35+
"WITH cte(foo) AS (VALUES (42)) VALUES ((SELECT foo FROM cte))",
36+
"WITH a(n) AS (VALUES (1)), b(n) AS (VALUES (2)) "
37+
+ "VALUES ((SELECT n FROM a), (SELECT n FROM b))",
38+
"WITH RECURSIVE c(n) AS (VALUES (1) UNION ALL "
39+
+ "SELECT n + 1 FROM c WHERE n < 3) VALUES ((SELECT max(n) FROM c))",
40+
"WITH c(n) AS MATERIALIZED (VALUES (1)) VALUES ((SELECT n FROM c))",
41+
"WITH c(n) AS NOT MATERIALIZED (VALUES (1)) VALUES ((SELECT n FROM c))",
42+
"WITH c AS (DELETE FROM t RETURNING id) VALUES ((SELECT count(*) FROM c))"
43+
})
44+
void preservesWithItemsInStatementAndDirectValuesDeparsers(String sql) throws Exception {
45+
Values values = assertInstanceOf(Values.class, assertSqlCanBeParsedAndDeparsed(sql, true,
46+
parser -> parser.withDialect(Dialect.POSTGRESQL)));
47+
String original = values.toString();
48+
for (boolean direct : new boolean[] {false, true}) {
49+
StringBuilder output = new StringBuilder();
50+
SelectDeParser selects = new SelectDeParser();
51+
ExpressionDeParser expressions = new ExpressionDeParser(selects, output);
52+
if (direct) {
53+
new ValuesStatementDeParser(expressions, output).deParse(values);
54+
} else {
55+
values.accept(new StatementDeParser(expressions, selects, output), null);
56+
}
57+
Values reparsed = assertInstanceOf(Values.class, parse(output.toString()));
58+
assertEquals(original, reparsed.toString());
59+
assertEquals(values.getWithItemsList().size(), reparsed.getWithItemsList().size());
60+
assertEquals(original, values.toString());
61+
}
62+
}
63+
64+
@ParameterizedTest
65+
@ValueSource(strings = {"statement", "select", "expression", "direct", "directWithSelect"})
66+
void preservesCustomVisitorsAndContext(String entryPoint) throws Exception {
67+
Values values = assertInstanceOf(Values.class,
68+
parse("WITH first_cte AS (VALUES (7)), second_cte AS (VALUES (8)) VALUES (9)"));
69+
String original = values.toString();
70+
Object expectedContext = new Object();
71+
StringBuilder output = new StringBuilder();
72+
List<String> withItems = new ArrayList<>();
73+
List<Long> numbers = new ArrayList<>();
74+
SelectDeParser selects = new SelectDeParser() {
75+
@Override
76+
public <S> StringBuilder visit(WithItem<?> item, S context) {
77+
assertSame(expectedContext, context);
78+
withItems.add(item.getAlias().getName());
79+
return super.visit(item, context);
80+
}
81+
};
82+
ExpressionDeParser expressions = new ExpressionDeParser(selects, output) {
83+
@Override
84+
public <S> StringBuilder visit(LongValue value, S context) {
85+
assertSame(expectedContext, context);
86+
numbers.add(value.getValue());
87+
return output.append(value.getValue() + 100);
88+
}
89+
};
90+
selects.setExpressionVisitor(expressions);
91+
selects.setBuilder(output);
92+
switch (entryPoint) {
93+
case "statement":
94+
values.accept(new StatementDeParser(expressions, selects, output), expectedContext);
95+
break;
96+
case "select":
97+
values.accept((SelectVisitor<StringBuilder>) selects, expectedContext);
98+
break;
99+
case "expression":
100+
values.accept((ExpressionVisitor<StringBuilder>) expressions, expectedContext);
101+
break;
102+
case "direct":
103+
new ValuesStatementDeParser(expressions, output).deParse(values, expectedContext);
104+
break;
105+
default:
106+
new ValuesStatementDeParser(expressions, selects, output)
107+
.deParse(values, expectedContext);
108+
}
109+
assertEquals(List.of("first_cte", "second_cte"), withItems);
110+
assertEquals(List.of(7L, 8L, 9L), numbers);
111+
assertEquals(parse("WITH first_cte AS (VALUES (107)), "
112+
+ "second_cte AS (VALUES (108)) VALUES (109)").toString(),
113+
parse(output.toString()).toString());
114+
assertEquals(original, values.toString());
115+
}
116+
117+
@ParameterizedTest
118+
@ValueSource(strings = {
119+
"SELECT (WITH c(n) AS (VALUES (1)) VALUES ((SELECT n FROM c)))",
120+
"VALUES ((WITH c(n) AS (VALUES (1)) VALUES ((SELECT n FROM c))))",
121+
"SELECT * FROM (WITH c(n) AS (VALUES (1)) VALUES ((SELECT n FROM c))) AS v(n)"
122+
})
123+
void rendersNestedValuesWithItemsOnce(String sql) throws Exception {
124+
Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true,
125+
parser -> parser.withDialect(Dialect.POSTGRESQL));
126+
StringBuilder output = new StringBuilder();
127+
statement.accept(new StatementDeParser(output), null);
128+
assertEquals(statement.toString(), parse(output.toString()).toString());
129+
}
130+
}

0 commit comments

Comments
 (0)