Skip to content

Commit 4136d51

Browse files
authored
fix: share MySQL character set parsing across column types and casts (#2662)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent f54a22a commit 4136d51

3 files changed

Lines changed: 113 additions & 2 deletions

File tree

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public enum NationalCharacterType {
4040
private String dataType;
4141
private List<String> argumentsStringList;
4242
private String characterSet;
43+
private boolean useCharsetKeyword;
4344
private IntervalQualifier intervalQualifier;
4445
private List<Integer> arrayData = new ArrayList<Integer>();
4546
private Signedness signedness;
@@ -173,6 +174,15 @@ public void setCharacterSet(String characterSet) {
173174
this.characterSet = characterSet;
174175
}
175176

177+
/** Whether the character set clause uses MySQL's CHARSET abbreviation. */
178+
public boolean isUseCharsetKeyword() {
179+
return useCharsetKeyword;
180+
}
181+
182+
public void setUseCharsetKeyword(boolean useCharsetKeyword) {
183+
this.useCharsetKeyword = useCharsetKeyword;
184+
}
185+
176186
public IntervalQualifier getIntervalQualifier() {
177187
return intervalQualifier;
178188
}
@@ -338,7 +348,9 @@ public String toString() {
338348
: (signedness != null ? " " + signedness : "")
339349
+ (zerofill ? " ZEROFILL" : ""))
340350
+ arraySpec.toString()
341-
+ (characterSet != null ? " CHARACTER SET " + characterSet : "");
351+
+ (characterSet != null
352+
? (useCharsetKeyword ? " CHARSET " : " CHARACTER SET ") + characterSet
353+
: "");
342354
}
343355

344356
public ColDataType withDataType(String dataType) {
@@ -435,6 +447,7 @@ public final boolean equals(Object o) {
435447
return dataType.equalsIgnoreCase(that.dataType)
436448
&& Objects.equals(argumentsStringList, that.argumentsStringList)
437449
&& Objects.equals(characterSet, that.characterSet)
450+
&& useCharsetKeyword == that.useCharsetKeyword
438451
&& Objects.equals(intervalQualifier, that.intervalQualifier)
439452
&& Objects.equals(arrayData, that.arrayData)
440453
&& signedness == that.signedness
@@ -452,6 +465,7 @@ public int hashCode() {
452465
.reduce(0, (hash, c) -> 31 * hash + c);
453466
result = 31 * result + Objects.hashCode(argumentsStringList);
454467
result = 31 * result + Objects.hashCode(characterSet);
468+
result = 31 * result + Boolean.hashCode(useCharsetKeyword);
455469
result = 31 * result + Objects.hashCode(intervalQualifier);
456470
result = 31 * result + Objects.hashCode(arrayData);
457471
result = 31 * result + Objects.hashCode(signedness);

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15358,7 +15358,8 @@ ColDataType ColDataType():
1535815358
( LOOKAHEAD(1) typeModifier=MySqlTypeModifier()
1535915359
{ colDataType.addTypeModifier(typeModifier); } )*
1536015360
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ]
15361-
[ LOOKAHEAD(2) <K_CHARACTER> <K_SET> (tk=<S_IDENTIFIER> | tk=<K_BINARY>) { colDataType.setCharacterSet(tk.image); } ]
15361+
[ LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
15362+
|| isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType) ]
1536215363

1536315364
{
1536415365
requireDdlSyntax(colDataType.getXmlTypeModifier() == null || argumentsStringList.isEmpty(),
@@ -15381,6 +15382,19 @@ ColDataType ColDataType():
1538115382
}
1538215383
}
1538315384

15385+
/** Shared by column definitions, casts and other uses of ColDataType. */
15386+
void TypeCharacterSet(ColDataType type):
15387+
{
15388+
Token name;
15389+
boolean abbreviated = false;
15390+
}
15391+
{
15392+
( <K_CHARACTER> <K_SET>
15393+
| LOOKAHEAD({ isKeywordAhead("CHARSET") }) <S_IDENTIFIER> { abbreviated = true; } )
15394+
( name=<S_IDENTIFIER> | name=<K_BINARY> | name=<S_CHAR_LITERAL> | name=<S_QUOTED_IDENTIFIER> )
15395+
{ type.setCharacterSet(name.image); type.setUseCharsetKeyword(abbreviated); }
15396+
}
15397+
1538415398
XmlTypeModifier XmlTypeModifier():
1538515399
{
1538615400
XmlTypeModifier.Kind kind = null;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.expression.CastExpression;
13+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
14+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
15+
import net.sf.jsqlparser.statement.Statement;
16+
import net.sf.jsqlparser.statement.select.PlainSelect;
17+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.ValueSource;
21+
22+
import static org.junit.jupiter.api.Assertions.*;
23+
24+
class MySqlTypeCharacterSetTest {
25+
private Statement parse(String sql) throws Exception {
26+
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL));
27+
}
28+
29+
@ParameterizedTest
30+
@ValueSource(strings = {
31+
"SELECT CAST('xx' AS CHAR(16) CHARSET BINARY)",
32+
"SELECT CAST('xx' AS CHAR(16) CHARACTER SET 'utf8mb4')",
33+
"SELECT CONVERT('xx' USING utf8mb4)",
34+
"CREATE TABLE t (c VARCHAR(20) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin')",
35+
"CREATE TABLE t (c TEXT CHARSET utf8mb4)",
36+
"CREATE TABLE t (c SET('a','b') CHARSET 'utf8mb4')",
37+
"CREATE TABLE t (c BINARY(16) AS (CAST('xx' AS CHAR(16) CHARSET BINARY)))",
38+
"ALTER TABLE t MODIFY c ENUM('a','b') CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'",
39+
"ALTER TABLE t ADD c CHAR(4) CHARSET utf8mb4, ADD d VARCHAR(10) CHARACTER SET utf8mb4"
40+
})
41+
void sharesTypeSyntaxAcrossCastsAndColumnDefinitions(String sql) throws Exception {
42+
Statement statement = parse(sql);
43+
StringBuilder output = new StringBuilder();
44+
statement.accept(new StatementDeParser(output), null);
45+
assertEquals(statement.toString(), output.toString());
46+
assertEquals(statement.toString(), parse(output.toString()).toString());
47+
}
48+
49+
@Test
50+
void exposesCharsetAndRetainsAbbreviationWhenMutated() throws Exception {
51+
CreateTable table = (CreateTable) parse("CREATE TABLE t (c VARCHAR(20) CHARSET 'utf8mb4')");
52+
ColDataType type = table.getColumnDefinitions().get(0).getColDataType();
53+
assertEquals("'utf8mb4'", type.getCharacterSet());
54+
assertTrue(type.isUseCharsetKeyword());
55+
type.setCharacterSet("latin1");
56+
assertEquals("CREATE TABLE t (c VARCHAR (20) CHARSET latin1)", table.toString());
57+
type.setUseCharsetKeyword(false);
58+
assertEquals("CREATE TABLE t (c VARCHAR (20) CHARACTER SET latin1)", table.toString());
59+
type.setCharacterSet(null);
60+
assertEquals("CREATE TABLE t (c VARCHAR (20))", table.toString());
61+
}
62+
63+
@Test
64+
void preservesDefaultApiSpellingAndTypeEquality() throws Exception {
65+
ColDataType legacy = new ColDataType("CHAR").withCharacterSet("binary");
66+
assertEquals("CHAR CHARACTER SET binary", legacy.toString());
67+
PlainSelect select = (PlainSelect) parse("SELECT CAST('a' AS CHAR CHARACTER SET binary)");
68+
ColDataType parsed =
69+
((CastExpression) select.getSelectItem(0).getExpression()).getColDataType();
70+
assertEquals(legacy, parsed);
71+
assertEquals(legacy.hashCode(), parsed.hashCode());
72+
parsed.setUseCharsetKeyword(true);
73+
assertNotEquals(legacy, parsed);
74+
}
75+
76+
@ParameterizedTest
77+
@ValueSource(strings = {"SELECT CAST('x' AS CHAR CHARSET)",
78+
"SELECT CAST('x' AS CHAR CHARACTER SET)",
79+
"SELECT CAST('x' AS CHAR CHARSET = utf8mb4)"})
80+
void requiresCharsetName(String sql) {
81+
assertThrows(Exception.class, () -> parse(sql));
82+
}
83+
}

0 commit comments

Comments
 (0)