Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@
import java.util.Collections;
import java.util.List;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.schema.Table;

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

public enum Kind {
ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, OTHER
ENGINE, CHARACTER_SET, COLLATE, COMMENT, AUTO_INCREMENT, STATS_AUTO_RECALC, STATS_PERSISTENT, STATS_SAMPLE_PAGES, UNION, OTHER
}

private Kind kind = Kind.OTHER;
private String name;
private String value;
private boolean useEquals;
private List<String> tokens;
private List<Table> unionTables;

public TableOption() {}

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

public void setKind(Kind kind) {
this.kind = kind;
if (kind != Kind.UNION) {
unionTables = null;
}
}

public String getName() {
Expand All @@ -65,11 +70,31 @@ public void setName(String name) {
}

public String getValue() {
return value;
return unionTables == null ? value : PlainSelect.getStringList(unionTables, true, true);
}

public void setValue(String value) {
this.value = value;
unionTables = null;
}

/** Returns the mutable MERGE table sources, including an empty UNION list. */
public List<Table> getUnionTables() {
return unionTables;
}

/** Replaces raw option contents with structured UNION table references. */
public void setUnionTables(List<Table> unionTables) {
this.unionTables = unionTables;
kind = Kind.UNION;
name = "UNION";
value = null;
tokens = null;
}

public TableOption withUnionTables(List<Table> unionTables) {
setUnionTables(unionTables);
return this;
}

public boolean isUseEquals() {
Expand All @@ -92,14 +117,18 @@ public List<String> getTokens() {
if (useEquals) {
result.add("=");
}
if (value != null) {
result.add(value);
String renderedValue = getValue();
if (renderedValue != null) {
result.add(renderedValue);
}
return Collections.unmodifiableList(result);
}

public void setTokens(List<String> tokens) {
this.tokens = tokens;
if (tokens != null) {
unionTables = null;
}
}

public TableOption withKind(Kind kind) {
Expand Down Expand Up @@ -127,6 +156,7 @@ public String toString() {
if (tokens != null) {
return PlainSelect.getStringList(tokens, false, false);
}
return name + (value != null ? (useEquals ? " = " : " ") + value : "");
String renderedValue = getValue();
return name + (renderedValue != null ? (useEquals ? " = " : " ") + renderedValue : "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ public static void visit(CreateTable table, Consumer<Expression> expressions,
table.getIndexes().forEach(index -> visit(index, expressions, tables));
}
}
if (table.getTableOptions() != null) {
table.getTableOptions().forEach(option -> {
if (option.getUnionTables() != null) {
option.getUnionTables().forEach(source -> accept(source, tables));
}
});
}
accept(table.getTrailingLikeTable(), tables);
accept(table.getPartitionOf(), tables);
visit(table.getPartitioning(), expressions);
Expand Down
73 changes: 55 additions & 18 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1515,10 +1515,18 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
|| "\"XML\"".equalsIgnoreCase(name);
}

private boolean isMySqlStatisticsOptionAhead() {
String name = getToken(1).image;
return "STATS_AUTO_RECALC".equalsIgnoreCase(name)
|| "STATS_PERSISTENT".equalsIgnoreCase(name)
|| "STATS_SAMPLE_PAGES".equalsIgnoreCase(name);
}

private boolean isMySqlTableOptionAhead() {
int kind = getToken(1).kind;
if (kind == K_ENGINE || kind == K_COLLATE || kind == K_COMMENT
|| kind == K_AUTO_INCREMENT) {
|| kind == K_AUTO_INCREMENT || kind == K_UNION
|| isMySqlStatisticsOptionAhead()) {
return true;
}
if (kind == K_CHARACTER && getToken(2).kind == K_SET
Expand Down Expand Up @@ -14803,7 +14811,6 @@ CreateTable CreateTable(boolean isUsingOrReplace):
Table table = null;
List<TableElement> tableElements = new ArrayList<TableElement>();
TableElement element;
List<String> tableOptions = new ArrayList<String>();
List<TableOption> typedTableOptions = new ArrayList<TableOption>();
List<String> createOptions = new ArrayList<String>();
Token tk = null;
Expand Down Expand Up @@ -14861,21 +14868,7 @@ CreateTable CreateTable(boolean isUsingOrReplace):
[ LOOKAHEAD({ partitionOfTable != null })
partitionBound=PostgreSqlPartitionBound()
{ createTable.setPartitionBound(partitionBound); } ]
( LOOKAHEAD(2, { getToken(1).kind != K_AS
&& !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) })
(
LOOKAHEAD({ isMySqlTableOptionAhead() })
tableOption=MySqlTableOption() {
typedTableOptions.add(tableOption);
tableOptions.addAll(tableOption.getTokens());
}
|
parameter=CreateParameter() {
typedTableOptions.add(TableOption.raw(parameter));
tableOptions.addAll(parameter);
}
)
)*
CreateTableOptions(typedTableOptions)
[ partitioning=CreateTablePartitioning() { createTable.setPartitioning(partitioning); } ]

// see https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_7002.htm#i2126725
Expand Down Expand Up @@ -14935,16 +14928,59 @@ ColumnDefinition CreateTableColumnDefinition(boolean typed):
{ if (true) { return column; } }
}

/** Reads complete options and keeps commas separate from legacy raw token groups. */
void CreateTableOptions(List<TableOption> options):
{ TableOption option; }
{
[ LOOKAHEAD(2, { getToken(1).kind != K_AS
&& !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) })
option=CreateTableOption() { options.add(option); }
( LOOKAHEAD(2, { getToken(1).kind != K_AS
&& !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY)
&& !(getToken(1).kind == K_COMMA
&& "INTERLEAVE".equalsIgnoreCase(getToken(2).image)) })
[ "," ] option=CreateTableOption() { options.add(option); }
)*
]
}

TableOption CreateTableOption():
{ TableOption option; List<String> tokens; }
{
( LOOKAHEAD({ isMySqlTableOptionAhead() }) option=MySqlTableOption()
| tokens=CreateParameter() { option = TableOption.raw(tokens); } )
{ return option; }
}

TableOption MySqlTableOption(): {
Token tk;
Token tk2 = null;
String value;
String value = null;
List<Table> unionTables = null;
Table unionTable;
boolean useEquals = false;
String name = "";
TableOption.Kind kind = null;
TableOption option;
} {
(
tk=<K_UNION> { name = tk.image; kind = TableOption.Kind.UNION;
unionTables = new ArrayList<Table>(); }
[ "=" { useEquals = true; } ]
"(" [ unionTable=Table() { unionTables.add(unionTable); }
( "," unionTable=Table() { unionTables.add(unionTable); } )* ] ")"
|
LOOKAHEAD({ isMySqlStatisticsOptionAhead() }) tk=<S_IDENTIFIER> {
name = tk.image;
kind = TableOption.Kind.valueOf(name.toUpperCase(Locale.ROOT));
}
[ "=" { useEquals = true; } ]
( tk=<K_DEFAULT> | tk=<S_LONG> ) { value = tk.image;
requireDdlSyntax(kind == TableOption.Kind.STATS_SAMPLE_PAGES
|| "DEFAULT".equalsIgnoreCase(value) || "0".equals(value) || "1".equals(value),
"Expected DEFAULT, 0, or 1 for " + name);
}
|
tk=<K_ENGINE> {
name = tk.image;
kind = TableOption.Kind.ENGINE;
Expand Down Expand Up @@ -14989,6 +15025,7 @@ TableOption MySqlTableOption(): {
)
{
option = new TableOption(kind, name, value, useEquals);
if (unionTables != null) { option.setUnionTables(unionTables); }
return option;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*-
* #%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.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.create.table.TableOption;
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.ValueSource;

class MySqlTableOptionsTest {
@ParameterizedTest
@ValueSource(strings = {
"STATS_PERSISTENT=1, STATS_AUTO_RECALC=1",
"STATS_PERSISTENT DEFAULT, STATS_AUTO_RECALC=0 STATS_SAMPLE_PAGES=32",
"ENGINE=InnoDB, DEFAULT CHARSET=utf8mb4",
"ENGINE=InnoDB, COMMENT='commas, in text', AUTO_INCREMENT=10",
"UNION=(t1,t2) ENGINE=MRG_MyISAM",
"ENGINE=MRG_MyISAM, UNION (db.t1, `db`.`t2`)",
"UNION=() ENGINE=MRG_MyISAM",
"ENGINE=InnoDB, STATS_PERSISTENT=1 PARTITION BY HASH(id) PARTITIONS 2",
"ENGINE=InnoDB, STATS_AUTO_RECALC=DEFAULT AS SELECT 1 AS other"
})
void parsesOptionSeparatorsAndValues(String options) throws JSQLParserException {
CreateTable table = parse("CREATE TABLE t (id INT) " + options);
assertRoundTrip(table);
assertEquals(table.toString(), CCJSqlParserUtil.parse(table.toString()).toString());
}

@Test
void statisticsOptionsHaveIndependentValues() throws JSQLParserException {
CreateTable table = parse("CREATE TABLE t (id INT) "
+ "STATS_PERSISTENT=1, STATS_AUTO_RECALC=DEFAULT");
TableOption persistent =
table.getTableOption(TableOption.Kind.STATS_PERSISTENT).orElseThrow();
assertEquals("1", persistent.getValue());
persistent.setValue("0");
assertThat(table.toString()).contains("STATS_PERSISTENT = 0 STATS_AUTO_RECALC = DEFAULT");
assertRoundTrip(table);
}

@Test
void unionSourcesAreTraversableAndMutable() throws JSQLParserException {
CreateTable table =
parse("CREATE TABLE merged (id INT) UNION=(db.t1,t2) ENGINE=MRG_MyISAM");
TableOption union = table.getTableOption(TableOption.Kind.UNION).orElseThrow();
assertEquals(2, union.getUnionTables().size());
assertEquals("db", union.getUnionTables().get(0).getSchemaName());
union.getUnionTables().get(0).setName("changed");
assertEquals("(db.changed, t2)", union.getValue());
assertThat(table.getTableOptionsStrings()).contains("(db.changed, t2)");
assertThat(new TablesNamesFinder().getTables(table))
.containsExactlyInAnyOrder("merged", "db.changed", "t2");
assertRoundTrip(table);
union.getUnionTables().clear();
assertEquals("()", union.getValue());
assertRoundTrip(table);
union.setValue("(replacement)");
assertEquals("(replacement)", union.getValue());
assertRoundTrip(table);
}

@ParameterizedTest
@ValueSource(strings = {", ENGINE=InnoDB", "ENGINE=InnoDB,", "ENGINE=InnoDB,, COMMENT='x'",
"UNION=(t1,)", "UNION=", "STATS_PERSISTENT=2", "STATS_AUTO_RECALC=-1"})
void rejectsIncompleteListsAndInvalidBooleanOptions(String options) {
assertThrows(JSQLParserException.class, () -> parse("CREATE TABLE t (id INT) " + options));
}

private static CreateTable parse(String sql) throws JSQLParserException {
return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL));
}

private static void assertRoundTrip(CreateTable table) throws JSQLParserException {
StringBuilder buffer = new StringBuilder();
table.accept(new StatementDeParser(buffer), null);
assertEquals(table.toString(), buffer.toString());
assertEquals(table.toString(), parse(buffer.toString()).toString());
}
}
Loading