From cf51671fc36dee09ec8e7a623a546bbc8fddd8ce Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Thu, 3 Sep 2026 16:23:42 +0900 Subject: [PATCH] fix(parser): distinguish MySQL constraint and index names --- .../create/table/CheckConstraint.java | 13 +++++- .../create/table/ForeignKeyIndex.java | 10 +++++ .../create/table/NamedConstraint.java | 44 ++++++++++++++++++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 34 ++++++++++---- .../statement/create/CreateTableTest.java | 43 ++++++++++++++++++ 5 files changed, 132 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java index 79d13b18ce..c9ee2be53f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java @@ -50,8 +50,12 @@ public void setEnforced(Boolean enforced) { @Override public String toString() { StringBuilder b = new StringBuilder(); - if (getName() != null) { - b.append("CONSTRAINT ").append(getName()).append(" "); + if (isUseConstraintKeyword() || getName() != null) { + b.append("CONSTRAINT"); + if (getName() != null) { + b.append(" ").append(getName()); + } + b.append(" "); } b.append("CHECK (").append(expression).append(")"); if (enforced != null) { @@ -89,6 +93,11 @@ public CheckConstraint withUsing(String using) { return (CheckConstraint) super.withUsing(using); } + @Override + public CheckConstraint withUseConstraintKeyword(boolean useConstraintKeyword) { + return (CheckConstraint) super.withUseConstraintKeyword(useConstraintKeyword); + } + @Override public CheckConstraint withName(List name) { return (CheckConstraint) super.withName(name); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java index c877a1ad9e..1ad10cb165 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java @@ -169,6 +169,16 @@ public ForeignKeyIndex withUsing(String using) { return (ForeignKeyIndex) super.withUsing(using); } + @Override + public ForeignKeyIndex withIndexName(String indexName) { + return (ForeignKeyIndex) super.withIndexName(indexName); + } + + @Override + public ForeignKeyIndex withUseConstraintKeyword(boolean useConstraintKeyword) { + return (ForeignKeyIndex) super.withUseConstraintKeyword(useConstraintKeyword); + } + @Override public ForeignKeyIndex withName(List name) { return (ForeignKeyIndex) super.withName(name); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java index ee2cce33bc..9746b26f20 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java @@ -16,15 +16,55 @@ public class NamedConstraint extends Index { + private String indexName; + private boolean useConstraintKeyword; + + /** + * Returns the optional index name declared after the constraint type. This is distinct from + * {@link #getName()}, which represents the optional constraint symbol. + * + * @return the index name, or {@code null} when it was omitted + */ + public String getIndexName() { + return indexName; + } + + public void setIndexName(String indexName) { + this.indexName = indexName; + } + + public boolean isUseConstraintKeyword() { + return useConstraintKeyword; + } + + public void setUseConstraintKeyword(boolean useConstraintKeyword) { + this.useConstraintKeyword = useConstraintKeyword; + } + @Override public String toString() { String idxSpecText = PlainSelect.getStringList(getIndexSpec(), false, false); - String head = getName() != null ? "CONSTRAINT " + getName() + " " : ""; - String tail = getType() + " " + PlainSelect.getStringList(getColumnsNames(), true, true) + + String head = useConstraintKeyword || getName() != null + ? "CONSTRAINT" + (getName() != null ? " " + getName() : "") + " " + : ""; + String tail = getType() + + (indexName != null ? " " + indexName : "") + + (getUsing() != null ? " USING " + getUsing() : "") + + " " + PlainSelect.getStringList(getColumnsNames(), true, true) + (!"".equals(idxSpecText) ? " " + idxSpecText : ""); return head + tail; } + public NamedConstraint withIndexName(String indexName) { + setIndexName(indexName); + return this; + } + + public NamedConstraint withUseConstraintKeyword(boolean useConstraintKeyword) { + setUseConstraintKeyword(useConstraintKeyword); + return this; + } + @Override public NamedConstraint withName(List name) { return (NamedConstraint) super.withName(name); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8ed4a18d54..f6ca0bf286 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -11446,6 +11446,9 @@ Index CreateTableConstraint(): Token tk2 = null; Token tk3 = null; String sk3 = null; + String indexName = null; + String using = null; + boolean useConstraintKeyword = false; List colNames = null; List parameter = new ArrayList(); List idxSpec = new ArrayList(); @@ -11473,16 +11476,25 @@ Index CreateTableConstraint(): { index = new NamedConstraint(); tk2=null; + indexName=null; + using=null; idxSpec.clear(); } - [ sk3=RelObjectName() {index.setName(sk3);} ] + [ { ((NamedConstraint) index).setUseConstraintKeyword(true); } + [ LOOKAHEAD({ getToken(1).kind != K_PRIMARY && getToken(1).kind != K_UNIQUE }) + sk3=RelObjectName() {index.setName(sk3);} ] + ] ( tk= tk2= | - tk= [ tk2= ] + tk= [ LOOKAHEAD(2) (tk2= | tk2=) ] + [ LOOKAHEAD(2, { getToken(1).kind != K_USING }) indexName=RelObjectName() ] + [ LOOKAHEAD(2) using=UsingIndexType() ] ) { index.setType( tk.image + ( tk2!=null ? " " + tk2.image : "" )); + ((NamedConstraint) index).setIndexName(indexName); + index.setUsing(using); tk2=null; } colNames = ColumnNamesWithParamsList() @@ -11513,17 +11525,21 @@ Index CreateTableConstraint(): ) | LOOKAHEAD(3) ( - { sk3=null; } - [ sk3=RelObjectName() ] + { sk3=null; useConstraintKeyword=false; } + [ { useConstraintKeyword=true; } + [ LOOKAHEAD({ getToken(1).kind != K_FOREIGN }) sk3=RelObjectName() ] + ] fkIndex = ForeignKeySpec(sk3) - { index = fkIndex; } + { fkIndex.setUseConstraintKeyword(useConstraintKeyword); index = fkIndex; } ) | LOOKAHEAD(3) ( - { sk3 = null; } - [ sk3 = RelObjectName() ] + { sk3 = null; useConstraintKeyword=false; } + [ { useConstraintKeyword=true; } + [ LOOKAHEAD({ getToken(1).kind != K_CHECK }) sk3 = RelObjectName() ] + ] checkCs = CheckConstraintSpec(sk3) - { index = checkCs; } + { checkCs.setUseConstraintKeyword(useConstraintKeyword); index = checkCs; } ) | LOOKAHEAD(2) ( @@ -11972,6 +11988,7 @@ CheckConstraint CheckConstraintSpec(String constraintName): ForeignKeyIndex ForeignKeySpec(String constraintName): { ForeignKeyIndex fkIndex = new ForeignKeyIndex(); + String indexName = null; Token tk; Token tk2; List refColNames = null; @@ -11980,6 +11997,7 @@ ForeignKeyIndex ForeignKeySpec(String constraintName): } { tk= tk2= + [ LOOKAHEAD(2) indexName=RelObjectName() { fkIndex.setIndexName(indexName); } ] colNames = ColumnNamesWithParamsList() { if (constraintName != null) { fkIndex.setName(constraintName); } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java index a58caea808..e006473c0e 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateTableTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.BufferedReader; @@ -35,7 +36,9 @@ import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex; import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; import net.sf.jsqlparser.statement.create.table.RowMovementMode; import net.sf.jsqlparser.test.TestException; import org.assertj.core.api.Assertions; @@ -1247,4 +1250,44 @@ void testUniqueIndexIssue1893() throws JSQLParserException { // A plain INDEX must still parse unchanged. assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a int, INDEX idx (a))", true); } + + @Test + void testMySqlConstraintSymbolAndIndexNameIssue1570() throws JSQLParserException { + String uniqueSql = "CREATE TABLE table1 (col1 INT, col2 INT UNIQUE, " + + "CONSTRAINT my_constraint UNIQUE KEY index_name (col1))"; + CreateTable uniqueTable = (CreateTable) assertSqlCanBeParsedAndDeparsed(uniqueSql, true); + NamedConstraint unique = (NamedConstraint) uniqueTable.getIndexes().get(0); + + assertEquals("my_constraint", unique.getName()); + assertEquals("index_name", unique.getIndexName()); + assertEquals("UNIQUE KEY", unique.getType()); + + String foreignKeySql = "CREATE TABLE table2 (id INT, parent_id INT, " + + "CONSTRAINT fk_parent FOREIGN KEY fk_parent_idx (parent_id) " + + "REFERENCES parent (id))"; + CreateTable foreignKeyTable = + (CreateTable) assertSqlCanBeParsedAndDeparsed(foreignKeySql, true); + ForeignKeyIndex foreignKey = (ForeignKeyIndex) foreignKeyTable.getIndexes().get(0); + + assertEquals("fk_parent", foreignKey.getName()); + assertEquals("fk_parent_idx", foreignKey.getIndexName()); + } + + @Test + void testMySqlUnnamedConstraintAndUniqueIndexOptionsIssues1570And538() + throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE table1 (col1 INT, CONSTRAINT UNIQUE KEY (col1))", true); + + String sql = "CREATE TABLE g_platform_payway (id INT, platform_code VARCHAR (45), " + + "pay_way VARCHAR (32), UNIQUE uniq_platform_payway USING BTREE " + + "(platform_code, pay_way) COMMENT 'should be unique')"; + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed(sql, true); + NamedConstraint unique = (NamedConstraint) createTable.getIndexes().get(0); + + assertNull(unique.getName()); + assertEquals("uniq_platform_payway", unique.getIndexName()); + assertEquals("BTREE", unique.getUsing()); + assertEquals(List.of("COMMENT", "'should be unique'"), unique.getIndexSpec()); + } }