Skip to content

Commit 8e2d660

Browse files
authored
feat: parse MySQL table option lists and UNION sources (#2656)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 0db9e0e commit 8e2d660

4 files changed

Lines changed: 193 additions & 23 deletions

File tree

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@
1515
import java.util.Collections;
1616
import java.util.List;
1717
import net.sf.jsqlparser.statement.select.PlainSelect;
18+
import net.sf.jsqlparser.schema.Table;
1819

1920
/** A structured option following a {@code CREATE TABLE} definition. */
2021
public class TableOption implements Serializable {
2122

2223
public enum Kind {
23-
ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, OTHER
24+
ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, OTHER
2425
}
2526

2627
private Kind kind = Kind.OTHER;
2728
private String name;
2829
private String value;
2930
private boolean useEquals;
3031
private List<String> tokens;
32+
private List<Table> unionTables;
3133

3234
public TableOption() {}
3335

@@ -54,6 +56,9 @@ public Kind getKind() {
5456

5557
public void setKind(Kind kind) {
5658
this.kind = kind;
59+
if (kind != Kind.UNION) {
60+
unionTables = null;
61+
}
5762
}
5863

5964
public String getName() {
@@ -65,11 +70,31 @@ public void setName(String name) {
6570
}
6671

6772
public String getValue() {
68-
return value;
73+
return unionTables == null ? value : PlainSelect.getStringList(unionTables, true, true);
6974
}
7075

7176
public void setValue(String value) {
7277
this.value = value;
78+
unionTables = null;
79+
}
80+
81+
/** Returns the mutable MERGE table sources, including an empty UNION list. */
82+
public List<Table> getUnionTables() {
83+
return unionTables;
84+
}
85+
86+
/** Replaces raw option contents with structured UNION table references. */
87+
public void setUnionTables(List<Table> unionTables) {
88+
this.unionTables = unionTables;
89+
kind = Kind.UNION;
90+
name = "UNION";
91+
value = null;
92+
tokens = null;
93+
}
94+
95+
public TableOption withUnionTables(List<Table> unionTables) {
96+
setUnionTables(unionTables);
97+
return this;
7398
}
7499

75100
public boolean isUseEquals() {
@@ -92,14 +117,18 @@ public List<String> getTokens() {
92117
if (useEquals) {
93118
result.add("=");
94119
}
95-
if (value != null) {
96-
result.add(value);
120+
String renderedValue = getValue();
121+
if (renderedValue != null) {
122+
result.add(renderedValue);
97123
}
98124
return Collections.unmodifiableList(result);
99125
}
100126

101127
public void setTokens(List<String> tokens) {
102128
this.tokens = tokens;
129+
if (tokens != null) {
130+
unionTables = null;
131+
}
103132
}
104133

105134
public TableOption withKind(Kind kind) {
@@ -127,6 +156,7 @@ public String toString() {
127156
if (tokens != null) {
128157
return PlainSelect.getStringList(tokens, false, false);
129158
}
130-
return name + (value != null ? (useEquals ? " = " : " ") + value : "");
159+
String renderedValue = getValue();
160+
return name + (renderedValue != null ? (useEquals ? " = " : " ") + renderedValue : "");
131161
}
132162
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ public static void visit(CreateTable table, Consumer<Expression> expressions,
9797
table.getIndexes().forEach(index -> visit(index, expressions, tables));
9898
}
9999
}
100+
if (table.getTableOptions() != null) {
101+
table.getTableOptions().forEach(option -> {
102+
if (option.getUnionTables() != null) {
103+
option.getUnionTables().forEach(source -> accept(source, tables));
104+
}
105+
});
106+
}
100107
accept(table.getTrailingLikeTable(), tables);
101108
accept(table.getPartitionOf(), tables);
102109
visit(table.getPartitioning(), expressions);

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

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,10 +1515,18 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15151515
|| "\"XML\"".equalsIgnoreCase(name);
15161516
}
15171517

1518+
private boolean isMySqlStatisticsOptionAhead() {
1519+
String name = getToken(1).image;
1520+
return "STATS_AUTO_RECALC".equalsIgnoreCase(name)
1521+
|| "STATS_PERSISTENT".equalsIgnoreCase(name)
1522+
|| "STATS_SAMPLE_PAGES".equalsIgnoreCase(name);
1523+
}
1524+
15181525
private boolean isMySqlTableOptionAhead() {
15191526
int kind = getToken(1).kind;
15201527
if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT
1521-
|| kind == K_AUTO_INCREMENT) {
1528+
|| kind == K_AUTO_INCREMENT || kind == K_UNION
1529+
|| isMySqlStatisticsOptionAhead()) {
15221530
return true;
15231531
}
15241532
if (kind == K_CHARACTER && getToken(2).kind == K_SET
@@ -14823,7 +14831,6 @@ CreateTable CreateTable(boolean isUsingOrReplace):
1482314831
Table table = null;
1482414832
List<TableElement> tableElements = new ArrayList<TableElement>();
1482514833
TableElement element;
14826-
List<String> tableOptions = new ArrayList<String>();
1482714834
List<TableOption> typedTableOptions = new ArrayList<TableOption>();
1482814835
List<String> createOptions = new ArrayList<String>();
1482914836
Token tk = null;
@@ -14881,21 +14888,7 @@ CreateTable CreateTable(boolean isUsingOrReplace):
1488114888
[ LOOKAHEAD({ partitionOfTable != null })
1488214889
partitionBound=PostgreSqlPartitionBound()
1488314890
{ createTable.setPartitionBound(partitionBound); } ]
14884-
( LOOKAHEAD(2, { getToken(1).kind != K_AS
14885-
&& !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) })
14886-
(
14887-
LOOKAHEAD({ isMySqlTableOptionAhead() })
14888-
tableOption=MySqlTableOption() {
14889-
typedTableOptions.add(tableOption);
14890-
tableOptions.addAll(tableOption.getTokens());
14891-
}
14892-
|
14893-
parameter=CreateParameter() {
14894-
typedTableOptions.add(TableOption.raw(parameter));
14895-
tableOptions.addAll(parameter);
14896-
}
14897-
)
14898-
)*
14891+
CreateTableOptions(typedTableOptions)
1489914892
[ partitioning=CreateTablePartitioning() { createTable.setPartitioning(partitioning); } ]
1490014893

1490114894
// see https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_7002.htm#i2126725
@@ -14955,16 +14948,59 @@ ColumnDefinition CreateTableColumnDefinition(boolean typed):
1495514948
{ if (true) { return column; } }
1495614949
}
1495714950

14951+
/** Reads complete options and keeps commas separate from legacy raw token groups. */
14952+
void CreateTableOptions(List<TableOption> options):
14953+
{ TableOption option; }
14954+
{
14955+
[ LOOKAHEAD(2, { getToken(1).kind != K_AS
14956+
&& !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) })
14957+
option=CreateTableOption() { options.add(option); }
14958+
( LOOKAHEAD(2, { getToken(1).kind != K_AS
14959+
&& !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY)
14960+
&& !(getToken(1).kind == K_COMMA
14961+
&& "INTERLEAVE".equalsIgnoreCase(getToken(2).image)) })
14962+
[ "," ] option=CreateTableOption() { options.add(option); }
14963+
)*
14964+
]
14965+
}
14966+
14967+
TableOption CreateTableOption():
14968+
{ TableOption option; List<String> tokens; }
14969+
{
14970+
( LOOKAHEAD({ isMySqlTableOptionAhead() }) option=MySqlTableOption()
14971+
| tokens=CreateParameter() { option = TableOption.raw(tokens); } )
14972+
{ return option; }
14973+
}
14974+
1495814975
TableOption MySqlTableOption(): {
1495914976
Token tk;
1496014977
Token tk2 = null;
14961-
String value;
14978+
String value = null;
14979+
List<Table> unionTables = null;
14980+
Table unionTable;
1496214981
boolean useEquals = false;
1496314982
String name = "";
1496414983
TableOption.Kind kind = null;
1496514984
TableOption option;
1496614985
} {
1496714986
(
14987+
tk=<K_UNION> { name = tk.image; kind = TableOption.Kind.UNION;
14988+
unionTables = new ArrayList<Table>(); }
14989+
[ "=" { useEquals = true; } ]
14990+
"(" [ unionTable=Table() { unionTables.add(unionTable); }
14991+
( "," unionTable=Table() { unionTables.add(unionTable); } )* ] ")"
14992+
|
14993+
LOOKAHEAD({ isMySqlStatisticsOptionAhead() }) tk=<S_IDENTIFIER> {
14994+
name = tk.image;
14995+
kind = TableOption.Kind.valueOf(name.toUpperCase(Locale.ROOT));
14996+
}
14997+
[ "=" { useEquals = true; } ]
14998+
( tk=<K_DEFAULT> | tk=<S_LONG> ) { value = tk.image;
14999+
requireDdlSyntax(kind == TableOption.Kind.STATS_SAMPLE_PAGES
15000+
|| "DEFAULT".equalsIgnoreCase(value) || "0".equals(value) || "1".equals(value),
15001+
"Expected DEFAULT, 0, or 1 for " + name);
15002+
}
15003+
|
1496815004
tk=<K_ENGINE> {
1496915005
name = tk.image;
1497015006
kind = TableOption.Kind.ENGINE;
@@ -15009,6 +15045,7 @@ TableOption MySqlTableOption(): {
1500915045
)
1501015046
{
1501115047
option = new TableOption(kind, name, value, useEquals);
15048+
if (unionTables != null) { option.setUnionTables(unionTables); }
1501215049
return option;
1501315050
}
1501415051
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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.create;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertThrows;
15+
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
18+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
19+
import net.sf.jsqlparser.statement.create.table.CreateTable;
20+
import net.sf.jsqlparser.statement.create.table.TableOption;
21+
import net.sf.jsqlparser.util.TablesNamesFinder;
22+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
26+
27+
class MySqlTableOptionsTest {
28+
@ParameterizedTest
29+
@ValueSource(strings = {
30+
"STATS_PERSISTENT=1, STATS_AUTO_RECALC=1",
31+
"STATS_PERSISTENT DEFAULT, STATS_AUTO_RECALC=0 STATS_SAMPLE_PAGES=32",
32+
"ENGINE=InnoDB, DEFAULT CHARSET=utf8mb4",
33+
"ENGINE=InnoDB, COMMENT='commas, in text', AUTO_INCREMENT=10",
34+
"UNION=(t1,t2) ENGINE=MRG_MyISAM",
35+
"ENGINE=MRG_MyISAM, UNION (db.t1, `db`.`t2`)",
36+
"UNION=() ENGINE=MRG_MyISAM",
37+
"ENGINE=InnoDB, STATS_PERSISTENT=1 PARTITION BY HASH(id) PARTITIONS 2",
38+
"ENGINE=InnoDB, STATS_AUTO_RECALC=DEFAULT AS SELECT 1 AS other"
39+
})
40+
void parsesOptionSeparatorsAndValues(String options) throws JSQLParserException {
41+
CreateTable table = parse("CREATE TABLE t (id INT) " + options);
42+
assertRoundTrip(table);
43+
assertEquals(table.toString(), CCJSqlParserUtil.parse(table.toString()).toString());
44+
}
45+
46+
@Test
47+
void statisticsOptionsHaveIndependentValues() throws JSQLParserException {
48+
CreateTable table = parse("CREATE TABLE t (id INT) "
49+
+ "STATS_PERSISTENT=1, STATS_AUTO_RECALC=DEFAULT");
50+
TableOption persistent =
51+
table.getTableOption(TableOption.Kind.STATS_PERSISTENT).orElseThrow();
52+
assertEquals("1", persistent.getValue());
53+
persistent.setValue("0");
54+
assertThat(table.toString()).contains("STATS_PERSISTENT = 0 STATS_AUTO_RECALC = DEFAULT");
55+
assertRoundTrip(table);
56+
}
57+
58+
@Test
59+
void unionSourcesAreTraversableAndMutable() throws JSQLParserException {
60+
CreateTable table =
61+
parse("CREATE TABLE merged (id INT) UNION=(db.t1,t2) ENGINE=MRG_MyISAM");
62+
TableOption union = table.getTableOption(TableOption.Kind.UNION).orElseThrow();
63+
assertEquals(2, union.getUnionTables().size());
64+
assertEquals("db", union.getUnionTables().get(0).getSchemaName());
65+
union.getUnionTables().get(0).setName("changed");
66+
assertEquals("(db.changed, t2)", union.getValue());
67+
assertThat(table.getTableOptionsStrings()).contains("(db.changed, t2)");
68+
assertThat(new TablesNamesFinder().getTables(table))
69+
.containsExactlyInAnyOrder("merged", "db.changed", "t2");
70+
assertRoundTrip(table);
71+
union.getUnionTables().clear();
72+
assertEquals("()", union.getValue());
73+
assertRoundTrip(table);
74+
union.setValue("(replacement)");
75+
assertEquals("(replacement)", union.getValue());
76+
assertRoundTrip(table);
77+
}
78+
79+
@ParameterizedTest
80+
@ValueSource(strings = {", ENGINE=InnoDB", "ENGINE=InnoDB,", "ENGINE=InnoDB,, COMMENT='x'",
81+
"UNION=(t1,)", "UNION=", "STATS_PERSISTENT=2", "STATS_AUTO_RECALC=-1"})
82+
void rejectsIncompleteListsAndInvalidBooleanOptions(String options) {
83+
assertThrows(JSQLParserException.class, () -> parse("CREATE TABLE t (id INT) " + options));
84+
}
85+
86+
private static CreateTable parse(String sql) throws JSQLParserException {
87+
return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL));
88+
}
89+
90+
private static void assertRoundTrip(CreateTable table) throws JSQLParserException {
91+
StringBuilder buffer = new StringBuilder();
92+
table.accept(new StatementDeParser(buffer), null);
93+
assertEquals(table.toString(), buffer.toString());
94+
assertEquals(table.toString(), parse(buffer.toString()).toString());
95+
}
96+
}

0 commit comments

Comments
 (0)