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
61 changes: 40 additions & 21 deletions src/main/java/net/sf/jsqlparser/statement/create/table/Index.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,36 +217,55 @@ 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;
}
}

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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading