Skip to content

Commit 8a11fb0

Browse files
authored
feat: model MySQL CTAS duplicate handling (#2690)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent d24e1e2 commit 8a11fb0

3 files changed

Lines changed: 104 additions & 3 deletions

File tree

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class CreateTable implements Statement {
3535
private List<Index> indexes;
3636
private List<TableElement> tableElements;
3737
private Select select;
38+
private DuplicateHandling duplicateHandling;
3839
private Table likeTable;
3940
private Table cloneTable;
4041
private ColDataType ofType;
@@ -51,6 +52,19 @@ public class CreateTable implements Statement {
5152

5253
private SpannerInterleaveIn interleaveIn = null;
5354

55+
public enum DuplicateHandling {
56+
IGNORE, REPLACE
57+
}
58+
59+
/** MySQL's duplicate-key handling when creating a table from a query; null if omitted. */
60+
public DuplicateHandling getDuplicateHandling() {
61+
return duplicateHandling;
62+
}
63+
64+
public void setDuplicateHandling(DuplicateHandling duplicateHandling) {
65+
this.duplicateHandling = duplicateHandling;
66+
}
67+
5468
@Override
5569
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
5670
return statementVisitor.visit(this, context);
@@ -217,6 +231,9 @@ public void setWithData(Boolean withData) {
217231
public StringBuilder appendSelectTo(StringBuilder builder,
218232
java.util.function.Consumer<Select> selectRenderer) {
219233
if (select != null) {
234+
if (duplicateHandling != null) {
235+
builder.append(' ').append(duplicateHandling);
236+
}
220237
builder.append(useAsKeyword ? " AS " : " ");
221238
if (selectParenthesis) {
222239
builder.append("(");

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
14401440
return isKeywordAhead("MATCH_RECOGNIZE") && getToken(2).kind == OPENING_BRACKET;
14411441
}
14421442

1443+
private boolean isCreateTableQueryPrefix() {
1444+
int kind = getToken(1).kind;
1445+
return kind == K_AS || kind == K_TABLE || kind == K_IGNORE || kind == K_REPLACE;
1446+
}
1447+
14431448
private boolean isRowPatternPrimaryAhead() {
14441449
Token next = getToken(1);
14451450
return next.kind == OPENING_BRACKET || next.kind == OP_CARET
@@ -15030,6 +15035,9 @@ CreateTable CreateTable(boolean isUsingOrReplace):
1503015035
[ rowMovement = RowMovement() { createTable.setRowMovement(rowMovement); }]
1503115036
[ LOOKAHEAD(2)
1503215037
{ createTable.setUseAsKeyword(false); }
15038+
[ ( tk=<K_IGNORE> | tk=<K_REPLACE> )
15039+
{ createTable.setDuplicateHandling(CreateTable.DuplicateHandling.valueOf(
15040+
tk.image.toUpperCase(Locale.ROOT))); } ]
1503315041
[ <K_AS> { createTable.setUseAsKeyword(true); } ]
1503415042
( LOOKAHEAD(<K_TABLE>) select=TableStatement() | select=Select() )
1503515043
{ createTable.setSelect(select, false); }
@@ -15093,10 +15101,10 @@ ColumnDefinition CreateTableColumnDefinition(boolean typed):
1509315101
void CreateTableOptions(List<TableOption> options):
1509415102
{ TableOption option; }
1509515103
{
15096-
[ LOOKAHEAD(2, { getToken(1).kind != K_AS && getToken(1).kind != K_TABLE
15104+
[ LOOKAHEAD(2, { !isCreateTableQueryPrefix()
1509715105
&& !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY) })
1509815106
option=CreateTableOption() { options.add(option); }
15099-
( LOOKAHEAD(2, { getToken(1).kind != K_AS && getToken(1).kind != K_TABLE
15107+
( LOOKAHEAD(2, { !isCreateTableQueryPrefix()
1510015108
&& !(getToken(1).kind == K_PARTITION && getToken(2).kind == K_BY)
1510115109
&& !(getToken(1).kind == K_COMMA
1510215110
&& "INTERLEAVE".equalsIgnoreCase(getToken(2).image)) })
@@ -16628,7 +16636,7 @@ TablePartitioning CreateTablePartitioning():
1662816636
]
1662916637
[ LOOKAHEAD(2) partitionDefinitions=PartitionDefinitions()
1663016638
{ partitioning.setPartitionDefinitions(partitionDefinitions); } ]
16631-
( LOOKAHEAD(2, { getToken(1).kind != K_AS })
16639+
( LOOKAHEAD(2, { !isCreateTableQueryPrefix() })
1663216640
parameter=CreateParameter() { partitionOptions.addAll(parameter); } )*
1663316641
{
1663416642
if (!partitionOptions.isEmpty()) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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.junit.jupiter.api.Assertions.*;
13+
import net.sf.jsqlparser.JSQLParserException;
14+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
15+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
16+
import net.sf.jsqlparser.statement.create.table.CreateTable;
17+
import net.sf.jsqlparser.statement.create.table.CreateTable.DuplicateHandling;
18+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.ValueSource;
22+
23+
class MySqlCreateTableDuplicatesTest {
24+
@ParameterizedTest
25+
@ValueSource(strings = {"SELECT id FROM source", "TABLE source", "VALUES ROW(1), ROW(2)",
26+
"(SELECT id FROM source)", "WITH c AS (SELECT 1 AS id) SELECT id FROM c"})
27+
void retainsModifierAndSourceAfterTableOptions(String query) throws JSQLParserException {
28+
for (DuplicateHandling handling : DuplicateHandling.values()) {
29+
for (boolean useAs : new boolean[] {false, true}) {
30+
CreateTable table = parse("CREATE TABLE t (id INT PRIMARY KEY) ENGINE=InnoDB "
31+
+ handling + (useAs ? " AS " : " ") + query);
32+
assertEquals(handling, table.getDuplicateHandling());
33+
assertEquals(useAs, table.isUseAsKeyword());
34+
assertNotNull(table.getSelect());
35+
roundTrip(table);
36+
}
37+
}
38+
}
39+
40+
@Test
41+
void preservesPartitionAndAllowsAstMutation() throws JSQLParserException {
42+
CreateTable table = parse("CREATE TABLE t (id INT PRIMARY KEY) PARTITION BY HASH(id) "
43+
+ "PARTITIONS 2 IGNORE AS SELECT id FROM source");
44+
assertNotNull(table.getPartitioning());
45+
assertNull(table.getPartitioning().getPartitionOptions());
46+
table.setDuplicateHandling(DuplicateHandling.REPLACE);
47+
assertTrue(table.toString().contains(" REPLACE AS SELECT"));
48+
roundTrip(table);
49+
table.setDuplicateHandling(null);
50+
assertFalse(table.toString().contains("REPLACE"));
51+
roundTrip(table);
52+
assertNull(parse("CREATE TABLE t AS SELECT 1").getDuplicateHandling());
53+
}
54+
55+
@ParameterizedTest
56+
@ValueSource(strings = {"CREATE TABLE t IGNORE", "CREATE TABLE t REPLACE AS",
57+
"CREATE TABLE t IGNORE REPLACE SELECT 1",
58+
"CREATE TABLE t ENGINE=InnoDB, IGNORE SELECT 1"})
59+
void rejectsMissingQueriesAndConflictingModifiers(String sql) {
60+
assertThrows(JSQLParserException.class, () -> parse(sql));
61+
}
62+
63+
private static CreateTable parse(String sql) throws JSQLParserException {
64+
return (CreateTable) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL));
65+
}
66+
67+
private static void roundTrip(CreateTable table) throws JSQLParserException {
68+
StringBuilder out = new StringBuilder();
69+
table.accept(new StatementDeParser(out));
70+
assertEquals(table.toString(), out.toString());
71+
CreateTable parsed = parse(out.toString());
72+
assertEquals(table.getDuplicateHandling(), parsed.getDuplicateHandling());
73+
assertEquals(table.isUseAsKeyword(), parsed.isUseAsKeyword());
74+
assertEquals(out.toString(), parsed.toString());
75+
}
76+
}

0 commit comments

Comments
 (0)