Skip to content

Commit 2b3bebe

Browse files
authored
Support PostgreSQL SET TO using typed assignment operators (#2730)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent fb1e42a commit 2b3bebe

9 files changed

Lines changed: 228 additions & 8 deletions

File tree

‎src/main/java/net/sf/jsqlparser/parser/feature/Feature.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,8 @@ public enum Feature {
708708
* @see SetStatement
709709
*/
710710
set,
711+
/** PostgreSQL SET configuration_parameter TO value. */
712+
setAssignmentTo,
711713
/** SQL Server SET option [, option] ON | OFF. */
712714
sqlServerSetOptions,
713715
/**

‎src/main/java/net/sf/jsqlparser/statement/SetStatement.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ public final class SetStatement implements Statement {
2626
private String effectParameter;
2727
private OnOffOptions onOffOptions;
2828

29-
/** Assignment punctuation; NONE also supports SET options and PostgreSQL value lists. */
29+
/** Assignment separator; NONE also supports SET options and PostgreSQL TIME ZONE. */
3030
public enum AssignmentOperator {
31-
NONE(""), EQUALS("="), COLON_EQUALS(":=");
31+
NONE(""), EQUALS("="), COLON_EQUALS(":="), TO("TO");
3232

3333
private final String token;
3434

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import net.sf.jsqlparser.statement.create.subscription.CreateSubscription;
2929
import net.sf.jsqlparser.statement.alter.AlterSubscription;
3030

31+
import net.sf.jsqlparser.expression.Expression;
3132
import net.sf.jsqlparser.expression.ExpressionVisitor;
3233
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
3334
import net.sf.jsqlparser.schema.Column;
@@ -456,7 +457,13 @@ public <S> T visit(CreatePolicy createPolicy, S context) {
456457

457458
@Override
458459
public <S> T visit(SetStatement set, S context) {
459-
460+
for (int i = 0; i < set.getCount(); i++) {
461+
if (set.getExpressions(i) != null) {
462+
for (Expression expression : set.getExpressions(i)) {
463+
expressionVisitor.visitExpression(expression, context);
464+
}
465+
}
466+
}
460467
return null;
461468
}
462469

‎src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public enum PostgresqlVersion implements Version {
152152
Feature.grant,
153153
Feature.revoke, Feature.createRole, Feature.alterRole,
154154
Feature.alterDefaultPrivileges, Feature.createTrigger, // https://www.postgresql.org/docs/current/sql-set.html
155-
Feature.set, // https://www.postgresql.org/docs/current/sql-reset.html
155+
Feature.set, Feature.setAssignmentTo, // https://www.postgresql.org/docs/current/sql-reset.html
156156
Feature.reset, // https://www.postgresql.org/docs/current/sql-commit.html
157157
Feature.commit)), V11("11", V10.copy().getFeatures()), V12("12",
158158
V11.copy().getFeatures()), V13("13",

‎src/main/java/net/sf/jsqlparser/util/validation/validator/SetStatementValidator.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public void validate(SetStatement set) {
2626
if (set.getOnOffOptions() != null) {
2727
validateFeature(c, Feature.sqlServerSetOptions);
2828
}
29+
for (int i = 0; i < set.getCount(); i++) {
30+
if (set.getAssignmentOperator(i) == SetStatement.AssignmentOperator.TO) {
31+
validateFeature(c, Feature.setAssignmentTo);
32+
}
33+
}
2934
}
3035
for (int i = 0; i < set.getCount(); i++) {
3136
validateOptionalExpressions(set.getExpressions(i));

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4770,10 +4770,40 @@ void SetAssignment(SetStatement set):
47704770
| name=UserVariable()
47714771
| name=IdentifierChain() )
47724772
[ "=" { operator = SetStatement.AssignmentOperator.EQUALS; }
4773-
| ":=" { operator = SetStatement.AssignmentOperator.COLON_EQUALS; } ]
4774-
expression=Expression()
4775-
{ set.add(name, new ExpressionList<Expression>(expression), operator); }
4773+
| ":=" { operator = SetStatement.AssignmentOperator.COLON_EQUALS; }
4774+
| LOOKAHEAD({ getToken(1).kind == K_TO && (getAsString(Feature.dialect) == null
4775+
|| Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) })
4776+
<K_TO> { operator = SetStatement.AssignmentOperator.TO; } ]
4777+
expression=SetAssignmentValue()
4778+
{
4779+
if (operator == SetStatement.AssignmentOperator.TO
4780+
&& (name instanceof UserVariable || "Time Zone".equals(name))) {
4781+
throw new ParseException("SET TO requires a configuration parameter name");
4782+
}
4783+
set.add(name, new ExpressionList<Expression>(expression), operator);
4784+
}
47764785
)
4786+
{
4787+
for (int i = 0; i < set.getCount(); i++) {
4788+
if (set.getAssignmentOperator(i) == SetStatement.AssignmentOperator.TO
4789+
&& set.getCount() != 1) {
4790+
throw new ParseException("SET TO accepts one configuration parameter with a value list");
4791+
}
4792+
}
4793+
}
4794+
}
4795+
4796+
/** ON is a setting value even when the preceding separator is TO. */
4797+
Expression SetAssignmentValue():
4798+
{
4799+
Token value;
4800+
Expression expression;
4801+
}
4802+
{
4803+
( LOOKAHEAD({ getToken(1).kind == K_ON && getToken(0).kind == K_TO })
4804+
value=<K_ON> { expression = new Column(value.image); }
4805+
| expression=Expression() )
4806+
{ return expression; }
47774807
}
47784808

47794809
/** Character set selection has a single name or DEFAULT, not an arbitrary expression. */
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertFalse;
14+
import static org.junit.jupiter.api.Assertions.assertThrows;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import net.sf.jsqlparser.JSQLParserException;
20+
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
21+
import net.sf.jsqlparser.expression.LongValue;
22+
import net.sf.jsqlparser.expression.StringValue;
23+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
24+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
25+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
26+
import net.sf.jsqlparser.statement.SetStatement.AssignmentOperator;
27+
import net.sf.jsqlparser.util.deparser.SelectDeParser;
28+
import net.sf.jsqlparser.statement.select.SelectVisitorAdapter;
29+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
30+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
31+
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.params.ParameterizedTest;
33+
import org.junit.jupiter.params.provider.ValueSource;
34+
35+
class PostgreSqlSetToTest {
36+
private static SetStatement parse(String sql) throws JSQLParserException {
37+
return (SetStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
38+
}
39+
40+
@ParameterizedTest
41+
@ValueSource(strings = {"SET search_path TO my_schema, public",
42+
"SET LOCAL statement_timeout TO 5000", "SET SESSION application_name TO 'worker'",
43+
"SET datestyle TO postgres, dmy", "SET client_encoding TO DEFAULT",
44+
"SET app.user_name TO 'reader'", "SET \"TimeZone\" TO 'UTC'",
45+
"SET standard_conforming_strings TO on", "SET enable_seqscan TO off",
46+
"SET enable_hashjoin TO true", "SET enable_mergejoin TO false",
47+
"SET search_path TO off, on"})
48+
void parsesScopeNamesAndValueLists(String sql) throws Exception {
49+
SetStatement set = parse(sql);
50+
assertEquals(1, set.getCount());
51+
assertEquals(AssignmentOperator.TO, set.getAssignmentOperator());
52+
assertFalse(set.isUseEqual());
53+
assertRoundTrip(set, sql);
54+
SetStatement generic = (SetStatement) CCJSqlParserUtil.parse(sql);
55+
assertEquals(AssignmentOperator.TO, generic.getAssignmentOperator());
56+
}
57+
58+
@Test
59+
void distinguishesOneValueListFromSeveralAssignments() throws Exception {
60+
SetStatement set = parse("SET LOCAL search_path TO my_schema, public, '$user'");
61+
assertEquals("LOCAL", set.getEffectParameter());
62+
assertEquals("search_path", set.getName());
63+
assertEquals(1, set.getCount());
64+
assertEquals(3, set.getExpressions().size());
65+
assertEquals("my_schema", set.getExpressions().get(0).toString());
66+
assertEquals("public", set.getExpressions().get(1).toString());
67+
assertEquals("$user", ((StringValue) set.getExpressions().get(2)).getValue());
68+
}
69+
70+
@Test
71+
void keepsLegacyAssignmentApiAndSeparators() throws Exception {
72+
SetStatement constructed = new SetStatement("x", new ExpressionList<>(new LongValue(1)));
73+
assertTrue(constructed.isUseEqual());
74+
constructed.setAssignmentOperator(AssignmentOperator.TO);
75+
assertRoundTrip(constructed, "SET x TO 1");
76+
assertFalse(constructed.isUseEqual());
77+
constructed.setUseEqual(true);
78+
assertEquals("SET x = 1", constructed.toString());
79+
constructed.setUseEqual(false);
80+
assertEquals("SET x 1", constructed.toString());
81+
SetStatement assignments = (SetStatement) CCJSqlParserUtil.parse("SET @a=1, @b:=2, @c=3",
82+
p -> p.withDialect(Dialect.MYSQL));
83+
assertEquals(3, assignments.getCount());
84+
assertEquals(AssignmentOperator.EQUALS, assignments.getAssignmentOperator(0));
85+
assertEquals(AssignmentOperator.COLON_EQUALS, assignments.getAssignmentOperator(1));
86+
assertEquals("SET @a = 1, @b := 2, @c = 3", assignments.toString());
87+
assertRoundTrip(parse("SET TIME ZONE 'UTC'"), "SET Time Zone 'UTC'");
88+
}
89+
90+
@Test
91+
void keepsExistingOnExpressionsOutsideToAssignments() throws Exception {
92+
for (String sql : List.of("SET v = on + 1", "SET v = 1, on")) {
93+
for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.MYSQL, Dialect.SQLSERVER)) {
94+
SetStatement set = (SetStatement) CCJSqlParserUtil.parse(sql,
95+
p -> p.withDialect(dialect));
96+
assertEquals(AssignmentOperator.EQUALS, set.getAssignmentOperator());
97+
assertRoundTrip(set, sql);
98+
}
99+
}
100+
}
101+
102+
@Test
103+
void visitsValuesInToEqualsAndSpaceForms() throws Exception {
104+
List<Long> values = new ArrayList<>();
105+
StatementVisitorAdapter<Void> visitor = new StatementVisitorAdapter<>(
106+
new SelectVisitorAdapter<>(new ExpressionVisitorAdapter<Void>() {
107+
@Override
108+
public <S> Void visit(LongValue value, S context) {
109+
values.add(value.getValue());
110+
return null;
111+
}
112+
}));
113+
parse("SET x TO 1, 2").accept(visitor, null);
114+
CCJSqlParserUtil.parse("SET x = 3, y 4").accept(visitor, null);
115+
assertEquals(List.of(1L, 2L, 3L, 4L), values);
116+
StringBuilder output = new StringBuilder();
117+
ExpressionDeParser expressions = new ExpressionDeParser() {
118+
@Override
119+
public <S> StringBuilder visit(LongValue value, S context) {
120+
return getBuilder().append(value.getValue() + 10);
121+
}
122+
};
123+
parse("SET x TO 1, 2").accept(
124+
new StatementDeParser(expressions, new SelectDeParser(), output), null);
125+
assertEquals("SET x TO 11, 12", output.toString());
126+
}
127+
128+
@Test
129+
void retainsFollowingStatements() throws Exception {
130+
Statements statements = CCJSqlParserUtil.parseStatements(
131+
"SET LOCAL search_path TO my_schema, public; SELECT 1; SET x = 3;",
132+
p -> p.withDialect(Dialect.POSTGRESQL));
133+
assertEquals(3, statements.size());
134+
assertEquals(AssignmentOperator.TO,
135+
((SetStatement) statements.get(0)).getAssignmentOperator());
136+
assertEquals(AssignmentOperator.EQUALS,
137+
((SetStatement) statements.get(2)).getAssignmentOperator());
138+
}
139+
140+
@ParameterizedTest
141+
@ValueSource(strings = {"SET search_path TO", "SET search_path TO public,",
142+
"SET x TO = 1", "SET x TO 1, y TO 2", "SET x = 1, y TO 2",
143+
"SET @x TO 1", "SET TIME ZONE TO 'UTC'"})
144+
void rejectsMalformedOrMultipleParameterToAssignments(String sql) {
145+
assertThrows(JSQLParserException.class, () -> parse(sql));
146+
}
147+
148+
@Test
149+
void doesNotEnableToForExplicitMySqlDialect() {
150+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("SET x TO 1",
151+
p -> p.withDialect(Dialect.MYSQL)));
152+
}
153+
154+
private static void assertRoundTrip(SetStatement set, String expected) throws Exception {
155+
assertEquals(expected, set.toString());
156+
StringBuilder output = new StringBuilder();
157+
set.accept(new StatementDeParser(output), null);
158+
assertEquals(expected, output.toString());
159+
SetStatement reparsed = parse(output.toString());
160+
assertEquals(set.getAssignmentOperator(), reparsed.getAssignmentOperator());
161+
assertEquals(set.getEffectParameter(), reparsed.getEffectParameter());
162+
assertEquals(set.getName(), reparsed.getName());
163+
assertEquals(set.getExpressions().toString(), reparsed.getExpressions().toString());
164+
}
165+
}

‎src/test/java/net/sf/jsqlparser/statement/UnsupportedStatementTest.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ void testInformixSetStatementIssue1945() throws JSQLParserException {
200200
void testRedshiftSetStatementIssue1708() throws JSQLParserException {
201201
Statement st = TestUtils.assertSqlCanBeParsedAndDeparsed(
202202
"SET x TO y;", true, parser -> parser.withUnsupportedStatements());
203-
assertInstanceOf(UnsupportedStatement.class, st);
203+
SetStatement set = assertInstanceOf(SetStatement.class, st);
204+
assertEquals(SetStatement.AssignmentOperator.TO, set.getAssignmentOperator());
204205
}
205206
}

‎src/test/java/net/sf/jsqlparser/util/validation/validator/SetStatementValidatorTest.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,22 @@
1111

1212
import java.util.Arrays;
1313
import net.sf.jsqlparser.JSQLParserException;
14+
import net.sf.jsqlparser.parser.feature.Feature;
1415
import net.sf.jsqlparser.util.validation.ValidationTestAsserts;
1516
import net.sf.jsqlparser.util.validation.feature.DatabaseType;
17+
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
1618
import org.junit.jupiter.api.Test;
1719

1820
public class SetStatementValidatorTest extends ValidationTestAsserts {
1921

22+
@Test
23+
public void validatesToSeparatelyFromTheGeneralSetFeature() {
24+
String sql = "SET LOCAL search_path TO my_schema, public";
25+
validateNoErrors(sql, 1, DatabaseType.POSTGRESQL);
26+
validateNotAllowed(sql, 1, 1, new FeaturesAllowed("set-only", Feature.set),
27+
Feature.setAssignmentTo);
28+
}
29+
2030
@Test
2131
public void testValidateSet() throws JSQLParserException {
2232
for (String sql : Arrays.asList(

0 commit comments

Comments
 (0)