Skip to content

Commit f54a22a

Browse files
authored
fix: share SET assignment parsing and support MySQL character set selection (#2661)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent da969af commit f54a22a

3 files changed

Lines changed: 218 additions & 55 deletions

File tree

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

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ 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. */
30+
public enum AssignmentOperator {
31+
NONE(""), EQUALS("="), COLON_EQUALS(":=");
32+
33+
private final String token;
34+
35+
AssignmentOperator(String token) {
36+
this.token = token;
37+
}
38+
39+
public String getToken() {
40+
return token;
41+
}
42+
}
43+
2944
/** SQL Server options that share the SET option [, option] ON | OFF syntax. */
3045
public enum OnOffOption {
3146
QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL, CURSOR_CLOSE_ON_COMMIT, ARITHABORT, ARITHIGNORE, FMTONLY, NOCOUNT, NOEXEC, NUMERIC_ROUNDABORT, PARSEONLY, ANSI_DEFAULTS, ANSI_NULL_DFLT_OFF, ANSI_NULL_DFLT_ON, ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, FORCEPLAN, SHOWPLAN_ALL, SHOWPLAN_TEXT, SHOWPLAN_XML, IMPLICIT_TRANSACTIONS, REMOTE_PROC_TRANSACTIONS, XACT_ABORT;
@@ -99,8 +114,12 @@ public SetStatement(Object name, ExpressionList<?> value) {
99114
}
100115

101116
public void add(Object name, ExpressionList<?> value, boolean useEqual) {
117+
add(name, value, useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE);
118+
}
119+
120+
public void add(Object name, ExpressionList<?> value, AssignmentOperator operator) {
102121
onOffOptions = null;
103-
values.add(new NameExpr(name, value, useEqual));
122+
values.add(new NameExpr(name, value, operator));
104123
}
105124

106125
public void remove(int idx) {
@@ -112,7 +131,7 @@ public int getCount() {
112131
}
113132

114133
public boolean isUseEqual(int idx) {
115-
return values.get(idx).useEqual;
134+
return values.get(idx).isUseEqual();
116135
}
117136

118137
public boolean isUseEqual() {
@@ -129,10 +148,27 @@ public SetStatement withUseEqual(int idx, boolean useEqual) {
129148
}
130149

131150
public SetStatement setUseEqual(int idx, boolean useEqual) {
132-
values.get(idx).useEqual = useEqual;
151+
values.get(idx).setUseEqual(useEqual);
133152
return this;
134153
}
135154

155+
public AssignmentOperator getAssignmentOperator(int idx) {
156+
return values.get(idx).operator;
157+
}
158+
159+
public AssignmentOperator getAssignmentOperator() {
160+
return getAssignmentOperator(0);
161+
}
162+
163+
public SetStatement setAssignmentOperator(int idx, AssignmentOperator operator) {
164+
values.get(idx).operator = Objects.requireNonNull(operator, "operator");
165+
return this;
166+
}
167+
168+
public SetStatement setAssignmentOperator(AssignmentOperator operator) {
169+
return setAssignmentOperator(0, operator);
170+
}
171+
136172
public SetStatement withUseEqual(boolean useEqual) {
137173
this.setUseEqual(useEqual);
138174
return this;
@@ -194,7 +230,10 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expres
194230

195231
private static void appendAssignment(StringBuilder builder, NameExpr value,
196232
Consumer<Expression> expressionRenderer) {
197-
builder.append(value.name).append(value.useEqual ? " = " : " ");
233+
builder.append(value.name).append(" ");
234+
if (value.operator != AssignmentOperator.NONE) {
235+
builder.append(value.operator.getToken()).append(" ");
236+
}
198237
if (value.expressions != null) {
199238
for (int i = 0; i < value.expressions.size(); i++) {
200239
if (i > 0) {
@@ -251,12 +290,16 @@ public SetStatement withEffectParameter(String effectParameter) {
251290
static class NameExpr implements Serializable {
252291
Object name;
253292
ExpressionList expressions;
254-
boolean useEqual;
293+
AssignmentOperator operator;
255294

256295
public NameExpr(Object name, ExpressionList<?> expressions, boolean useEqual) {
296+
this(name, expressions, useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE);
297+
}
298+
299+
public NameExpr(Object name, ExpressionList<?> expressions, AssignmentOperator operator) {
257300
this.name = name;
258301
this.expressions = expressions;
259-
this.useEqual = useEqual;
302+
this.operator = Objects.requireNonNull(operator, "operator");
260303
}
261304

262305
public Object getName() {
@@ -276,11 +319,11 @@ public void setExpressions(ExpressionList<?> expressions) {
276319
}
277320

278321
public boolean isUseEqual() {
279-
return useEqual;
322+
return operator == AssignmentOperator.EQUALS;
280323
}
281324

282325
public void setUseEqual(boolean useEqual) {
283-
this.useEqual = useEqual;
326+
operator = useEqual ? AssignmentOperator.EQUALS : AssignmentOperator.NONE;
284327
}
285328
}
286329
}

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

Lines changed: 45 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4491,62 +4491,60 @@ SetStatement Set(): {
44914491
}
44924492

44934493
SetStatement SetAssignments(): {
4494-
Object name;
4495-
ExpressionList expList;
4496-
boolean useEqual = false;
4497-
SetStatement set;
4498-
Expression exp = null;
4494+
SetStatement set = new SetStatement();
4495+
Expression exp;
44994496
Token tk = null;
4500-
String effectParameter = null;
45014497
}
45024498
{
4503-
[LOOKAHEAD(3) (tk = <K_LOCAL> | tk = <K_SESSION>) {effectParameter = tk.image; } ]
4499+
[LOOKAHEAD(3) (tk = <K_LOCAL> | tk = <K_SESSION>) { set.setEffectParameter(tk.image); } ]
4500+
SetAssignment(set)
45044501
(
4505-
LOOKAHEAD(2)
4506-
<K_DATETIMELITERAL> <K_ZONE> { name = "Time Zone"; useEqual=false; }
4507-
|
4508-
(
4509-
name = UserVariable() ["=" { useEqual=true; } ]
4510-
)
4511-
|
4512-
(
4513-
name = IdentifierChain()
4514-
["=" { useEqual=true; } ]
4515-
)
4516-
)
4517-
exp=Expression()
4518-
{
4519-
expList = new ExpressionList();
4520-
expList.add(exp);
4521-
set = new SetStatement(name, expList)
4522-
.withUseEqual(useEqual)
4523-
.withEffectParameter(effectParameter);
4524-
}
4525-
4526-
(
4527-
{ useEqual=false; }
45284502
","
4529-
(LOOKAHEAD(3)
4530-
(
4531-
( LOOKAHEAD(2)
4532-
<K_DATETIMELITERAL> <K_ZONE> { name = "Time Zone"; useEqual=false; }
4533-
|
4534-
(name = RelObjectName() ["=" { useEqual=true; } ])
4535-
)
4536-
exp=Expression()
4537-
{
4538-
expList = new ExpressionList();
4539-
expList.add(exp);
4540-
set.add(name, expList, useEqual);
4541-
}
4542-
)
4543-
|
4544-
exp=Expression() { expList.add(exp); }
4545-
)
4503+
( LOOKAHEAD(3) SetAssignment(set)
4504+
| exp=Expression() { set.getExpressions(set.getCount() - 1).add(exp); } )
45464505
)*
45474506
{ return set; }
45484507
}
45494508

4509+
/** The first and subsequent assignments use identical names and operators. */
4510+
void SetAssignment(SetStatement set):
4511+
{
4512+
Object name;
4513+
Expression expression;
4514+
SetStatement.AssignmentOperator operator = SetStatement.AssignmentOperator.NONE;
4515+
}
4516+
{
4517+
(
4518+
LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
4519+
|| (isKeywordAhead("CHARSET") && !"=".equals(getToken(2).image)
4520+
&& !":=".equals(getToken(2).image)) }) SetCharacterSet(set)
4521+
|
4522+
( LOOKAHEAD(2) <K_DATETIMELITERAL> <K_ZONE> { name = "Time Zone"; }
4523+
| name=UserVariable()
4524+
| name=IdentifierChain() )
4525+
[ "=" { operator = SetStatement.AssignmentOperator.EQUALS; }
4526+
| ":=" { operator = SetStatement.AssignmentOperator.COLON_EQUALS; } ]
4527+
expression=Expression()
4528+
{ set.add(name, new ExpressionList<Expression>(expression), operator); }
4529+
)
4530+
}
4531+
4532+
/** Character set selection has a single name or DEFAULT, not an arbitrary expression. */
4533+
void SetCharacterSet(SetStatement set):
4534+
{
4535+
String name;
4536+
Token value;
4537+
Expression expression;
4538+
}
4539+
{
4540+
( <K_CHARACTER> <K_SET> { name = "CHARACTER SET"; }
4541+
| <S_IDENTIFIER> { name = "CHARSET"; } )
4542+
( value=<S_CHAR_LITERAL> { expression = new StringValue(value.image); }
4543+
| (value=<S_IDENTIFIER> | value=<S_QUOTED_IDENTIFIER> | value=<K_DEFAULT>)
4544+
{ expression = new Column(value.image); } )
4545+
{ set.add(name, new ExpressionList<Expression>(expression), false); }
4546+
}
4547+
45504548
SetStatement SqlServerSetOnOffOptions(): {
45514549
SetStatement set = new SetStatement();
45524550
List<SetStatement.OnOffOption> options = new ArrayList<SetStatement.OnOffOption>();
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 net.sf.jsqlparser.expression.LongValue;
13+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
14+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
15+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
16+
import net.sf.jsqlparser.statement.SetStatement.AssignmentOperator;
17+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
18+
import net.sf.jsqlparser.util.deparser.SelectDeParser;
19+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.ValueSource;
23+
24+
import java.util.List;
25+
26+
import static org.junit.jupiter.api.Assertions.*;
27+
28+
class MySqlSetStatementTest {
29+
private SetStatement parse(String sql) throws Exception {
30+
return (SetStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL));
31+
}
32+
33+
@ParameterizedTest
34+
@ValueSource(strings = {"SET @a := 1", "SET @a=1, @b:=2", "SET @a:=1, @b=2, @c:=3",
35+
"SET @@session.sql_mode := ''", "SET SESSION sql_mode = ''",
36+
"SET @a = (@b := 2), @c := 3", "SET CHARACTER SET utf8mb4",
37+
"SET CHARACTER SET 'utf8mb4'", "SET CHARACTER SET DEFAULT", "SET CHARSET utf8mb4",
38+
"SET CHARSET DEFAULT", "SET NAMES utf8mb4 COLLATE utf8mb4_bin",
39+
"SET CHARSET DEFAULT, @a=1", "SET @a=1, CHARACTER SET utf8mb4"})
40+
void preservesSetSyntax(String sql) throws Exception {
41+
SetStatement statement = parse(sql);
42+
StringBuilder output = new StringBuilder();
43+
statement.accept(new StatementDeParser(output), null);
44+
assertEquals(statement.toString(), output.toString());
45+
assertEquals(statement.toString(), parse(output.toString()).toString());
46+
}
47+
48+
@Test
49+
void exposesEachAssignmentAndOperator() throws Exception {
50+
SetStatement statement = parse("SET @a=1, @b:=2, @c=3");
51+
assertEquals(3, statement.getCount());
52+
assertEquals("@b", statement.getName(1).toString());
53+
assertEquals(AssignmentOperator.COLON_EQUALS, statement.getAssignmentOperator(1));
54+
assertEquals(1, statement.getExpressions(1).size());
55+
assertInstanceOf(LongValue.class, statement.getExpressions(1).get(0));
56+
statement.setExpressions(1, new ExpressionList<>(new LongValue(9)));
57+
statement.setAssignmentOperator(2, AssignmentOperator.COLON_EQUALS);
58+
assertEquals("SET @a = 1, @b := 9, @c := 3", statement.toString());
59+
statement.setUseEqual(1, true);
60+
assertEquals(AssignmentOperator.EQUALS, statement.getAssignmentOperator(1));
61+
assertTrue(statement.isUseEqual(1));
62+
statement.remove(0);
63+
assertEquals("SET @b = 9, @c := 3", statement.toString());
64+
}
65+
66+
@Test
67+
void exposesCharsetSelectionAlongsideAssignments() throws Exception {
68+
SetStatement statement = parse("SET @a=1, CHARACTER SET utf8mb4, @b:=2");
69+
assertEquals(3, statement.getCount());
70+
assertEquals("CHARACTER SET", statement.getName(1));
71+
assertEquals(AssignmentOperator.NONE, statement.getAssignmentOperator(1));
72+
assertEquals(AssignmentOperator.COLON_EQUALS, statement.getAssignmentOperator(2));
73+
}
74+
75+
@Test
76+
void keepsNestedAssignmentInItsValue() throws Exception {
77+
SetStatement statement = parse("SET @a = (@b := 2), @c := 3");
78+
assertEquals(2, statement.getCount());
79+
assertEquals("@a", statement.getName().toString());
80+
assertEquals(1, statement.getExpressions().size());
81+
assertEquals("(@b := 2)", statement.getExpressions().get(0).toString());
82+
}
83+
84+
@Test
85+
void retainsPostgresqlValueListsAndLegacyApi() throws Exception {
86+
SetStatement statement = (SetStatement) CCJSqlParserUtil.parse(
87+
"SET search_path = public, extensions", p -> p.withDialect(Dialect.POSTGRESQL));
88+
assertEquals(1, statement.getCount());
89+
assertEquals(2, statement.getExpressions().size());
90+
assertEquals("SET search_path = public, extensions", statement.toString());
91+
SetStatement legacy = new SetStatement("x", new ExpressionList<>(new LongValue(1)));
92+
assertEquals(AssignmentOperator.EQUALS, legacy.getAssignmentOperator());
93+
legacy.setUseEqual(false);
94+
assertEquals("SET x 1", legacy.toString());
95+
legacy.setAssignmentOperator(AssignmentOperator.COLON_EQUALS);
96+
assertEquals("SET x := 1", legacy.toString());
97+
}
98+
99+
@Test
100+
void visitsAllAssignmentValues() throws Exception {
101+
List<Long> values = new java.util.ArrayList<>();
102+
ExpressionDeParser expressions = new ExpressionDeParser() {
103+
@Override
104+
public <S> StringBuilder visit(LongValue value, S context) {
105+
values.add(value.getValue());
106+
return getBuilder().append(value.getValue() + 10);
107+
}
108+
};
109+
StringBuilder output = new StringBuilder();
110+
parse("SET @a:=1, @b=2").accept(
111+
new StatementDeParser(expressions, new SelectDeParser(), output), null);
112+
assertEquals(List.of(1L, 2L), values);
113+
assertEquals("SET @a := 11, @b = 12", output.toString());
114+
}
115+
116+
@ParameterizedTest
117+
@ValueSource(strings = {"SET @a :=", "SET @a := 1,", "SET CHARACTER SET",
118+
"SET CHARACTER SET utf8mb4 COLLATE utf8mb4_bin", "SET CHARSET DEFAULT,"})
119+
void rejectsIncompleteAssignmentsAndInvalidCharsetClauses(String sql) {
120+
assertThrows(Exception.class, () -> parse(sql));
121+
}
122+
}

0 commit comments

Comments
 (0)