Skip to content

Commit aeea767

Browse files
authored
feat: structure MySQL ALTER TABLE ordering and share table options (#2708)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 42d4670 commit aeea767

7 files changed

Lines changed: 202 additions & 3 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.alter;
11+
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.function.Consumer;
15+
import net.sf.jsqlparser.expression.Expression;
16+
import net.sf.jsqlparser.statement.select.OrderByElement;
17+
18+
/** MySQL ALTER TABLE ORDER BY, using the same mutable order elements as query ordering. */
19+
public class AlterExpressionOrderBy extends AlterExpression {
20+
private final List<OrderByElement> orderByElements = new ArrayList<>();
21+
22+
public AlterExpressionOrderBy() {
23+
setOperation(AlterOperation.ORDER_BY);
24+
}
25+
26+
public List<OrderByElement> getOrderByElements() {
27+
return orderByElements;
28+
}
29+
30+
@Override
31+
protected void appendBody(StringBuilder builder) {
32+
appendOrderBy(builder, builder::append);
33+
}
34+
35+
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
36+
appendOrderBy(builder, expressionPrinter);
37+
appendCommonTail(builder);
38+
return builder;
39+
}
40+
41+
private void appendOrderBy(StringBuilder builder, Consumer<Expression> expressionPrinter) {
42+
builder.append("ORDER BY ");
43+
for (int i = 0; i < orderByElements.size(); i++) {
44+
if (i > 0) {
45+
builder.append(", ");
46+
}
47+
OrderByElement element = orderByElements.get(i);
48+
expressionPrinter.accept(element.getExpression());
49+
if (!element.isAsc()) {
50+
builder.append(" DESC");
51+
} else if (element.isAscDescPresent()) {
52+
builder.append(" ASC");
53+
}
54+
}
55+
}
56+
}

‎src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import java.util.Locale;
1313

1414
public enum AlterOperation {
15-
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, ALTER_RELATION;
15+
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, ALTER_RELATION, ORDER_BY;
1616

1717
public static AlterOperation from(String operation) {
1818
return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT));

‎src/main/java/net/sf/jsqlparser/statement/create/table/TableOption.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
public class TableOption implements Serializable {
2222

2323
public enum Kind {
24-
ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, OTHER
24+
ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, ENCRYPTION, PASSWORD, DATA_DIRECTORY, INDEX_DIRECTORY, STORAGE_PARAMETERS, WITHOUT_OIDS, ENGINE_ATTRIBUTE, SECONDARY_ENGINE_ATTRIBUTE, ROW_FORMAT, OTHER
2525
}
2626

2727
private Kind kind = Kind.OTHER;

‎src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public static void visit(CreateIndex createIndex, Consumer<Expression> expressio
4949
/** Visits the structured definitions and expressions belonging to a single ALTER action. */
5050
public static void visit(AlterExpression action, Consumer<Expression> expressions,
5151
Consumer<Table> tables) {
52+
if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) {
53+
((net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) action).getOrderByElements()
54+
.forEach(element -> accept(element.getExpression(), expressions));
55+
}
5256
if (action instanceof net.sf.jsqlparser.statement.alter.RelationAlterAction) {
5357
net.sf.jsqlparser.statement.alter.RelationAlterAction relation =
5458
(net.sf.jsqlparser.statement.alter.RelationAlterAction) action;

‎src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ public void deParse(Alter alter) {
5151
}
5252

5353
private void deParseAction(AlterExpression action) {
54+
if (action instanceof net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) {
55+
((net.sf.jsqlparser.statement.alter.AlterExpressionOrderBy) action).appendTo(builder,
56+
expression -> expression.accept(expressionVisitor, null));
57+
return;
58+
}
5459
if (action instanceof RelationAlterAction) {
5560
((RelationAlterAction) action).appendTo(builder,
5661
expression -> expression.accept(expressionVisitor, null));

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1570,7 +1570,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15701570
int kind = getToken(1).kind;
15711571
if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT
15721572
|| isMySqlScalarTableOptionAhead() || kind == K_UNION
1573-
|| isMySqlStatisticsOptionAhead()) {
1573+
|| isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT")) {
15741574
return true;
15751575
}
15761576
if (kind == K_CHARACTER && getToken(2).kind == K_SET
@@ -15480,6 +15480,13 @@ TableOption MySqlTableOption(): {
1548015480
"Expected DEFAULT, 0, or 1 for " + name);
1548115481
}
1548215482
|
15483+
LOOKAHEAD({ isKeywordAhead("ROW_FORMAT") }) ContextualKeyword("ROW_FORMAT")
15484+
{ name = "ROW_FORMAT"; kind = TableOption.Kind.ROW_FORMAT; }
15485+
[ "=" { useEquals = true; } ] value=RelObjectName() {
15486+
requireDdlSyntax(Arrays.asList("DEFAULT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", "COMPACT")
15487+
.contains(value.toUpperCase(Locale.ROOT)), "Invalid MySQL ROW_FORMAT");
15488+
}
15489+
|
1548315490
LOOKAHEAD({ isMySqlScalarTableOptionAhead() }) option=MySqlScalarTableOption()
1548415491
{ return option; }
1548515492
|
@@ -15554,6 +15561,30 @@ TableOption MySqlScalarTableOption():
1555415561
{ return new TableOption(kind, name, value, useEquals); }
1555515562
}
1555615563

15564+
/** Statistics and row format share CREATE's validation and option nodes. */
15565+
AlterExpressionTableOption AlterSharedTableOption():
15566+
{ TableOption option; AlterExpressionTableOption action = new AlterExpressionTableOption(); }
15567+
{
15568+
option=MySqlTableOption()
15569+
{ action.setStructuredTableOption(option); return action; }
15570+
}
15571+
15572+
AlterExpressionOrderBy MySqlAlterOrderBy():
15573+
{ AlterExpressionOrderBy action = new AlterExpressionOrderBy(); OrderByElement element; }
15574+
{
15575+
<K_ORDER> <K_BY> element=MySqlAlterOrderColumn() { action.getOrderByElements().add(element); }
15576+
( LOOKAHEAD(1) "," element=MySqlAlterOrderColumn() { action.getOrderByElements().add(element); } )*
15577+
{ return action; }
15578+
}
15579+
15580+
OrderByElement MySqlAlterOrderColumn():
15581+
{ OrderByElement element = new OrderByElement(); String name; Token order; }
15582+
{
15583+
name=RelObjectName() { element.setExpression(createColumn(name)); }
15584+
[ LOOKAHEAD(2) ( order=<K_ASC> | order=<K_DESC> ) { element.setAsc(order.kind == K_ASC); element.setAscDescPresent(true); } ]
15585+
{ return element; }
15586+
}
15587+
1555715588
AlterExpressionTableOption AlterScalarTableOption():
1555815589
{
1555915590
TableOption option;
@@ -18068,6 +18099,11 @@ AlterExpression AlterExpression():
1806818099
{
1806918100

1807018101
(
18102+
LOOKAHEAD(<K_ORDER> <K_BY>) alterExp=MySqlAlterOrderBy()
18103+
|
18104+
LOOKAHEAD({ isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT") })
18105+
alterExp=AlterSharedTableOption()
18106+
|
1807118107
LOOKAHEAD({ isPostgreSqlRelationActionAhead() }) alterExp=PostgreSqlRelationAction() {
1807218108
requireDdlSyntax(((RelationAlterAction) alterExp).getColumnNumber() == null,
1807318109
"ALTER TABLE expects a column name, not an ordinal");
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.alter;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import net.sf.jsqlparser.JSQLParserException;
16+
import net.sf.jsqlparser.expression.Expression;
17+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
18+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
19+
import net.sf.jsqlparser.schema.Column;
20+
import net.sf.jsqlparser.statement.create.table.CreateTable;
21+
import net.sf.jsqlparser.statement.create.table.TableOption;
22+
import net.sf.jsqlparser.util.TableDefinitionTraversal;
23+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.ValueSource;
27+
28+
class MySqlAlterOrderingOptionsTest {
29+
@ParameterizedTest
30+
@ValueSource(strings = {"ORDER BY id DESC, label ASC", "ORDER BY id, label",
31+
"ENGINE = InnoDB, ROW_FORMAT = DYNAMIC, STATS_PERSISTENT = DEFAULT",
32+
"STATS_AUTO_RECALC = 1, STATS_SAMPLE_PAGES = 16",
33+
"ENGINE = InnoDB, ORDER BY id DESC, label ASC", "ROW_FORMAT COMPACT, ORDER BY id DESC"})
34+
void actionAndOrderListBoundariesRemainSeparate(String actions) throws JSQLParserException {
35+
Alter alter = parse("ALTER TABLE t " + actions);
36+
assertEquals("ALTER TABLE t " + actions, alter.toString());
37+
alter.getAlterExpressions().forEach(
38+
action -> assertNotEquals(AlterOperation.UNSPECIFIC, action.getOperation()));
39+
assertRoundTrip(alter);
40+
assertEquals(2, CCJSqlParserUtil
41+
.parseStatements(alter + "; SELECT 1", p -> p.withDialect(Dialect.MYSQL)).size());
42+
}
43+
44+
@Test
45+
void orderElementsAreMutableAndVisited() throws JSQLParserException {
46+
Alter alter = parse("ALTER TABLE t ORDER BY id DESC, label ASC");
47+
AlterExpressionOrderBy order = (AlterExpressionOrderBy) alter.getAlterExpressions().get(0);
48+
assertEquals(2, order.getOrderByElements().size());
49+
order.getOrderByElements().get(0).setExpression(new Column("new_id"));
50+
order.getOrderByElements().get(0).setAsc(true);
51+
List<Expression> visited = new ArrayList<>();
52+
TableDefinitionTraversal.visit(order, visited::add, ignored -> {
53+
});
54+
assertEquals(2, visited.size());
55+
assertEquals("ALTER TABLE t ORDER BY new_id ASC, label ASC", alter.toString());
56+
assertRoundTrip(alter);
57+
}
58+
59+
@Test
60+
void rowFormatAndStatisticsShareCreateOptions() throws JSQLParserException {
61+
Alter alter = parse("ALTER TABLE t ROW_FORMAT = DYNAMIC, STATS_PERSISTENT = DEFAULT");
62+
TableOption rowFormat = ((AlterExpressionTableOption) alter.getAlterExpressions().get(0))
63+
.getStructuredTableOption();
64+
TableOption statistics = ((AlterExpressionTableOption) alter.getAlterExpressions().get(1))
65+
.getStructuredTableOption();
66+
assertEquals(TableOption.Kind.ROW_FORMAT, rowFormat.getKind());
67+
assertEquals(TableOption.Kind.STATS_PERSISTENT, statistics.getKind());
68+
rowFormat.setValue("COMPACT");
69+
statistics.setValue("1");
70+
assertEquals("ALTER TABLE t ROW_FORMAT = COMPACT, STATS_PERSISTENT = 1", alter.toString());
71+
CreateTable create =
72+
(CreateTable) CCJSqlParserUtil.parse("CREATE TABLE t (id INT) ROW_FORMAT = COMPACT",
73+
p -> p.withDialect(Dialect.MYSQL));
74+
assertEquals(rowFormat.toString(),
75+
create.getTableOption(TableOption.Kind.ROW_FORMAT).orElseThrow().toString());
76+
assertRoundTrip(alter);
77+
}
78+
79+
@Test
80+
void invalidOptionsAndOrderExpressionsFail() {
81+
for (String actions : new String[] {"ROW_FORMAT = invalid", "STATS_PERSISTENT = 2",
82+
"ORDER BY id + 1", "ORDER BY id, ENGINE = InnoDB",
83+
"ORDER BY id, ADD COLUMN x INT"}) {
84+
assertThrows(JSQLParserException.class, () -> parse("ALTER TABLE t " + actions));
85+
}
86+
}
87+
88+
private static Alter parse(String sql) throws JSQLParserException {
89+
return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL));
90+
}
91+
92+
private static void assertRoundTrip(Alter alter) throws JSQLParserException {
93+
StringBuilder sql = new StringBuilder();
94+
alter.accept(new StatementDeParser(sql), null);
95+
assertEquals(alter.toString(), sql.toString());
96+
assertEquals(alter.toString(), parse(sql.toString()).toString());
97+
}
98+
}

0 commit comments

Comments
 (0)