Skip to content

Commit abb28d3

Browse files
committed
feat: structure MySQL ALTER TABLE ordering and share table options
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent dac8159 commit abb28d3

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;
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, 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, OTHER
24+
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
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.getOperation() == AlterOperation.RENAME_TABLE) {
5357
accept(action.getNewTable(), tables);
5458
}

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

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

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

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1565,7 +1565,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15651565
int kind = getToken(1).kind;
15661566
if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT
15671567
|| isMySqlScalarTableOptionAhead() || kind == K_UNION
1568-
|| isMySqlStatisticsOptionAhead()) {
1568+
|| isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT")) {
15691569
return true;
15701570
}
15711571
if (kind == K_CHARACTER && getToken(2).kind == K_SET
@@ -15237,6 +15237,13 @@ TableOption MySqlTableOption(): {
1523715237
"Expected DEFAULT, 0, or 1 for " + name);
1523815238
}
1523915239
|
15240+
LOOKAHEAD({ isKeywordAhead("ROW_FORMAT") }) ContextualKeyword("ROW_FORMAT")
15241+
{ name = "ROW_FORMAT"; kind = TableOption.Kind.ROW_FORMAT; }
15242+
[ "=" { useEquals = true; } ] value=RelObjectName() {
15243+
requireDdlSyntax(Arrays.asList("DEFAULT", "DYNAMIC", "FIXED", "COMPRESSED", "REDUNDANT", "COMPACT")
15244+
.contains(value.toUpperCase(Locale.ROOT)), "Invalid MySQL ROW_FORMAT");
15245+
}
15246+
|
1524015247
tk=<K_ENGINE> {
1524115248
name = tk.image;
1524215249
kind = TableOption.Kind.ENGINE;
@@ -15307,6 +15314,31 @@ TableOption MySqlScalarTableOption():
1530715314
{ return new TableOption(kind, name, value, useEquals); }
1530815315
}
1530915316

15317+
/** Statistics and row format share CREATE's validation and option nodes. */
15318+
AlterExpressionTableOption AlterSharedTableOption():
15319+
{ TableOption option; AlterExpressionTableOption action = new AlterExpressionTableOption(); }
15320+
{
15321+
option=MySqlTableOption()
15322+
{ action.setStructuredTableOption(option); return action; }
15323+
}
15324+
15325+
AlterExpressionOrderBy MySqlAlterOrderBy():
15326+
{ AlterExpressionOrderBy action = new AlterExpressionOrderBy(); OrderByElement element; }
15327+
{
15328+
<K_ORDER> <K_BY> element=MySqlAlterOrderColumn() { action.getOrderByElements().add(element); }
15329+
( LOOKAHEAD("," RelObjectName() [ <K_ASC> | <K_DESC> ] ( "," | <ST_SEMICOLON> | <EOF> | <K_END> ))
15330+
"," element=MySqlAlterOrderColumn() { action.getOrderByElements().add(element); } )*
15331+
{ return action; }
15332+
}
15333+
15334+
OrderByElement MySqlAlterOrderColumn():
15335+
{ OrderByElement element = new OrderByElement(); String name; Token order; }
15336+
{
15337+
name=RelObjectName() { element.setExpression(createColumn(name)); }
15338+
[ LOOKAHEAD(2) ( order=<K_ASC> | order=<K_DESC> ) { element.setAsc(order.kind == K_ASC); element.setAscDescPresent(true); } ]
15339+
{ return element; }
15340+
}
15341+
1531015342
AlterExpressionTableOption AlterScalarTableOption():
1531115343
{
1531215344
TableOption option;
@@ -17754,6 +17786,11 @@ AlterExpression AlterExpression():
1775417786
{
1775517787

1775617788
(
17789+
LOOKAHEAD(<K_ORDER> <K_BY>) alterExp=MySqlAlterOrderBy()
17790+
|
17791+
LOOKAHEAD({ isMySqlStatisticsOptionAhead() || isKeywordAhead("ROW_FORMAT") })
17792+
alterExp=AlterSharedTableOption()
17793+
|
1775717794
LOOKAHEAD(<K_ALTER> <K_PRIMARY> <K_KEY>,
1775817795
{ Dialect.COCKROACHDB.name().equals(getAsString(Feature.dialect)) })
1775917796
alterExp = CockroachAlterPrimaryKey()
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
"ORDER BY id DESC, label ASC, ENGINE = InnoDB", "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"}) {
83+
assertThrows(JSQLParserException.class, () -> parse("ALTER TABLE t " + actions));
84+
}
85+
}
86+
87+
private static Alter parse(String sql) throws JSQLParserException {
88+
return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL));
89+
}
90+
91+
private static void assertRoundTrip(Alter alter) throws JSQLParserException {
92+
StringBuilder sql = new StringBuilder();
93+
alter.accept(new StatementDeParser(sql), null);
94+
assertEquals(alter.toString(), sql.toString());
95+
assertEquals(alter.toString(), parse(sql.toString()).toString());
96+
}
97+
}

0 commit comments

Comments
 (0)