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 @@ -27,6 +27,10 @@ public enum Kind {
PRIMARY_KEY, UNIQUE, INDEX, FULLTEXT, SPATIAL, FOREIGN_KEY, CHECK, EXCLUDE, DEFAULT, OTHER
}

public enum Clustering {
CLUSTERED, NONCLUSTERED
}

private final List<String> name = new ArrayList<>();
private String type;
private String using;
Expand All @@ -35,12 +39,31 @@ public enum Kind {
private String commentText;
private String indexKeyword;
private Kind kind = Kind.OTHER;
private Clustering clustering;
private Boolean nullsDistinct;
private List<String> includeColumns;
private List<Option> storageParameters;
private String tableSpace;
private ConstraintAttributes constraintAttributes;

/** Returns the explicit SQL Server clustering option, or null when it was omitted. */
public Clustering getClustering() {
return clustering;
}

public void setClustering(Clustering clustering) {
this.clustering = clustering;
}

public Index withClustering(Clustering clustering) {
setClustering(clustering);
return this;
}

public String clusteringClause() {
return clustering == null ? "" : " " + clustering;
}

public Boolean getNullsDistinct() {
return nullsDistinct;
}
Expand Down Expand Up @@ -277,7 +300,8 @@ public String toString() {
: "")
+ (!idxSpecText.isEmpty() ? " " + idxSpecText : "");

StringBuilder sql = new StringBuilder(head).append(nullsDistinctClause());
StringBuilder sql = new StringBuilder(head).append(nullsDistinctClause())
.append(clusteringClause());
if (!tail.isEmpty()) {
sql.append(' ').append(tail);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public String toString() {
String tail = getType()
+ nullsDistinctClause()
+ keyword
+ clusteringClause()
+ (indexName != null ? " " + indexName : "")
+ (getUsing() != null ? " USING " + getUsing() : "")
+ (getColumns() == null ? ""
Expand All @@ -113,6 +114,12 @@ public NamedConstraint withIndexName(String indexName) {
return this;
}

@Override
public NamedConstraint withClustering(Clustering clustering) {
setClustering(clustering);
return this;
}

public NamedConstraint withUseConstraintKeyword(boolean useConstraintKeyword) {
setUseConstraintKeyword(useConstraintKeyword);
return this;
Expand Down
30 changes: 27 additions & 3 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -12471,6 +12471,22 @@ CreateDatabase CreateDatabase():
}
}

/** Shares optional SQL Server constraint modifiers across CREATE and ALTER. */
Index.Clustering SqlServerIndexClustering():
{
Token token;
Index.Clustering clustering = null;
}
{
[ LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect))
&& (isKeywordAhead("CLUSTERED") || isKeywordAhead("NONCLUSTERED")) })
token=<S_IDENTIFIER> {
clustering = Index.Clustering.valueOf(token.image.toUpperCase(Locale.ROOT));
}
]
{ return clustering; }
}

/** Parses PRIMARY, UNIQUE, plain, FULLTEXT, and SPATIAL indexes for CREATE and ALTER. */
Index TableIndexSpec(boolean createContext):
{
Expand All @@ -12482,11 +12498,13 @@ Index TableIndexSpec(boolean createContext):
List<String> indexOptions = new ArrayList<String>();
Index index;
Boolean nullsDistinct = null;
Index.Clustering clustering = null;
}
{
(
typeToken=<K_PRIMARY> keywordToken=<K_KEY>
[ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET }) indexName=RelObjectName() ]
clustering=SqlServerIndexClustering()
[ LOOKAHEAD({ clustering == null && getToken(1).kind != OPENING_BRACKET }) indexName=RelObjectName() ]
columns=IndexColumnsWithParamsList()
TableIndexOptions(createContext, indexOptions)
{
Expand All @@ -12502,7 +12520,8 @@ Index TableIndexSpec(boolean createContext):
if (nullsDistinct == null) { nullsDistinct = true; }
} ]
[ LOOKAHEAD(2) (keywordToken=<K_KEY> | keywordToken=<K_INDEX>) ]
[ LOOKAHEAD({ getToken(1).kind != OPENING_BRACKET
clustering=SqlServerIndexClustering()
[ LOOKAHEAD({ clustering == null && getToken(1).kind != OPENING_BRACKET
&& getToken(1).kind != K_USING }) indexName=RelObjectName() ]
[ using=UsingIndexType() ]
columns=IndexColumnsWithParamsList()
Expand Down Expand Up @@ -12562,7 +12581,7 @@ Index TableIndexSpec(boolean createContext):
.withName(indexName).withColumns(columns).withIndexSpec(indexOptions);
}
)
{ index.setNullsDistinct(nullsDistinct); }
{ index.setNullsDistinct(nullsDistinct); index.setClustering(clustering); }
PostgreSqlConstraintOptions(index)
{ return index; }
}
Expand Down Expand Up @@ -14600,6 +14619,7 @@ void AlterExpressionAddConstraint(AlterExpression alterExp):
Table fkTable;
List<ConstraintState> constraints = null;
CheckConstraint checkCs = null;
Index.Clustering clustering = null;
}
{
<K_CONSTRAINT>
Expand Down Expand Up @@ -14627,11 +14647,13 @@ void AlterExpressionAddConstraint(AlterExpression alterExp):
)
|
( tk=<K_PRIMARY> tk2=<K_KEY>
clustering=SqlServerIndexClustering()
columnNames=ColumnsNamesList()
{
index = new NamedConstraint()
.withName(sk3)
.withType(tk.image + " " + tk2.image)
.withClustering(clustering)
.withColumnsNames(columnNames);
alterExp.setIndex(index);
}
Expand All @@ -14655,11 +14677,13 @@ void AlterExpressionAddConstraint(AlterExpression alterExp):
|
(
tk=<K_UNIQUE> (tk2=<K_KEY> { alterExp.setUk(true); } | tk2=<K_INDEX>)?
clustering=SqlServerIndexClustering()
columnNames=ColumnsNamesList()
{
index = new NamedConstraint()
.withName(sk3)
.withType(tk.image + (tk2!=null?" " + tk2.image:""))
.withClustering(clustering)
.withColumnsNames(columnNames);
alterExp.setIndex(index);
}
Expand Down
7 changes: 6 additions & 1 deletion src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
* - ``MYSQL``
- ``withBackslashEscapeCharacter``, ``withHashLineComments``, ``withDoubleQuotedStrings`` (MySQL and MariaDB, the last for the default ``sql_mode``)
* - ``SQLSERVER``
- ``withSquareBracketQuotation``
- ``withSquareBracketQuotation`` and ``CLUSTERED`` / ``NONCLUSTERED`` options on table-level primary key and unique constraints
* - ``POSTGRESQL``, ``ANSI_SQL``
- the newline rule for adjacent string literals
* - ``BIGQUERY``
Expand All @@ -716,6 +716,11 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ

Features set explicitly *after* the preset win over it.

With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words
retain their existing interpretation as optional index names.

Informix's constraint form requires an explicit dialect selection:

.. code-block:: java
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*-
* #%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.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.create.table.Index;
import net.sf.jsqlparser.statement.create.table.NamedConstraint;
import net.sf.jsqlparser.test.TestUtils;
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.EnumSource;

class SqlServerConstraintClusteringTest {
private static Index constraint(Statement statement) {
return statement instanceof CreateTable
? ((CreateTable) statement).getIndexes().get(0)
: ((Alter) statement).getAlterExpressions().get(0).getIndex();
}

private static Statement parse(String sql) throws JSQLParserException {
return CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(Dialect.SQLSERVER));
}

@Test
void parsesSakilaReproducerIssue1589() throws Exception {
String sql = "CREATE TABLE actor (actor_id INT NOT NULL IDENTITY, "
+ "first_name VARCHAR (45) NOT NULL, last_name VARCHAR (45) NOT NULL, "
+ "last_update DATETIME NOT NULL, PRIMARY KEY NONCLUSTERED (actor_id))";
Index index = constraint(TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true,
parser -> parser.withDialect(Dialect.SQLSERVER)));
assertEquals(Index.Clustering.NONCLUSTERED, index.getClustering());
assertNull(((NamedConstraint) index).getIndexName());
assertEquals(List.of("actor_id"), index.getColumnsNames());
}

@ParameterizedTest
@EnumSource(Index.Clustering.class)
void sharesClusteringAcrossCreateAndAlter(Index.Clustering clustering) throws Exception {
for (String type : List.of("PRIMARY KEY", "UNIQUE")) {
for (String name : List.of("", "CONSTRAINT [key name] ")) {
String definition = name + type + " " + clustering + " (id)";
for (String sql : List.of("CREATE TABLE t (id INT, " + definition + ")",
"ALTER TABLE t ADD " + definition)) {
Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true,
parser -> parser.withDialect(Dialect.SQLSERVER));
Index index = constraint(statement);
assertEquals(clustering, index.getClustering(), sql);
assertEquals(type, index.getType());
assertEquals(name.isEmpty() ? null : "[key name]", index.getName());
if (index instanceof NamedConstraint) {
assertNull(((NamedConstraint) index).getIndexName());
}
StringBuilder output = new StringBuilder();
statement.accept(new StatementDeParser(output), null);
assertEquals(clustering, constraint(parse(output.toString())).getClustering());
assertEquals(clustering,
constraint(parse(statement.toString())).getClustering());
}
}
}
}

@Test
void preservesNamesOutsideSqlServerAndQuotedNames() throws Exception {
for (String type : List.of("PRIMARY KEY", "UNIQUE")) {
String sql = "CREATE TABLE t (id INT, " + type + " NONCLUSTERED (id))";
NamedConstraint defaultIndex =
(NamedConstraint) constraint(CCJSqlParserUtil.parse(sql));
assertNull(defaultIndex.getClustering());
assertEquals("NONCLUSTERED", defaultIndex.getIndexName());
for (Dialect dialect : Dialect.values()) {
if (dialect != Dialect.SQLSERVER) {
NamedConstraint index = (NamedConstraint) constraint(CCJSqlParserUtil.parse(sql,
parser -> parser.withDialect(dialect)));
assertNull(index.getClustering(), dialect.name());
assertEquals("NONCLUSTERED", index.getIndexName());
}
}
NamedConstraint quoted = (NamedConstraint) constraint(
parse("CREATE TABLE t (id INT, " + type + " [NONCLUSTERED] (id))"));
assertNull(quoted.getClustering());
assertEquals("[NONCLUSTERED]", quoted.getIndexName());
}
}

@Test
void supportsMutationAndLeavesOmittedOptionUnspecified() throws Exception {
Statement statement = parse("CREATE TABLE t (id INT, PRIMARY KEY (id))");
Index index = constraint(statement);
assertNull(index.getClustering());
index.setClustering(Index.Clustering.CLUSTERED);
TestUtils.assertDeparse(statement, "CREATE TABLE t (id INT, PRIMARY KEY CLUSTERED (id))");
index.setClustering(null);
TestUtils.assertDeparse(statement, "CREATE TABLE t (id INT, PRIMARY KEY (id))");
NamedConstraint built = new NamedConstraint().withType("UNIQUE").withName("uq_t")
.withClustering(Index.Clustering.NONCLUSTERED).withColumnsNames(List.of("id"));
assertEquals("CONSTRAINT uq_t UNIQUE NONCLUSTERED (id)", built.toString());
}

@Test
void keepsFollowingConstraintsAndRejectsDuplicateModifiers() throws Exception {
CreateTable table = (CreateTable) parse("CREATE TABLE t (id INT, other_id INT, "
+ "PRIMARY KEY NONCLUSTERED (id), UNIQUE (other_id))");
assertNull(table.getIndexes().get(1).getClustering());
for (String sql : List.of(
"CREATE TABLE t (id INT, PRIMARY KEY NONCLUSTERED CLUSTERED (id))",
"ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY CLUSTERED NONCLUSTERED (id)",
"ALTER TABLE t ADD UNIQUE NONCLUSTERED")) {
assertThrows(JSQLParserException.class, () -> parse(sql));
}
}
}
Loading