From abb28d37dc03f3e2600b6a89f6d0ea21f00c37dd Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 25 Sep 2026 15:31:50 +0900 Subject: [PATCH] feat: structure MySQL ALTER TABLE ordering and share table options Signed-off-by: minleejae --- .../alter/AlterExpressionOrderBy.java | 56 +++++++++++ .../statement/alter/AlterOperation.java | 2 +- .../statement/create/table/TableOption.java | 2 +- .../util/TableDefinitionTraversal.java | 4 + .../util/deparser/AlterDeParser.java | 5 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 39 +++++++- .../alter/MySqlAlterOrderingOptionsTest.java | 97 +++++++++++++++++++ 7 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterOrderingOptionsTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java new file mode 100644 index 000000000..2814d1358 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionOrderBy.java @@ -0,0 +1,56 @@ +/*- + * #%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.alter; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.select.OrderByElement; + +/** MySQL ALTER TABLE ORDER BY, using the same mutable order elements as query ordering. */ +public class AlterExpressionOrderBy extends AlterExpression { + private final List orderByElements = new ArrayList<>(); + + public AlterExpressionOrderBy() { + setOperation(AlterOperation.ORDER_BY); + } + + public List getOrderByElements() { + return orderByElements; + } + + @Override + protected void appendBody(StringBuilder builder) { + appendOrderBy(builder, builder::append); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { + appendOrderBy(builder, expressionPrinter); + appendCommonTail(builder); + return builder; + } + + private void appendOrderBy(StringBuilder builder, Consumer expressionPrinter) { + builder.append("ORDER BY "); + for (int i = 0; i < orderByElements.size(); i++) { + if (i > 0) { + builder.append(", "); + } + OrderByElement element = orderByElements.get(i); + expressionPrinter.accept(element.getExpression()); + if (!element.isAsc()) { + builder.append(" DESC"); + } else if (element.isAscDescPresent()) { + builder.append(" ASC"); + } + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java index bca23bbe4..ed34d4950 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java @@ -12,7 +12,7 @@ import java.util.Locale; public enum AlterOperation { - ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY; + ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY, ORDER_BY; public static AlterOperation from(String operation) { return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT)); 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 f886f703f..e1d3fe6c6 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 @@ -21,7 +21,7 @@ public class TableOption implements Serializable { 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, OTHER + ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, ROW_FORMAT, OTHER } private Kind kind = Kind.OTHER; diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 5c6bfe7d1..8e2b19e45 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -49,6 +49,10 @@ public static void visit(CreateIndex createIndex, Consumer expressio /** Visits the structured definitions and expressions belonging to a single ALTER action. */ public static void visit(AlterExpression action, Consumer expressions, Consumer tables) { + if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) { + ((net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) action).getOrderByElements() + .forEach(element -> accept(element.getExpression(), expressions)); + } if (action.getOperation() == AlterOperation.RENAME_TABLE) { accept(action.getNewTable(), tables); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java index e162f0ec8..2e9e080fd 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -50,6 +50,11 @@ public void deParse(Alter alter) { } private void deParseAction(AlterExpression action) { + if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) { + ((net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) action).appendTo(builder, + expression -> expression.accept(expressionVisitor, null)); + return; + } if (action instanceof AlterExpressionPartition) { ((AlterExpressionPartition) action).appendTo(builder, expression -> expression.accept(expressionVisitor, null)); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6c42e69e6..ca9616318 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1565,7 +1565,7 @@ public class CCJSqlParser extends AbstractJSqlParser { int kind = getToken(1).kind; if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT || isMySqlScalarTableOptionAhead() || kind == K_UNION - || isMySqlStatisticsOptionAhead()) { + || isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT")) { return true; } if (kind == K_CHARACTER && getToken(2).kind == K_SET @@ -15237,6 +15237,13 @@ TableOption MySqlTableOption(): { "Expected DEFAULT, 0, or 1 for " + name); } | + LOOKAHEAD({ isKeywordAhead("ROW_FORMAT") }) ContextualKeyword("ROW_FORMAT") + { name = "ROW_FORMAT"; kind = TableOption.Kind.ROW_FORMAT; } + [ "=" { useEquals = true; } ] value=RelObjectName() { + requireDdlSyntax(Arrays.asList("DEFAULT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", "COMPACT") + .contains(value.toUpperCase(Locale.ROOT)), "Invalid MySQL ROW_FORMAT"); + } + | tk= { name = tk.image; kind = TableOption.Kind.ENGINE; @@ -15307,6 +15314,31 @@ TableOption MySqlScalarTableOption(): { return new TableOption(kind, name, value, useEquals); } } +/** Statistics and row format share CREATE's validation and option nodes. */ +AlterExpressionTableOption AlterSharedTableOption(): +{ TableOption option; AlterExpressionTableOption action = new AlterExpressionTableOption(); } +{ + option=MySqlTableOption() + { action.setStructuredTableOption(option); return action; } +} + +AlterExpressionOrderBy MySqlAlterOrderBy(): +{ AlterExpressionOrderBy action = new AlterExpressionOrderBy(); OrderByElement element; } +{ + element=MySqlAlterOrderColumn() { action.getOrderByElements().add(element); } + ( LOOKAHEAD("," RelObjectName() [ | ] ( "," | | | )) + "," element=MySqlAlterOrderColumn() { action.getOrderByElements().add(element); } )* + { return action; } +} + +OrderByElement MySqlAlterOrderColumn(): +{ OrderByElement element = new OrderByElement(); String name; Token order; } +{ + name=RelObjectName() { element.setExpression(createColumn(name)); } + [ LOOKAHEAD(2) ( order= | order= ) { element.setAsc(order.kind == K_ASC); element.setAscDescPresent(true); } ] + { return element; } +} + AlterExpressionTableOption AlterScalarTableOption(): { TableOption option; @@ -17754,6 +17786,11 @@ AlterExpression AlterExpression(): { ( + LOOKAHEAD( ) alterExp=MySqlAlterOrderBy() + | + LOOKAHEAD({ isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT") }) + alterExp=AlterSharedTableOption() + | LOOKAHEAD( , { Dialect.COCKROACHDB.name().equals(getAsString(Feature.dialect)) }) alterExp = CockroachAlterPrimaryKey() diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterOrderingOptionsTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterOrderingOptionsTest.java new file mode 100644 index 000000000..ee284948a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/MySqlAlterOrderingOptionsTest.java @@ -0,0 +1,97 @@ +/*- + * #%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.alter; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.TableOption; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +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; + +class MySqlAlterOrderingOptionsTest { + @ParameterizedTest + @ValueSource(strings = {"ORDER BY id DESC, label ASC", "ORDER BY id, label", + "ENGINE = InnoDB, ROW_FORMAT = DYNAMIC, STATS_PERSISTENT = DEFAULT", + "STATS_AUTO_RECALC = 1, STATS_SAMPLE_PAGES = 16", + "ORDER BY id DESC, label ASC, ENGINE = InnoDB", "ROW_FORMAT COMPACT, ORDER BY id DESC"}) + void actionAndOrderListBoundariesRemainSeparate(String actions) throws JSQLParserException { + Alter alter = parse("ALTER TABLE t " + actions); + assertEquals("ALTER TABLE t " + actions, alter.toString()); + alter.getAlterExpressions().forEach( + action -> assertNotEquals(AlterOperation.UNSPECIFIC, action.getOperation())); + assertRoundTrip(alter); + assertEquals(2, CCJSqlParserUtil + .parseStatements(alter + "; SELECT 1", p -> p.withDialect(Dialect.MYSQL)).size()); + } + + @Test + void orderElementsAreMutableAndVisited() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ORDER BY id DESC, label ASC"); + AlterExpressionOrderBy order = (AlterExpressionOrderBy) alter.getAlterExpressions().get(0); + assertEquals(2, order.getOrderByElements().size()); + order.getOrderByElements().get(0).setExpression(new Column("new_id")); + order.getOrderByElements().get(0).setAsc(true); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(order, visited::add, ignored -> { + }); + assertEquals(2, visited.size()); + assertEquals("ALTER TABLE t ORDER BY new_id ASC, label ASC", alter.toString()); + assertRoundTrip(alter); + } + + @Test + void rowFormatAndStatisticsShareCreateOptions() throws JSQLParserException { + Alter alter = parse("ALTER TABLE t ROW_FORMAT = DYNAMIC, STATS_PERSISTENT = DEFAULT"); + TableOption rowFormat = ((AlterExpressionTableOption) alter.getAlterExpressions().get(0)) + .getStructuredTableOption(); + TableOption statistics = ((AlterExpressionTableOption) alter.getAlterExpressions().get(1)) + .getStructuredTableOption(); + assertEquals(TableOption.Kind.ROW_FORMAT, rowFormat.getKind()); + assertEquals(TableOption.Kind.STATS_PERSISTENT, statistics.getKind()); + rowFormat.setValue("COMPACT"); + statistics.setValue("1"); + assertEquals("ALTER TABLE t ROW_FORMAT = COMPACT, STATS_PERSISTENT = 1", alter.toString()); + CreateTable create = + (CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t (id INT) ROW_FORMAT = COMPACT", + p -> p.withDialect(Dialect.MYSQL)); + assertEquals(rowFormat.toString(), + create.getTableOption(TableOption.Kind.ROW_FORMAT).orElseThrow().toString()); + assertRoundTrip(alter); + } + + @Test + void invalidOptionsAndOrderExpressionsFail() { + for (String actions : new String[] {"ROW_FORMAT = invalid", "STATS_PERSISTENT = 2", + "ORDER BY id + 1"}) { + assertThrows(JSQLParserException.class, () -> parse("ALTER TABLE t " + actions)); + } + } + + private static Alter parse(String sql) throws JSQLParserException { + return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)); + } + + private static void assertRoundTrip(Alter alter) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + alter.accept(new StatementDeParser(sql), null); + assertEquals(alter.toString(), sql.toString()); + assertEquals(alter.toString(), parse(sql.toString()).toString()); + } +}