diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java index 0f654326f..3d764b374 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/PartitionDefinition.java @@ -11,6 +11,7 @@ import java.io.Serializable; import java.util.List; +import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.statement.select.PlainSelect; public class PartitionDefinition implements Serializable { @@ -39,7 +40,7 @@ public String toString() { private boolean subPartition; private boolean storageKeyword; private boolean storageEngineUseEquals = true; - private String comment; + private StringValue commentValue; private boolean commentUseEquals; private String dataDirectory; private boolean dataDirectoryUseEquals; @@ -143,12 +144,26 @@ public void setStorageEngineUseEquals(boolean storageEngineUseEquals) { this.storageEngineUseEquals = storageEngineUseEquals; } + /** Returns the COMMENT literal's SQL text, for example {@code 'it''s'}, or null. */ public String getComment() { - return comment; + return commentValue != null ? commentValue.toString() : null; } + /** + * Sets the COMMENT from a string literal's SQL text, read with standard escaping. Use + * {@link #setCommentValue(StringValue)} for another escaping. Null removes the comment. + */ public void setComment(String comment) { - this.comment = comment; + commentValue = comment != null ? new StringValue(comment) : null; + } + + /** Returns the COMMENT string literal; {@link StringValue#getNotExcapedValue()} decodes it. */ + public StringValue getCommentValue() { + return commentValue; + } + + public void setCommentValue(StringValue commentValue) { + this.commentValue = commentValue; } public boolean isCommentUseEquals() { @@ -266,7 +281,7 @@ public String toString() { } appendOption(b, "ENGINE", storageEngine, storageEngineUseEquals); } - appendOption(b, "COMMENT", comment, commentUseEquals); + appendOption(b, "COMMENT", getComment(), commentUseEquals); appendOption(b, "DATA DIRECTORY", dataDirectory, dataDirectoryUseEquals); appendOption(b, "INDEX DIRECTORY", indexDirectory, indexDirectoryUseEquals); appendOption(b, "MAX_ROWS", maxRows, maxRowsUseEquals); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java index f305d547c..003907831 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java @@ -14,6 +14,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.schema.Table; @@ -34,6 +35,7 @@ public class TableOption implements Serializable { private List storageParameters; private ColumnOption.Storage tablespaceStorage; private OnCommitAction onCommitAction; + private StringValue comment; public enum Kind { ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, SECONDARY_ENGINE, AUTOEXTEND_SIZE, INSERT_METHOD, PACK_KEYS, DELAY_KEY_WRITE, CHECKSUM, CONNECTION, COMPRESSION, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, ROW_FORMAT, AVG_ROW_LENGTH, MAX_ROWS, MIN_ROWS, KEY_BLOCK_SIZE, TABLESPACE, FOREIGN_SERVER, ACCESS_METHOD, ON_COMMIT, OTHER @@ -129,6 +131,39 @@ public void setStorageParameters(List storageParameters) { name = "WITH"; } + /** + * Returns the COMMENT string literal, or null for another kind, opaque tokens or a plain value + * set with {@link #setValue(String)}. {@link StringValue#getNotExcapedValue()} decodes it. + */ + public StringValue getComment() { + return comment; + } + + /** + * Replaces the current contents with a COMMENT string literal and keeps the equals spelling; an + * existing COMMENT also keeps its name. A null literal omits the value. + */ + public void setComment(StringValue comment) { + String commentName = kind == Kind.COMMENT && name != null ? name : "COMMENT"; + boolean equals = useEquals; + clearContents(); + kind = Kind.COMMENT; + name = commentName; + useEquals = equals; + this.comment = comment; + } + + public TableOption withComment(StringValue comment) { + setComment(comment); + return this; + } + + public static TableOption comment(StringValue comment) { + TableOption option = new TableOption(); + option.setComment(comment); + return option; + } + public void appendTo(StringBuilder builder, java.util.function.Consumer expressionPrinter) { if (foreignTableOptions != null) { @@ -136,6 +171,9 @@ public void appendTo(StringBuilder builder, } else if (storageParameters != null) { builder.append(name).append(useEquals ? " = " : " "); IndexOption.appendListTo(builder, storageParameters, expressionPrinter); + } else if (comment != null) { + builder.append(name).append(useEquals ? " = " : " "); + expressionPrinter.accept(comment); } else { builder.append(toString()); } @@ -183,6 +221,11 @@ public void setKind(Kind kind) { if (kind != Kind.ON_COMMIT) { onCommitAction = null; } + if (kind != Kind.COMMENT && comment != null) { + // keep the literal's SQL text, as for other scalar values + value = comment.toString(); + comment = null; + } } public String getName() { @@ -204,9 +247,13 @@ public String getValue() { if (onCommitAction != null) { return onCommitAction.toString(); } + if (comment != null) { + return comment.toString(); + } return unionTables == null ? value : PlainSelect.getStringList(unionTables, true, true); } + /** Sets a plain value; it replaces a COMMENT literal without being parsed. */ public void setValue(String value) { tokens = null; this.value = value; @@ -214,6 +261,7 @@ public void setValue(String value) { unionTables = null; storageParameters = null; onCommitAction = null; + comment = null; } /** Returns the mutable MERGE table sources, including an empty UNION list. */ @@ -296,6 +344,7 @@ private void clearContents() { unionTables = null; storageParameters = null; onCommitAction = null; + comment = null; } public TableOption withKind(Kind kind) { diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 071b1c39f..a6d1f0563 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -168,11 +168,12 @@ public static void visit(CreateTable table, Consumer expressions, visit(table.getPartitionBound(), expressions); } - /** Shared CREATE/ALTER table option traversal. */ + /** Shared CREATE/ALTER table option traversal, including COMMENT literals. */ public static void visit(net.sf.jsqlparser.statement.create.table.TableOption option, Consumer expressions, Consumer tables) { if (option != null) { visitOptions(option.getStorageParameters(), expressions); + accept(option.getComment(), expressions); if (option.getUnionTables() != null) { option.getUnionTables().forEach(source -> accept(source, tables)); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f49b3ddba..a22f26e29 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -16947,6 +16947,7 @@ TableOption MySqlTableOption(): { TableOption.Kind kind = null; TableOption option; ColumnOption.Storage storage = null; + StringValue comment = null; } { ( tk= { name = tk.image; kind = TableOption.Kind.UNION; @@ -17017,10 +17018,16 @@ TableOption MySqlTableOption(): { kind = TableOption.Kind.COMMENT; } [ "=" { useEquals = true; } ] - value=MySqlTableOptionValue() + // A string literal keeps its escaping; other values remain plain text + ( + LOOKAHEAD({ getToken(1).kind == S_CHAR_LITERAL }) tk= + { comment = stringValue(tk); } + | value=MySqlTableOptionValue() + ) ) { option = new TableOption(kind, name, value, useEquals); + if (comment != null) { option.setComment(comment); } if (unionTables != null) { option.setUnionTables(unionTables); } option.setTablespaceStorage(storage); return option; @@ -19048,7 +19055,7 @@ void PartitionDefinitionOption(PartitionDefinition partitionDefinition): | [ "=" { useEquals = true; } ] tk= { - partitionDefinition.setComment(tk.image); + partitionDefinition.setCommentValue(stringValue(tk)); partitionDefinition.setCommentUseEquals(useEquals); } | diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index fac9650c5..bcb58165f 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -468,6 +468,47 @@ Use ``TableOption.onCommit`` and ``TableOption.accessMethod`` to build them. .setOnCommitAction(TableOption.OnCommitAction.PRESERVE_ROWS); // CREATE TEMP TABLE t (id INT) USING heap ON COMMIT PRESERVE ROWS +MySQL table and partition comments +---------------------------------- + +A table ``COMMENT`` in CREATE TABLE, CREATE TABLE … AS SELECT and ALTER TABLE is a +``StringValue`` in ``TableOption.getComment()``. Partition and subpartition comments, +including those of ALTER TABLE ADD and REORGANIZE PARTITION, are in +``PartitionDefinition.getCommentValue()``. The literal records the escaping of the +parser configuration, so ``getNotExcapedValue()`` returns the text MySQL stores. + +.. code-block:: java + + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT) COMMENT = 'it\\'s' " + + "PARTITION BY HASH (id) (PARTITION p0 COMMENT 'a\\\\b')", + parser -> parser.withDialect(Dialect.MYSQL)); + TableOption option = table.getTableOption(TableOption.Kind.COMMENT).orElseThrow(); + option.getComment().getNotExcapedValue(); // it's + option.getValue(); // 'it\'s' + table.getPartitioning().getPartitionDefinitions().get(0) + .getCommentValue().getNotExcapedValue(); // a\b + +``getValue()``, ``getTokens()``, ``CreateTable.getTableOptionsStrings()``, +``AlterExpressionTableOption.getCommentText()`` and ``PartitionDefinition.getComment()`` +still return the literal as written, and the SQL output is unchanged except that a +string prefix such as ``n'…'`` is printed in upper case, as in other string literals. +The table comment literal is rendered by the expression deparser and visited like a +column COMMENT literal. + +``setComment``, ``withComment`` and the ``TableOption.comment`` factory replace the +option's contents with the literal and select COMMENT. They keep the equals spelling, +and an existing COMMENT also keeps its name; a null literal omits the value. +``setValue`` stores a plain value and ``setTokens`` opaque tokens. Neither is parsed, +so ``getComment()`` then returns null. Selecting another kind keeps the literal's SQL +text as a plain value. A COMMENT value that is not a string literal also stays a plain +value, for example a double-quoted string parsed without the MySQL dialect, where it +is a quoted identifier. + +``PartitionDefinition.setComment(String)`` reads the SQL text of a literal with +standard escaping; pass a ``StringValue`` with another escaping to +``setCommentValue``. + Inspect PostgreSQL schema statements ------------------------------------ diff --git a/src/test/java/net/sf/jsqlparser/statement/create/MySqlCommentLiteralTest.java b/src/test/java/net/sf/jsqlparser/statement/create/MySqlCommentLiteralTest.java new file mode 100644 index 000000000..f8ffcd23c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/MySqlCommentLiteralTest.java @@ -0,0 +1,366 @@ +/*- + * #%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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.alter.AlterExpression; +import net.sf.jsqlparser.statement.alter.AlterExpressionTableOption; +import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.PartitionDefinition; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** MySQL table and partition COMMENT literals and the values a server stores for them. */ +class MySqlCommentLiteralTest { + + /** Literals with the comment MySQL 8.4 stores for them. */ + static Stream mySqlLiterals() { + return Stream.of( + Arguments.of("'users'", "users"), + Arguments.of("'사용자 테이블'", "사용자 테이블"), + Arguments.of("'it''s'", "it's"), + Arguments.of("'it\\'s'", "it's"), + Arguments.of("\"dq\"", "dq"), + Arguments.of("\"double \\\"quoted\\\" text\"", "double \"quoted\" text"), + Arguments.of("''", ""), + Arguments.of("'line1\\nline2'", "line1\nline2"), + Arguments.of("'a\\\\b'", "a\\b"), + Arguments.of("'tab\\there'", "tab\there"), + Arguments.of("'percent\\% underscore\\_'", "percent\\% underscore\\_"), + Arguments.of("'emoji 😀'", "emoji 😀"), + Arguments.of("'first line\nsecond line'", "first line\nsecond line")); + } + + /** Literals that read the same with and without a dialect. */ + static Stream portableLiterals() { + return Stream.of( + Arguments.of("'users'", "users"), + Arguments.of("'it''s'", "it's"), + Arguments.of("''", ""), + Arguments.of("'사용자 테이블'", "사용자 테이블"), + Arguments.of("'emoji 😀'", "emoji 😀")); + } + + @ParameterizedTest + @MethodSource("mySqlLiterals") + void tableCommentsDecodeToTheStoredValue(String literal, String stored) + throws JSQLParserException { + assertTableComments(literal, stored, Dialect.MYSQL); + } + + @ParameterizedTest + @MethodSource("portableLiterals") + void tableCommentsDecodeWithoutDialect(String literal, String stored) + throws JSQLParserException { + assertTableComments(literal, stored, null); + } + + private static void assertTableComments(String literal, String stored, Dialect dialect) + throws JSQLParserException { + String sql = "CREATE TABLE t (id INT) ENGINE = InnoDB COMMENT = " + literal + + " DEFAULT CHARSET = utf8mb4"; + CreateTable create = (CreateTable) parse(sql, dialect); + assertComment(create.getTableOptions().get(1), literal, stored); + assertEquals(List.of("ENGINE", "=", "InnoDB", "COMMENT", "=", literal, "DEFAULT", + "CHARSET", "=", "utf8mb4"), create.getTableOptionsStrings()); + assertEquals(sql, create.toString()); + assertRoundTrip(create, dialect); + + String ctas = "CREATE TABLE t (id INT) COMMENT " + literal + " AS SELECT 1 AS id"; + CreateTable query = (CreateTable) parse(ctas, dialect); + assertComment(query.getTableOptions().get(0), literal, stored); + assertEquals(List.of("COMMENT", literal), query.getTableOptionsStrings()); + assertEquals(ctas, query.toString()); + assertRoundTrip(query, dialect); + + Alter alter = (Alter) parse("ALTER TABLE t COMMENT = " + literal, dialect); + AlterExpressionTableOption action = + (AlterExpressionTableOption) alter.getAlterExpressions().get(0); + assertComment(action.getStructuredTableOption(), literal, stored); + assertEquals(literal, action.getCommentText()); + assertEquals(AlterOperation.COMMENT_WITH_EQUAL_SIGN, action.getOperation()); + assertEquals("ALTER TABLE t COMMENT = " + literal, alter.toString()); + assertRoundTrip(alter, dialect); + } + + private static void assertComment(TableOption option, String literal, String stored) { + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertEquals(stored, option.getComment().getNotExcapedValue()); + assertEquals(literal, option.getComment().toString()); + assertEquals(literal, option.getValue()); + assertEquals(literal, option.getTokens().get(option.getTokens().size() - 1)); + } + + @Test + void mySqlEscapingIsRecordedOnTheLiteral() throws JSQLParserException { + CreateTable mySql = + (CreateTable) parse("CREATE TABLE t (id INT) COMMENT 'a\\\\b'", Dialect.MYSQL); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, + mySql.getTableOptions().get(0).getComment().getEscaping()); + CreateTable standard = (CreateTable) parse("CREATE TABLE t (id INT) COMMENT 'a\\b'", null); + assertEquals(StringValue.Escaping.STANDARD, + standard.getTableOptions().get(0).getComment().getEscaping()); + assertEquals("a\\b", standard.getTableOptions().get(0).getComment().getNotExcapedValue()); + } + + @Test + void alterCommentsKeepTheirPositionAmongOtherActions() throws JSQLParserException { + Alter alter = (Alter) parse( + "ALTER TABLE t MODIFY id INT COMMENT 'col', ENGINE=InnoDB, comment='it\\'s'", + Dialect.MYSQL); + List actions = alter.getAlterExpressions(); + TableOption comment = + ((AlterExpressionTableOption) actions.get(2)).getStructuredTableOption(); + assertEquals("comment", comment.getName()); + assertTrue(comment.isUseEquals()); + assertEquals("it's", comment.getComment().getNotExcapedValue()); + assertNull(((AlterExpressionTableOption) actions.get(1)).getStructuredTableOption() + .getComment()); + assertRoundTrip(alter, Dialect.MYSQL); + + Alter algorithm = + (Alter) parse("ALTER TABLE t COMMENT 'x', ALGORITHM=INSTANT", Dialect.MYSQL); + AlterExpressionTableOption action = + (AlterExpressionTableOption) algorithm.getAlterExpressions().get(0); + assertEquals(AlterOperation.COMMENT, action.getOperation()); + assertEquals("x", action.getStructuredTableOption().getComment().getNotExcapedValue()); + assertRoundTrip(algorithm, Dialect.MYSQL); + } + + @Test + void valuesThatAreNotStringLiteralsRemainPlainValues() throws JSQLParserException { + // Accepted leniently; without the MySQL dialect "x" is a quoted identifier + for (String sql : new String[] {"CREATE TABLE t (id INT) COMMENT = \"x\"", + "CREATE TABLE t (id INT) COMMENT plain"}) { + CreateTable create = (CreateTable) parse(sql, null); + TableOption option = create.getTableOptions().get(0); + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertNull(option.getComment()); + assertEquals(sql.substring(sql.lastIndexOf(' ') + 1), option.getValue()); + assertRoundTrip(create, null); + } + CreateTable mySql = + (CreateTable) parse("CREATE TABLE t (id INT) COMMENT = \"x\"", Dialect.MYSQL); + assertEquals("x", mySql.getTableOptions().get(0).getComment().getNotExcapedValue()); + } + + @Test + void commentLiteralEditsAreRenderedAndVisited() throws JSQLParserException { + CreateTable create = + (CreateTable) parse("CREATE TABLE t (id INT) comment = 'old'", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + StringValue replacement = new StringValue("'new'"); + option.setComment(replacement); + assertSame(replacement, option.getComment()); + assertEquals("comment = 'new'", option.toString()); + replacement.setValue("it''s"); + assertEquals("CREATE TABLE t (id INT) comment = 'it''s'", create.toString()); + assertEquals(List.of("comment", "=", "'it''s'"), option.getTokens()); + assertRoundTrip(create, Dialect.MYSQL); + + StringBuilder customized = new StringBuilder(); + option.appendTo(customized, expression -> customized.append("'printed'")); + assertEquals("comment = 'printed'", customized.toString()); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(option, visited::add, table -> { + }); + assertEquals(List.of(replacement), visited); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(create)); + } + + @Test + void commentFactoryAndReplacementOfOtherOptions() throws JSQLParserException { + TableOption created = TableOption.comment(new StringValue("'x'")); + assertEquals(TableOption.Kind.COMMENT, created.getKind()); + assertEquals("COMMENT", created.getName()); + assertFalse(created.isUseEquals()); + assertEquals("COMMENT 'x'", created.toString()); + assertEquals("COMMENT = 'x'", created.withUseEquals(true).toString()); + + CreateTable create = + (CreateTable) parse("CREATE TABLE t (id INT) ENGINE=InnoDB", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + option.withComment(new StringValue("'x'")); + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertEquals("COMMENT", option.getName()); + assertTrue(option.isUseEquals()); + assertEquals("CREATE TABLE t (id INT) COMMENT = 'x'", create.toString()); + assertTrue(create.getTableOption(TableOption.Kind.ENGINE).isEmpty()); + assertRoundTrip(create, Dialect.MYSQL); + + option.setComment(null); + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertNull(option.getValue()); + assertEquals("COMMENT", option.toString()); + } + + @Test + void scalarAndRawSettersReplaceTheLiteralWithoutParsing() throws JSQLParserException { + CreateTable create = + (CreateTable) parse("CREATE TABLE t (id INT) COMMENT='it\\'s'", Dialect.MYSQL); + TableOption option = create.getTableOptions().get(0); + option.setValue("'plain'"); + assertNull(option.getComment()); + assertEquals(TableOption.Kind.COMMENT, option.getKind()); + assertEquals("COMMENT = 'plain'", option.toString()); + assertRoundTrip(create, Dialect.MYSQL); + + option.setComment(new StringValue("'typed'")); + option.setKind(TableOption.Kind.OTHER); + assertNull(option.getComment()); + assertEquals("'typed'", option.getValue()); + option.setKind(TableOption.Kind.COMMENT); + assertNull(option.getComment()); + assertEquals("COMMENT = 'typed'", option.toString()); + + option.setComment(new StringValue("'typed'")); + List raw = new ArrayList<>(List.of("COMMENT", "'raw'")); + option.setTokens(raw); + assertNull(option.getComment()); + assertEquals(TableOption.Kind.OTHER, option.getKind()); + assertSame(raw, option.getTokens()); + option.setTokens(null); + assertNull(option.getComment()); + + option.setTokens(raw); + option.setComment(new StringValue("'typed'")); + assertEquals(List.of("COMMENT", "'typed'"), option.getTokens()); + assertRoundTrip(create, Dialect.MYSQL); + + AlterExpressionTableOption action = (AlterExpressionTableOption) ((Alter) parse( + "ALTER TABLE t COMMENT 'old'", Dialect.MYSQL)).getAlterExpressions().get(0); + action.setCommentText("'new'"); + assertNull(action.getStructuredTableOption().getComment()); + assertEquals("'new'", action.getCommentText()); + } + + @Test + void partitionAndSubpartitionCommentsDecodeToTheStoredValue() throws JSQLParserException { + String sql = "CREATE TABLE t (id INT, d INT) PARTITION BY RANGE (id) " + + "SUBPARTITION BY HASH (d) (PARTITION p0 VALUES LESS THAN (10) " + + "COMMENT = 'it\\'s' (SUBPARTITION s0 COMMENT 'sub0', " + + "SUBPARTITION s1 COMMENT = 'line1\\nline2'), " + + "PARTITION p1 VALUES LESS THAN MAXVALUE COMMENT '사용자 😀' " + + "(SUBPARTITION s2, SUBPARTITION s3 COMMENT 'a\\\\b'))"; + CreateTable create = (CreateTable) parse(sql, Dialect.MYSQL); + List partitions = + create.getPartitioning().getPartitionDefinitions(); + assertPartitionComment(partitions.get(0), "'it\\'s'", "it's", true); + assertPartitionComment(partitions.get(0).getSubPartitionDefinitions().get(0), "'sub0'", + "sub0", false); + assertPartitionComment(partitions.get(0).getSubPartitionDefinitions().get(1), + "'line1\\nline2'", "line1\nline2", true); + assertPartitionComment(partitions.get(1), "'사용자 😀'", "사용자 😀", false); + assertNull(partitions.get(1).getSubPartitionDefinitions().get(0).getCommentValue()); + assertNull(partitions.get(1).getSubPartitionDefinitions().get(0).getComment()); + assertPartitionComment(partitions.get(1).getSubPartitionDefinitions().get(1), "'a\\\\b'", + "a\\b", false); + assertEquals(StringValue.Escaping.MYSQL_BACKSLASH, + partitions.get(0).getCommentValue().getEscaping()); + assertEquals(sql, create.toString()); + assertRoundTrip(create, Dialect.MYSQL); + } + + @Test + void alterPartitionCommentsDecodeToTheStoredValue() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, null}) { + Alter add = (Alter) parse("ALTER TABLE t ADD PARTITION (PARTITION p1 VALUES " + + "LESS THAN (20) COMMENT 'it''s added')", dialect); + PartitionDefinition added = + add.getAlterExpressions().get(0).getPartitionDefinitions().get(0); + assertPartitionComment(added, "'it''s added'", "it's added", false); + assertRoundTrip(add, dialect); + + Alter reorganize = (Alter) parse("ALTER TABLE t REORGANIZE PARTITION p1 INTO " + + "(PARTITION p1a VALUES LESS THAN (15) COMMENT = 'reorg', " + + "PARTITION p1b VALUES LESS THAN (20))", dialect); + List definitions = + reorganize.getAlterExpressions().get(0).getPartitionDefinitions(); + assertPartitionComment(definitions.get(0), "'reorg'", "reorg", true); + assertNull(definitions.get(1).getCommentValue()); + assertRoundTrip(reorganize, dialect); + } + } + + private static void assertPartitionComment(PartitionDefinition definition, String literal, + String stored, boolean useEquals) { + assertEquals(stored, definition.getCommentValue().getNotExcapedValue()); + assertEquals(literal, definition.getComment()); + assertEquals(useEquals, definition.isCommentUseEquals()); + } + + @Test + void partitionCommentStringAccessorsConvertTheLiteral() throws JSQLParserException { + CreateTable create = (CreateTable) parse("CREATE TABLE t (id INT) PARTITION BY " + + "RANGE (id) (PARTITION p0 VALUES LESS THAN (10) COMMENT 'old')", Dialect.MYSQL); + PartitionDefinition definition = create.getPartitioning().getPartitionDefinitions().get(0); + + definition.setComment("'it''s'"); + assertInstanceOf(StringValue.class, definition.getCommentValue()); + assertEquals(StringValue.Escaping.STANDARD, definition.getCommentValue().getEscaping()); + assertEquals("it's", definition.getCommentValue().getNotExcapedValue()); + assertEquals("'it''s'", definition.getComment()); + assertRoundTrip(create, Dialect.MYSQL); + + StringValue escaped = new StringValue("'a\\nb'") + .withEscaping(StringValue.Escaping.MYSQL_BACKSLASH); + definition.setCommentValue(escaped); + definition.setCommentUseEquals(true); + assertSame(escaped, definition.getCommentValue()); + assertEquals("'a\\nb'", definition.getComment()); + assertEquals("PARTITION p0 VALUES LESS THAN (10) COMMENT = 'a\\nb'", + definition.toString()); + assertRoundTrip(create, Dialect.MYSQL); + + definition.setComment(null); + assertNull(definition.getCommentValue()); + assertEquals("PARTITION p0 VALUES LESS THAN (10)", definition.toString()); + } + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }); + } + + private static void assertRoundTrip(Statement statement, Dialect dialect) + throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString(), dialect).toString()); + } +}