Skip to content
Open
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
@@ -0,0 +1,63 @@
/*-
* #%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.table;

import java.util.function.Consumer;
import net.sf.jsqlparser.expression.Expression;

/**
* A PostgreSQL UNIQUE or PRIMARY KEY constraint backed by an existing index. The referenced index
* is distinct from the constraint name and from a newly declared index.
*/
public class ConstraintUsingIndex extends NamedConstraint {
private String existingIndexName;

public ConstraintUsingIndex() {
setType("UNIQUE");
}

/** Returns the referenced index identifier, retaining SQL quotes. */
public String getExistingIndexName() {
return existingIndexName;
}

public void setExistingIndexName(String existingIndexName) {
this.existingIndexName = existingIndexName;
}

public ConstraintUsingIndex withExistingIndexName(String existingIndexName) {
setExistingIndexName(existingIndexName);
return this;
}

@Override
public ConstraintUsingIndex withName(String name) {
super.withName(name);
return this;
}

@Override
public ConstraintUsingIndex withType(String type) {
super.withType(type);
return this;
}

public ConstraintUsingIndex withConstraintAttributes(ConstraintAttributes attributes) {
setConstraintAttributes(attributes);
return this;
}

@Override
public void appendTo(StringBuilder sql, Consumer<Expression> expressionPrinter) {
appendConstraintPrefixTo(sql);
sql.append(getType()).append(" USING INDEX ").append(existingIndexName);
appendConstraintAttributesTo(sql);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull;
import net.sf.jsqlparser.statement.alter.AlterOperation;
import net.sf.jsqlparser.statement.create.table.DefaultConstraint;
import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex;
import net.sf.jsqlparser.util.TableDefinitionTraversal;
import net.sf.jsqlparser.util.validation.ValidationCapability;
import net.sf.jsqlparser.util.validation.ValidationUtil;
Expand Down Expand Up @@ -82,7 +83,12 @@ public void validate(Alter alter, AlterExpression e) {
validateOptionalColumnNames(c, e.getUkColumns(), NamedObject.uniqueConstraint);
}

if (e.getIndex() instanceof DefaultConstraint) {
if (e.getIndex() instanceof ConstraintUsingIndex) {
ConstraintUsingIndex constraint = (ConstraintUsingIndex) e.getIndex();
validateOptionalName(c, NamedObject.constraint, constraint.getName(), null, false,
NamedObject.table);
validateName(c, NamedObject.index, constraint.getExistingIndexName());
} else if (e.getIndex() instanceof DefaultConstraint) {
validateOptionalName(c, NamedObject.constraint, e.getIndex().getName(), null, false,
NamedObject.table);
} else if (e.getIndex() != null) {
Expand Down
18 changes: 18 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -14845,6 +14845,18 @@ Index CreateTableConstraint():
{ Index index; }
{ index=TableConstraint(true) { return index; } }

/** Attaches an existing PostgreSQL index without declaring new index columns. */
ConstraintUsingIndex PostgreSqlConstraintUsingIndex():
{ ConstraintUsingIndex constraint = new ConstraintUsingIndex(); String name; }
{
[ <K_CONSTRAINT> name=RelObjectName() { constraint.setName(name); } ]
( <K_UNIQUE> { constraint.setType("UNIQUE"); }
| <K_PRIMARY> <K_KEY> { constraint.setType("PRIMARY KEY"); } )
<K_USING> <K_INDEX> name=RelObjectName() { constraint.setExistingIndexName(name); }
PostgreSqlConstraintAttributes(constraint)
{ return constraint; }
}

/** Shared table-constraint body; index option boundaries depend on CREATE versus ALTER. */
Index TableConstraint(boolean createContext):
{
Expand Down Expand Up @@ -17248,6 +17260,12 @@ AlterExpression AlterExpressionAddAlterModify():
alterExp.setIndex(index);
}
|
LOOKAHEAD([ <K_CONSTRAINT> RelObjectName() ]
( <K_UNIQUE> | <K_PRIMARY> <K_KEY> ) <K_USING> <K_INDEX>,
{ alterExp.getOperation() == AlterOperation.ADD
&& Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
index=PostgreSqlConstraintUsingIndex() { alterExp.setIndex(index); }
|
LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ADD
&& (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
|| Dialect.MYSQL.name().equals(getAsString(Feature.dialect)))
Expand Down
23 changes: 23 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1300,3 +1300,26 @@ This API identifies expression metadata rather than performing database
validation. Precision reports the requested value, without applying defaults,
server range checks or clamping. For example, PostgreSQL accepts precision 7 with
a warning and clamps it to 6, whereas MySQL rejects it.

Attach a PostgreSQL constraint to an existing index
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

With ``Dialect.POSTGRESQL``, ``ALTER TABLE ... ADD UNIQUE USING INDEX`` and
``ADD PRIMARY KEY USING INDEX`` expose a ``ConstraintUsingIndex`` through
``AlterExpression.getIndex()``. Its ``getName()`` is the optional new constraint
name, while ``getExistingIndexName()`` identifies the existing index. This is
separate from an index declaration's name, columns and access method.

.. code-block:: java

Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i",
parser -> parser.withDialect(Dialect.POSTGRESQL));
ConstraintUsingIndex constraint = (ConstraintUsingIndex)
alter.getAlterExpressions().get(0).getIndex();
constraint.setExistingIndexName("replacement_index");
constraint.setName("replacement_constraint");

``getConstraintAttributes()`` exposes deferrability and initial timing when
present. The same AST can be constructed with ``new ConstraintUsingIndex()``
and its fluent ``withName``, ``withType`` and ``withExistingIndexName`` methods.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*-
* #%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.alter;

import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex;
import net.sf.jsqlparser.statement.create.table.Index;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import net.sf.jsqlparser.util.validation.ValidationContext;
import net.sf.jsqlparser.util.validation.metadata.Named;
import net.sf.jsqlparser.util.validation.metadata.NamedObject;
import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation;
import net.sf.jsqlparser.util.validation.validator.AlterValidator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class PostgreSqlConstraintUsingIndexTest {
@ParameterizedTest
@ValueSource(strings = {"UNIQUE USING INDEX i", "PRIMARY KEY USING INDEX i",
"CONSTRAINT uq UNIQUE USING INDEX i DEFERRABLE INITIALLY DEFERRED",
"CONSTRAINT pk PRIMARY KEY USING INDEX i NOT DEFERRABLE INITIALLY IMMEDIATE",
"CONSTRAINT \"Unique Name\" UNIQUE USING INDEX \"Index Name\""})
void roundTripsExistingIndexConstraints(String body) throws JSQLParserException {
String sql = "ALTER TABLE t ADD " + body;
Alter alter = parse(sql);
ConstraintUsingIndex constraint = assertInstanceOf(ConstraintUsingIndex.class,
alter.getAlterExpressions().get(0).getIndex());
assertNull(constraint.getColumns());
assertNull(constraint.getIndexName());
assertNull(constraint.getUsing());
assertEquals(sql, alter.toString());
roundTrip(alter);
}

@Test
void supportsMutationAndConstruction() throws JSQLParserException {
Alter alter = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i DEFERRABLE");
ConstraintUsingIndex constraint =
(ConstraintUsingIndex) alter.getAlterExpressions().get(0).getIndex();
assertEquals(Boolean.TRUE, constraint.getConstraintAttributes().getDeferrable());
constraint.setName("pk");
constraint.setType("PRIMARY KEY");
constraint.setExistingIndexName("other_index");
assertEquals(Index.Kind.PRIMARY_KEY, constraint.getKind());
assertEquals(
"ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY USING INDEX other_index DEFERRABLE",
alter.toString());
roundTrip(alter);
Alter created = new Alter().withTable(new Table("t"));
created.addAlterExpressions(new AlterExpression().withOperation(AlterOperation.ADD)
.withIndex(new ConstraintUsingIndex().withName("uq").withExistingIndexName("i")));
assertEquals("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i", created.toString());
roundTrip(created);
}

@Test
void distinguishesTableAndIndexAndNewConstraint() throws JSQLParserException {
Alter alter = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i");
assertEquals(Set.of("t"), new TablesNamesFinder().getTables(alter));
List<Named> visited = new ArrayList<>();
DatabaseMetaDataValidation metadata = named -> {
visited.add(named);
return named.getNamedObject() != NamedObject.constraint;
};
AlterValidator validator = new AlterValidator();
validator.setContext(new ValidationContext().setCapabilities(List.of(metadata)));
validator.validate(alter);
assertTrue(validator.getValidationErrors().isEmpty());
assertTrue(visited.stream()
.anyMatch(n -> n.getNamedObject() == NamedObject.index && "i".equals(n.getFqn())));
assertTrue(visited.stream().anyMatch(
n -> n.getNamedObject() == NamedObject.constraint && "uq".equals(n.getFqn())));
}

@Test
void preservesOrdinaryConstraintsAndActionBoundaries() throws JSQLParserException {
roundTrip(parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE (id)"));
Alter alter = parse("ALTER TABLE t ADD UNIQUE USING INDEX i, ADD COLUMN extra INT");
assertEquals(2, alter.getAlterExpressions().size());
roundTrip(alter);
String oracle = "ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) USING INDEX i";
assertEquals(oracle, CCJSqlParserUtil.parse(oracle).toString());
}

private static Alter parse(String sql) throws JSQLParserException {
return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
}

private static void roundTrip(Alter alter) throws JSQLParserException {
StringBuilder sql = new StringBuilder();
alter.accept(new StatementDeParser(sql), null);
assertEquals(alter.toString(), sql.toString());
assertEquals(sql.toString(), parse(sql.toString()).toString());
}
}
Loading