Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,15 @@ public enum NationalCharacterType {
CHAR, VARCHAR
}

/** Spelling of a character set clause; MySQL shorthands also select a character set. */
public enum CharacterSetSyntax {
CHARACTER_SET, CHARSET, ASCII, UNICODE
}

private String dataType;
private List<String> argumentsStringList;
private String characterSet;
private boolean useCharsetKeyword;
private CharacterSetSyntax characterSetSyntax = CharacterSetSyntax.CHARACTER_SET;
private IntervalQualifier intervalQualifier;
private List<Integer> arrayData = new ArrayList<Integer>();
private Signedness signedness;
Expand Down Expand Up @@ -166,21 +171,51 @@ public void setDataType(List<String> list) {
dataType = list.stream().collect(joining("."));
}

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

public void setCharacterSet(String characterSet) {
this.characterSet = characterSet;
if (characterSetSyntax == CharacterSetSyntax.ASCII
|| characterSetSyntax == CharacterSetSyntax.UNICODE) {
characterSetSyntax = CharacterSetSyntax.CHARACTER_SET;
}
}

public CharacterSetSyntax getCharacterSetSyntax() {
return characterSetSyntax;
}

/**
* Selects the clause spelling. ASCII selects latin1 and UNICODE selects ucs2; the explicit
* spellings retain the current character set. Calling {@link #setCharacterSet(String)} after a
* shorthand switches to CHARACTER SET.
*/
public void setCharacterSetSyntax(CharacterSetSyntax characterSetSyntax) {
this.characterSetSyntax = Objects.requireNonNull(characterSetSyntax, "characterSetSyntax");
if (characterSetSyntax == CharacterSetSyntax.ASCII) {
characterSet = "latin1";
} else if (characterSetSyntax == CharacterSetSyntax.UNICODE) {
characterSet = "ucs2";
}
}

public ColDataType withCharacterSetSyntax(CharacterSetSyntax characterSetSyntax) {
setCharacterSetSyntax(characterSetSyntax);
return this;
}

/** Whether the character set clause uses MySQL's CHARSET abbreviation. */
public boolean isUseCharsetKeyword() {
return useCharsetKeyword;
return characterSetSyntax == CharacterSetSyntax.CHARSET;
}

public void setUseCharsetKeyword(boolean useCharsetKeyword) {
this.useCharsetKeyword = useCharsetKeyword;
setCharacterSetSyntax(useCharsetKeyword
? CharacterSetSyntax.CHARSET
: CharacterSetSyntax.CHARACTER_SET);
}

public IntervalQualifier getIntervalQualifier() {
Expand Down Expand Up @@ -348,9 +383,22 @@ public String toString() {
: (signedness != null ? " " + signedness : "")
+ (zerofill ? " ZEROFILL" : ""))
+ arraySpec.toString()
+ (characterSet != null
? (useCharsetKeyword ? " CHARSET " : " CHARACTER SET ") + characterSet
: "");
+ characterSetClause();
}

private String characterSetClause() {
if (characterSet == null) {
return "";
}
switch (characterSetSyntax) {
case ASCII:
case UNICODE:
return " " + characterSetSyntax;
case CHARSET:
return " CHARSET " + characterSet;
default:
return " CHARACTER SET " + characterSet;
}
}

public ColDataType withDataType(String dataType) {
Expand Down Expand Up @@ -447,7 +495,7 @@ public final boolean equals(Object o) {
return dataType.equalsIgnoreCase(that.dataType)
&& Objects.equals(argumentsStringList, that.argumentsStringList)
&& Objects.equals(characterSet, that.characterSet)
&& useCharsetKeyword == that.useCharsetKeyword
&& characterSetSyntax == that.characterSetSyntax
&& Objects.equals(intervalQualifier, that.intervalQualifier)
&& Objects.equals(arrayData, that.arrayData)
&& signedness == that.signedness
Expand All @@ -465,7 +513,7 @@ public int hashCode() {
.reduce(0, (hash, c) -> 31 * hash + c);
result = 31 * result + Objects.hashCode(argumentsStringList);
result = 31 * result + Objects.hashCode(characterSet);
result = 31 * result + Boolean.hashCode(useCharsetKeyword);
result = 31 * result + Objects.hashCode(characterSetSyntax);
result = 31 * result + Objects.hashCode(intervalQualifier);
result = 31 * result + Objects.hashCode(arrayData);
result = 31 * result + Objects.hashCode(signedness);
Expand Down
12 changes: 10 additions & 2 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -15445,8 +15445,16 @@ ColDataType ColDataType():
( LOOKAHEAD(1) typeModifier=MySqlTypeModifier()
{ colDataType.addTypeModifier(typeModifier); } )*
[ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk=<S_LONG> ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ]
[ LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
|| isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType) ]
[
LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET)
|| isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType)
|
LOOKAHEAD({ Dialect.MYSQL.name().equals(getAsString(Feature.dialect))
&& (isKeywordAhead("ASCII") || isKeywordAhead("UNICODE")) })
tk=<S_IDENTIFIER>
{ colDataType.setCharacterSetSyntax(
ColDataType.CharacterSetSyntax.valueOf(tk.image.toUpperCase(Locale.ROOT))); }
]

{
requireDdlSyntax(colDataType.getXmlTypeModifier() == null || argumentsStringList.isEmpty(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.create.table;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.CastExpression;
import net.sf.jsqlparser.expression.JsonFunction;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.create.table.ColDataType.CharacterSetSyntax;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.test.TestUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.*;

class CharacterSetShorthandTest {
@ParameterizedTest
@CsvSource({"ASCII,latin1", "UNICODE,ucs2"})
void shorthandsExposeCharacterSetAndRetainSpelling(String shorthand, String charset)
throws JSQLParserException {
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT CAST('ab' AS CHAR(10) " + shorthand + "), "
+ "JSON_VALUE('{\"v\":\"ab\"}', '$.v' RETURNING CHAR(10) " + shorthand
+ ")",
true, parser -> parser.withDialect(Dialect.MYSQL));
ColDataType castType =
((CastExpression) select.getSelectItem(0).getExpression()).getColDataType();
ColDataType jsonType =
((JsonFunction) select.getSelectItem(1).getExpression()).getReturningType();
for (ColDataType type : new ColDataType[] {castType, jsonType}) {
assertEquals("CHAR", type.getBaseTypeName());
assertEquals(charset, type.getCharacterSet());
assertEquals(CharacterSetSyntax.valueOf(shorthand), type.getCharacterSetSyntax());
}
TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false,
parser -> parser.withDialect(Dialect.MYSQL));
TestUtils.assertSqlCanBeParsedAndDeparsed(
"CREATE TABLE t (j JSON, g CHAR(10) AS (JSON_VALUE(j, '$.v' RETURNING CHAR(10) "
+ shorthand + ")) STORED)",
true, parser -> parser.withDialect(Dialect.MYSQL));
CreateTable create = (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(
"CREATE TABLE t (c CHAR(10) " + shorthand + ")", true,
parser -> parser.withDialect(Dialect.MYSQL));
assertEquals(charset,
create.getColumnDefinitions().get(0).getColDataType().getCharacterSet());
}

@Test
void parsedReturningTypeCanBeChanged() throws JSQLParserException {
String sql = "SELECT JSON_VALUE('{\"v\":\"ab\"}', '$.v' RETURNING CHAR(10) ASCII)";
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql,
parser -> parser.withDialect(Dialect.MYSQL));
ColDataType type =
((JsonFunction) select.getSelectItem(0).getExpression()).getReturningType();
type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE);
TestUtils.assertStatementCanBeDeparsedAs(select, sql.replace("ASCII", "UNICODE"), true);
type.setCharacterSet("utf8mb4");
TestUtils.assertStatementCanBeDeparsedAs(select,
sql.replace("ASCII", "CHARACTER SET utf8mb4"), true);
TestUtils.assertSqlCanBeParsedAndDeparsed(select.toString(), false,
parser -> parser.withDialect(Dialect.MYSQL));
}

@Test
void characterSetSyntaxCanBeConstructedChangedAndCleared() throws JSQLParserException {
ColDataType type = new ColDataType("CHAR").addArgumentsStringList("10")
.withCharacterSetSyntax(CharacterSetSyntax.ASCII);
assertEquals("latin1", type.getCharacterSet());
assertEquals("CHAR (10) ASCII", type.toString());
type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE);
assertEquals("ucs2", type.getCharacterSet());
assertEquals("CHAR (10) UNICODE", type.toString());
type.setCharacterSet("utf8mb4");
assertEquals(CharacterSetSyntax.CHARACTER_SET, type.getCharacterSetSyntax());
assertEquals("CHAR (10) CHARACTER SET utf8mb4", type.toString());
type.setUseCharsetKeyword(true);
assertTrue(type.isUseCharsetKeyword());
type.setCharacterSet("latin1");
assertEquals("CHAR (10) CHARSET latin1", type.toString());
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT CAST('ab' AS " + type + ")", true,
parser -> parser.withDialect(Dialect.MYSQL));
type.setCharacterSetSyntax(CharacterSetSyntax.ASCII);
type.setUseCharsetKeyword(false);
assertEquals("CHAR (10) CHARACTER SET latin1", type.toString());
type.setCharacterSetSyntax(CharacterSetSyntax.UNICODE);
type.setCharacterSet(null);
assertEquals("CHAR (10)", type.toString());
}

@Test
void syntaxParticipatesInEqualityAndHashCode() {
ColDataType ascii =
new ColDataType("CHAR").withCharacterSetSyntax(CharacterSetSyntax.ASCII);
ColDataType same = new ColDataType("char").withCharacterSetSyntax(CharacterSetSyntax.ASCII);
ColDataType explicit = new ColDataType("CHAR").withCharacterSet("latin1");
assertEquals(ascii, same);
assertEquals(ascii.hashCode(), same.hashCode());
assertNotEquals(ascii, explicit);
}

@Test
void shorthandIsMySqlSpecificAndDoesNotReserveIdentifiers() throws JSQLParserException {
for (String shorthand : new String[] {"ASCII", "UNICODE"}) {
String sql = "SELECT CAST('ab' AS CHAR(10) " + shorthand + ")";
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse(sql,
parser -> parser.withDialect(Dialect.POSTGRESQL)));
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT " + shorthand + " FROM t", true,
parser -> parser.withDialect(Dialect.MYSQL));
}
}
}
Loading