From 515e389dfbd0bc725781396474dd2195b5c79911 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 18 Sep 2026 13:00:00 +0900 Subject: [PATCH] fix: refresh index classification when replacing its type --- .../statement/create/table/Index.java | 61 +++++++---- .../statement/create/IndexKindStateTest.java | 101 ++++++++++++++++++ 2 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/IndexKindStateTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java index bb60df7a0..bf1bbbf40 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java @@ -217,29 +217,43 @@ public String getType() { return type; } + /** + * Sets the rendered type and refreshes its classification, including when replacing an existing + * type. Null and unrecognized types reset the classification to {@link Kind#OTHER}. + */ public void setType(String string) { type = string; - if (kind == Kind.OTHER && string != null) { - String normalized = string.toUpperCase(java.util.Locale.ROOT); - if (normalized.startsWith("PRIMARY")) { - kind = Kind.PRIMARY_KEY; - } else if (normalized.startsWith("UNIQUE")) { - kind = Kind.UNIQUE; - } else if (normalized.startsWith("FULLTEXT")) { - kind = Kind.FULLTEXT; - } else if (normalized.startsWith("SPATIAL")) { - kind = Kind.SPATIAL; - } else if (normalized.startsWith("FOREIGN")) { - kind = Kind.FOREIGN_KEY; - } else if (normalized.startsWith("CHECK")) { - kind = Kind.CHECK; - } else if (normalized.startsWith("EXCLUDE")) { - kind = Kind.EXCLUDE; - } else if (normalized.equals("DEFAULT")) { - kind = Kind.DEFAULT; - } else if (normalized.contains("INDEX") || normalized.contains("KEY")) { - kind = Kind.INDEX; - } + kind = classifyType(string); + } + + private static Kind classifyType(String type) { + if (type == null) { + return Kind.OTHER; + } + String normalized = type.trim().toUpperCase(java.util.Locale.ROOT); + String keyword = normalized.split("\\s+", 2)[0]; + switch (keyword) { + case "PRIMARY": + return Kind.PRIMARY_KEY; + case "UNIQUE": + return Kind.UNIQUE; + case "FULLTEXT": + return Kind.FULLTEXT; + case "SPATIAL": + return Kind.SPATIAL; + case "FOREIGN": + return Kind.FOREIGN_KEY; + case "CHECK": + return Kind.CHECK; + case "EXCLUDE": + return Kind.EXCLUDE; + case "DEFAULT": + return Kind.DEFAULT; + default: + return normalized.equals("INDEX") || normalized.equals("KEY") + || normalized.endsWith(" INDEX") || normalized.endsWith(" KEY") + ? Kind.INDEX + : Kind.OTHER; } } @@ -247,6 +261,11 @@ public Kind getKind() { return kind; } + /** + * Sets classification metadata without changing the rendered type. This also supports index + * declarations whose keyword is stored separately. A subsequent {@link #setType(String)} + * derives the classification from the new type again. + */ public void setKind(Kind kind) { this.kind = kind; } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/IndexKindStateTest.java b/src/test/java/net/sf/jsqlparser/statement/create/IndexKindStateTest.java new file mode 100644 index 000000000..fbf740fb9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/IndexKindStateTest.java @@ -0,0 +1,101 @@ +/*- + * #%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 net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.DefaultConstraint; +import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.Index.Kind; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class IndexKindStateTest { + + @ParameterizedTest + @CsvSource({"PRIMARY KEY, PRIMARY_KEY", "unique key, UNIQUE", "UNIQUE INDEX, UNIQUE", + "KEY, INDEX", "INDEX, INDEX", "FULLTEXT KEY, FULLTEXT", "SPATIAL INDEX, SPATIAL", + "FOREIGN KEY, FOREIGN_KEY", "CHECK, CHECK", "EXCLUDE, EXCLUDE", "DEFAULT, DEFAULT", + "BITMAP INDEX, INDEX"}) + void replacingTypeRefreshesEveryPreviousClassification(String type, Kind expected) { + for (Kind previous : Kind.values()) { + Index index = new Index().withType("UNIQUE").withKind(previous); + index.setType(type); + assertEquals(type, index.getType()); + assertEquals(expected, index.getKind(), previous + " -> " + type); + } + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"CUSTOM", "UNIQUE_CUSTOM", "PRIMARY_CUSTOM", "MONKEY", "INDEXED"}) + void clearingOrReplacingWithCustomTypeClearsStaleKind(String type) { + Index index = new Index().withType("UNIQUE").withType(type); + assertEquals(type, index.getType()); + assertEquals(Kind.OTHER, index.getKind()); + } + + @Test + void classificationDoesNotRewriteSpellingOrDependOnLeadingWhitespace() { + Index index = new Index().withType(" unique\tkey "); + assertEquals(Kind.UNIQUE, index.getKind()); + assertEquals(" unique\tkey ", index.getType()); + } + + @Test + void explicitMetadataSupportsOmittedTypeAndSpecializedConstraints() { + Index index = new Index().withKind(Kind.INDEX); + assertNull(index.getType()); + assertEquals(Kind.INDEX, index.getKind()); + index.setType("UNIQUE"); + assertEquals(Kind.UNIQUE, index.getKind()); + assertEquals(Kind.CHECK, new CheckConstraint().getKind()); + assertEquals(Kind.EXCLUDE, new ExcludeConstraint().getKind()); + assertEquals(Kind.DEFAULT, new DefaultConstraint().getKind()); + } + + // Both the original and rewritten SQL execute on MySQL 8.4 and PostgreSQL 18. + @ParameterizedTest + @CsvSource({"UNIQUE, PRIMARY KEY, PRIMARY_KEY", "PRIMARY KEY, UNIQUE, UNIQUE"}) + void rewritingPostgreSqlConstraintPreservesKindThroughDeparsing(String before, String after, + Kind expected) throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE index_state (id INT, CONSTRAINT key_state " + before + " (id))"); + Index index = table.getIndexes().get(0); + index.setType(after); + assertEquals(expected, index.getKind()); + CreateTable reparsed = (CreateTable) assertSqlCanBeParsedAndDeparsed(table.toString()); + assertEquals(expected, reparsed.getIndexes().get(0).getKind()); + assertEquals("key_state", reparsed.getIndexes().get(0).getName()); + } + + // MySQL KEY and INDEX are aliases; UNIQUE changes the constraint's meaning. + @ParameterizedTest + @CsvSource({"UNIQUE KEY, KEY, INDEX", "KEY, UNIQUE KEY, UNIQUE", + "UNIQUE INDEX, INDEX, INDEX", "INDEX, UNIQUE INDEX, UNIQUE"}) + void rewritingMySqlIndexPreservesKindThroughDeparsing(String before, String after, + Kind expected) throws JSQLParserException { + CreateTable table = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE index_state (id INT, " + before + " key_state (id))"); + table.getIndexes().get(0).setType(after); + assertEquals(expected, table.getIndexes().get(0).getKind()); + CreateTable reparsed = (CreateTable) assertSqlCanBeParsedAndDeparsed(table.toString()); + assertEquals(expected, reparsed.getIndexes().get(0).getKind()); + } +}