diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java index e3de9f329..cc82907fe 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java @@ -40,6 +40,7 @@ public enum NationalCharacterType { private String dataType; private List argumentsStringList; private String characterSet; + private boolean useCharsetKeyword; private IntervalQualifier intervalQualifier; private List arrayData = new ArrayList(); private Signedness signedness; @@ -173,6 +174,15 @@ public void setCharacterSet(String characterSet) { this.characterSet = characterSet; } + /** Whether the character set clause uses MySQL's CHARSET abbreviation. */ + public boolean isUseCharsetKeyword() { + return useCharsetKeyword; + } + + public void setUseCharsetKeyword(boolean useCharsetKeyword) { + this.useCharsetKeyword = useCharsetKeyword; + } + public IntervalQualifier getIntervalQualifier() { return intervalQualifier; } @@ -338,7 +348,9 @@ public String toString() { : (signedness != null ? " " + signedness : "") + (zerofill ? " ZEROFILL" : "")) + arraySpec.toString() - + (characterSet != null ? " CHARACTER SET " + characterSet : ""); + + (characterSet != null + ? (useCharsetKeyword ? " CHARSET " : " CHARACTER SET ") + characterSet + : ""); } public ColDataType withDataType(String dataType) { @@ -435,6 +447,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 && Objects.equals(intervalQualifier, that.intervalQualifier) && Objects.equals(arrayData, that.arrayData) && signedness == that.signedness @@ -452,6 +465,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(intervalQualifier); result = 31 * result + Objects.hashCode(arrayData); result = 31 * result + Objects.hashCode(signedness); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 5e6d9c46e..1d2bcf2b7 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -15281,7 +15281,8 @@ ColDataType ColDataType(): ( LOOKAHEAD(1) typeModifier=MySqlTypeModifier() { colDataType.addTypeModifier(typeModifier); } )* [ LOOKAHEAD(2) ( LOOKAHEAD(2) "[" {tk=null;} [ tk= ] { array.add(tk!=null?Integer.valueOf(tk.image):null); } "]" )+ { colDataType.setArrayData(array); } ] - [ LOOKAHEAD(2) (tk= | tk=) { colDataType.setCharacterSet(tk.image); } ] + [ LOOKAHEAD({ (getToken(1).kind == K_CHARACTER && getToken(2).kind == K_SET) + || isKeywordAhead("CHARSET") }) TypeCharacterSet(colDataType) ] { requireDdlSyntax(colDataType.getXmlTypeModifier() == null || argumentsStringList.isEmpty(), @@ -15304,6 +15305,19 @@ ColDataType ColDataType(): } } +/** Shared by column definitions, casts and other uses of ColDataType. */ +void TypeCharacterSet(ColDataType type): +{ + Token name; + boolean abbreviated = false; +} +{ + ( + | LOOKAHEAD({ isKeywordAhead("CHARSET") }) { abbreviated = true; } ) + ( name= | name= | name= | name= ) + { type.setCharacterSet(name.image); type.setUseCharsetKeyword(abbreviated); } +} + XmlTypeModifier XmlTypeModifier(): { XmlTypeModifier.Kind kind = null; diff --git a/src/test/java/net/sf/jsqlparser/statement/create/table/MySqlTypeCharacterSetTest.java b/src/test/java/net/sf/jsqlparser/statement/create/table/MySqlTypeCharacterSetTest.java new file mode 100644 index 000000000..e562fddcc --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/table/MySqlTypeCharacterSetTest.java @@ -0,0 +1,83 @@ +/*- + * #%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.expression.CastExpression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class MySqlTypeCharacterSetTest { + private Statement parse(String sql) throws Exception { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT CAST('xx' AS CHAR(16) CHARSET BINARY)", + "SELECT CAST('xx' AS CHAR(16) CHARACTER SET 'utf8mb4')", + "SELECT CONVERT('xx' USING utf8mb4)", + "CREATE TABLE t (c VARCHAR(20) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin')", + "CREATE TABLE t (c TEXT CHARSET utf8mb4)", + "CREATE TABLE t (c SET('a','b') CHARSET 'utf8mb4')", + "CREATE TABLE t (c BINARY(16) AS (CAST('xx' AS CHAR(16) CHARSET BINARY)))", + "ALTER TABLE t MODIFY c ENUM('a','b') CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'", + "ALTER TABLE t ADD c CHAR(4) CHARSET utf8mb4, ADD d VARCHAR(10) CHARACTER SET utf8mb4" + }) + void sharesTypeSyntaxAcrossCastsAndColumnDefinitions(String sql) throws Exception { + Statement statement = parse(sql); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(statement.toString(), output.toString()); + assertEquals(statement.toString(), parse(output.toString()).toString()); + } + + @Test + void exposesCharsetAndRetainsAbbreviationWhenMutated() throws Exception { + CreateTable table = (CreateTable) parse("CREATE TABLE t (c VARCHAR(20) CHARSET 'utf8mb4')"); + ColDataType type = table.getColumnDefinitions().get(0).getColDataType(); + assertEquals("'utf8mb4'", type.getCharacterSet()); + assertTrue(type.isUseCharsetKeyword()); + type.setCharacterSet("latin1"); + assertEquals("CREATE TABLE t (c VARCHAR (20) CHARSET latin1)", table.toString()); + type.setUseCharsetKeyword(false); + assertEquals("CREATE TABLE t (c VARCHAR (20) CHARACTER SET latin1)", table.toString()); + type.setCharacterSet(null); + assertEquals("CREATE TABLE t (c VARCHAR (20))", table.toString()); + } + + @Test + void preservesDefaultApiSpellingAndTypeEquality() throws Exception { + ColDataType legacy = new ColDataType("CHAR").withCharacterSet("binary"); + assertEquals("CHAR CHARACTER SET binary", legacy.toString()); + PlainSelect select = (PlainSelect) parse("SELECT CAST('a' AS CHAR CHARACTER SET binary)"); + ColDataType parsed = + ((CastExpression) select.getSelectItem(0).getExpression()).getColDataType(); + assertEquals(legacy, parsed); + assertEquals(legacy.hashCode(), parsed.hashCode()); + parsed.setUseCharsetKeyword(true); + assertNotEquals(legacy, parsed); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT CAST('x' AS CHAR CHARSET)", + "SELECT CAST('x' AS CHAR CHARACTER SET)", + "SELECT CAST('x' AS CHAR CHARSET = utf8mb4)"}) + void requiresCharsetName(String sql) { + assertThrows(Exception.class, () -> parse(sql)); + } +}