Skip to content

Commit c0fa95a

Browse files
authored
feat: model MySQL character set shorthands (#2684)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 52e81a6 commit c0fa95a

3 files changed

Lines changed: 189 additions & 10 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java‎

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ public enum NationalCharacterType {
3737
CHAR, VARCHAR
3838
}
3939

40+
/** Spelling of a character set clause; MySQL shorthands also select a character set. */
41+
public enum CharacterSetSyntax {
42+
CHARACTER_SET, CHARSET, ASCII, UNICODE
43+
}
44+
4045
private String dataType;
4146
private List<String> argumentsStringList;
4247
private String characterSet;
43-
private boolean useCharsetKeyword;
48+
private CharacterSetSyntax characterSetSyntax = CharacterSetSyntax.CHARACTER_SET;
4449
private IntervalQualifier intervalQualifier;
4550
private List<Integer> arrayData = new ArrayList<Integer>();
4651
private Signedness signedness;
@@ -166,21 +171,51 @@ public void setDataType(List<String> list) {
166171
dataType = list.stream().collect(joining("."));
167172
}
168173

174+
/** Returns the character set name, resolving ASCII to latin1 and UNICODE to ucs2. */
169175
public String getCharacterSet() {
170176
return characterSet;
171177
}
172178

173179
public void setCharacterSet(String characterSet) {
174180
this.characterSet = characterSet;
181+
if (characterSetSyntax == CharacterSetSyntax.ASCII
182+
|| characterSetSyntax == CharacterSetSyntax.UNICODE) {
183+
characterSetSyntax = CharacterSetSyntax.CHARACTER_SET;
184+
}
185+
}
186+
187+
public CharacterSetSyntax getCharacterSetSyntax() {
188+
return characterSetSyntax;
189+
}
190+
191+
/**
192+
* Selects the clause spelling. ASCII selects latin1 and UNICODE selects ucs2; the explicit
193+
* spellings retain the current character set. Calling {@link #setCharacterSet(String)} after a
194+
* shorthand switches to CHARACTER SET.
195+
*/
196+
public void setCharacterSetSyntax(CharacterSetSyntax characterSetSyntax) {
197+
this.characterSetSyntax = Objects.requireNonNull(characterSetSyntax, "characterSetSyntax");
198+
if (characterSetSyntax == CharacterSetSyntax.ASCII) {
199+
characterSet = "latin1";
200+
} else if (characterSetSyntax == CharacterSetSyntax.UNICODE) {
201+
characterSet = "ucs2";
202+
}
203+
}
204+
205+
public ColDataType withCharacterSetSyntax(CharacterSetSyntax characterSetSyntax) {
206+
setCharacterSetSyntax(characterSetSyntax);
207+
return this;
175208
}
176209

177210
/** Whether the character set clause uses MySQL's CHARSET abbreviation. */
178211
public boolean isUseCharsetKeyword() {
179-
return useCharsetKeyword;
212+
return characterSetSyntax == CharacterSetSyntax.CHARSET;
180213
}
181214

182215
public void setUseCharsetKeyword(boolean useCharsetKeyword) {
183-
this.useCharsetKeyword = useCharsetKeyword;
216+
setCharacterSetSyntax(useCharsetKeyword
217+
? CharacterSetSyntax.CHARSET
218+
: CharacterSetSyntax.CHARACTER_SET);
184219
}
185220

186221
public IntervalQualifier getIntervalQualifier() {
@@ -348,9 +383,22 @@ public String toString() {
348383
: (signedness != null ? " " + signedness : "")
349384
+ (zerofill ? " ZEROFILL" : ""))
350385
+ arraySpec.toString()
351-
+ (characterSet != null
352-
? (useCharsetKeyword ? " CHARSET " : " CHARACTER SET ") + characterSet
353-
: "");
386+
+ characterSetClause();
387+
}
388+
389+
private String characterSetClause() {
390+
if (characterSet == null) {
391+
return "";
392+
}
393+
switch (characterSetSyntax) {
394+
case ASCII:
395+
case UNICODE:
396+
return " " + characterSetSyntax;
397+
case CHARSET:
398+
return " CHARSET " + characterSet;
399+
default:
400+
return " CHARACTER SET " + characterSet;
401+
}
354402
}
355403

356404
public ColDataType withDataType(String dataType) {
@@ -447,7 +495,7 @@ public final boolean equals(Object o) {
447495
return dataType.equalsIgnoreCase(that.dataType)
448496
&& Objects.equals(argumentsStringList, that.argumentsStringList)
449497
&& Objects.equals(characterSet, that.characterSet)
450-
&& useCharsetKeyword == that.useCharsetKeyword
498+
&& characterSetSyntax == that.characterSetSyntax
451499
&& Objects.equals(intervalQualifier, that.intervalQualifier)
452500
&& Objects.equals(arrayData, that.arrayData)
453501
&& signedness == that.signedness
@@ -465,7 +513,7 @@ public int hashCode() {
465513
.reduce(0, (hash, c) -> 31 * hash + c);
466514
result = 31 * result + Objects.hashCode(argumentsStringList);
467515
result = 31 * result + Objects.hashCode(characterSet);
468-
result = 31 * result + Boolean.hashCode(useCharsetKeyword);
516+
result = 31 * result + Objects.hashCode(characterSetSyntax);
469517
result = 31 * result + Objects.hashCode(intervalQualifier);
470518
result = 31 * result + Objects.hashCode(arrayData);
471519
result = 31 * result + Objects.hashCode(signedness);

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15455,8 +15455,16 @@ ColDataType ColDataType():
1545515455
( LOOKAHEAD(1) typeModifier=MySqlTypeModifier()
1545615456
{ colDataType.addTypeModifier(typeModifier); } )*
1545715457
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ]
15458-
[ LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
15459-
|| isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType) ]
15458+
[
15459+
LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
15460+
|| isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType)
15461+
|
15462+
LOOKAHEAD({ Dialect.MYSQL.name().equals(getAsString(Feature.dialect))
15463+
&& (isKeywordAhead("ASCII") || isKeywordAhead("UNICODE")) })
15464+
tk=<S_IDENTIFIER>
15465+
{ colDataType.setCharacterSetSyntax(
15466+
ColDataType.CharacterSetSyntax.valueOf(tk.image.toUpperCase(Locale.ROOT))); }
15467+
]
1546015468

1546115469
{
1546215470
requireDdlSyntax(colDataType.getXmlTypeModifier() == null || argumentsStringList.isEmpty(),
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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.create.table;
11+
12+
import net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.expression.CastExpression;
14+
import net.sf.jsqlparser.expression.JsonFunction;
15+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
16+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
17+
import net.sf.jsqlparser.statement.create.table.ColDataType.CharacterSetSyntax;
18+
import net.sf.jsqlparser.statement.select.PlainSelect;
19+
import net.sf.jsqlparser.test.TestUtils;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.CsvSource;
23+
24+
import static org.junit.jupiter.api.Assertions.*;
25+
26+
class CharacterSetShorthandTest {
27+
@ParameterizedTest
28+
@CsvSource({"ASCII,latin1", "UNICODE,ucs2"})
29+
void shorthandsExposeCharacterSetAndRetainSpelling(String shorthand, String charset)
30+
throws JSQLParserException {
31+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
32+
"SELECT CAST('ab' AS CHAR(10) " + shorthand + "), "
33+
+ "JSON_VALUE('{\"v\":\"ab\"}', '$.v' RETURNING CHAR(10) " + shorthand
34+
+ ")",
35+
true, parser -> parser.withDialect(Dialect.MYSQL));
36+
ColDataType castType =
37+
((CastExpression) select.getSelectItem(0).getExpression()).getColDataType();
38+
ColDataType jsonType =
39+
((JsonFunction) select.getSelectItem(1).getExpression()).getReturningType();
40+
for (ColDataType type : new ColDataType[] {castType, jsonType}) {
41+
assertEquals("CHAR", type.getBaseTypeName());
42+
assertEquals(charset, type.getCharacterSet());
43+
assertEquals(CharacterSetSyntax.valueOf(shorthand), type.getCharacterSetSyntax());
44+
}
45+
TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false,
46+
parser -> parser.withDialect(Dialect.MYSQL));
47+
TestUtils.assertSqlCanBeParsedAndDeparsed(
48+
"CREATE TABLE t (j JSON, g CHAR(10) AS (JSON_VALUE(j, '$.v' RETURNING CHAR(10) "
49+
+ shorthand + ")) STORED)",
50+
true, parser -> parser.withDialect(Dialect.MYSQL));
51+
CreateTable create = (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(
52+
"CREATE TABLE t (c CHAR(10) " + shorthand + ")", true,
53+
parser -> parser.withDialect(Dialect.MYSQL));
54+
assertEquals(charset,
55+
create.getColumnDefinitions().get(0).getColDataType().getCharacterSet());
56+
}
57+
58+
@Test
59+
void parsedReturningTypeCanBeChanged() throws JSQLParserException {
60+
String sql = "SELECT JSON_VALUE('{\"v\":\"ab\"}', '$.v' RETURNING CHAR(10) ASCII)";
61+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql,
62+
parser -> parser.withDialect(Dialect.MYSQL));
63+
ColDataType type =
64+
((JsonFunction) select.getSelectItem(0).getExpression()).getReturningType();
65+
type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE);
66+
TestUtils.assertStatementCanBeDeparsedAs(select, sql.replace("ASCII", "UNICODE"), true);
67+
type.setCharacterSet("utf8mb4");
68+
TestUtils.assertStatementCanBeDeparsedAs(select,
69+
sql.replace("ASCII", "CHARACTER SET utf8mb4"), true);
70+
TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false,
71+
parser -> parser.withDialect(Dialect.MYSQL));
72+
}
73+
74+
@Test
75+
void characterSetSyntaxCanBeConstructedChangedAndCleared() throws JSQLParserException {
76+
ColDataType type = new ColDataType("CHAR").addArgumentsStringList("10")
77+
.withCharacterSetSyntax(CharacterSetSyntax.ASCII);
78+
assertEquals("latin1", type.getCharacterSet());
79+
assertEquals("CHAR (10) ASCII", type.toString());
80+
type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE);
81+
assertEquals("ucs2", type.getCharacterSet());
82+
assertEquals("CHAR (10) UNICODE", type.toString());
83+
type.setCharacterSet("utf8mb4");
84+
assertEquals(CharacterSetSyntax.CHARACTER_SET, type.getCharacterSetSyntax());
85+
assertEquals("CHAR (10) CHARACTER SET utf8mb4", type.toString());
86+
type.setUseCharsetKeyword(true);
87+
assertTrue(type.isUseCharsetKeyword());
88+
type.setCharacterSet("latin1");
89+
assertEquals("CHAR (10) CHARSET latin1", type.toString());
90+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT CAST('ab' AS " + type + ")", true,
91+
parser -> parser.withDialect(Dialect.MYSQL));
92+
type.setCharacterSetSyntax(CharacterSetSyntax.ASCII);
93+
type.setUseCharsetKeyword(false);
94+
assertEquals("CHAR (10) CHARACTER SET latin1", type.toString());
95+
type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE);
96+
type.setCharacterSet(null);
97+
assertEquals("CHAR (10)", type.toString());
98+
}
99+
100+
@Test
101+
void syntaxParticipatesInEqualityAndHashCode() {
102+
ColDataType ascii =
103+
new ColDataType("CHAR").withCharacterSetSyntax(CharacterSetSyntax.ASCII);
104+
ColDataType same = new ColDataType("char").withCharacterSetSyntax(CharacterSetSyntax.ASCII);
105+
ColDataType explicit = new ColDataType("CHAR").withCharacterSet("latin1");
106+
assertEquals(ascii, same);
107+
assertEquals(ascii.hashCode(), same.hashCode());
108+
assertNotEquals(ascii, explicit);
109+
}
110+
111+
@Test
112+
void shorthandIsMySqlSpecificAndDoesNotReserveIdentifiers() throws JSQLParserException {
113+
for (String shorthand : new String[] {"ASCII", "UNICODE"}) {
114+
String sql = "SELECT CAST('ab' AS CHAR(10) " + shorthand + ")";
115+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
116+
assertThrows(JSQLParserException.class,
117+
() -> CCJSqlParserUtil.parse(sql,
118+
parser -> parser.withDialect(Dialect.POSTGRESQL)));
119+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT " + shorthand + " FROM t", true,
120+
parser -> parser.withDialect(Dialect.MYSQL));
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)