Skip to content

Commit f99acbe

Browse files
committed
feat: attach PostgreSQL constraints to existing indexes
Signed-off-by: mj-db <mj.db@kakaocorp.com>
1 parent 60c8dc2 commit f99acbe

5 files changed

Lines changed: 222 additions & 1 deletion

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.table;
11+
12+
import java.util.function.Consumer;
13+
import net.sf.jsqlparser.expression.Expression;
14+
15+
/**
16+
* A PostgreSQL UNIQUE or PRIMARY KEY constraint backed by an existing index. The referenced index
17+
* is distinct from the constraint name and from a newly declared index.
18+
*/
19+
public class ConstraintUsingIndex extends NamedConstraint {
20+
private String existingIndexName;
21+
22+
public ConstraintUsingIndex() {
23+
setType("UNIQUE");
24+
}
25+
26+
/** Returns the referenced index identifier, retaining SQL quotes. */
27+
public String getExistingIndexName() {
28+
return existingIndexName;
29+
}
30+
31+
public void setExistingIndexName(String existingIndexName) {
32+
this.existingIndexName = existingIndexName;
33+
}
34+
35+
public ConstraintUsingIndex withExistingIndexName(String existingIndexName) {
36+
setExistingIndexName(existingIndexName);
37+
return this;
38+
}
39+
40+
@Override
41+
public ConstraintUsingIndex withName(String name) {
42+
super.withName(name);
43+
return this;
44+
}
45+
46+
@Override
47+
public ConstraintUsingIndex withType(String type) {
48+
super.withType(type);
49+
return this;
50+
}
51+
52+
public ConstraintUsingIndex withConstraintAttributes(ConstraintAttributes attributes) {
53+
setConstraintAttributes(attributes);
54+
return this;
55+
}
56+
57+
@Override
58+
public void appendTo(StringBuilder sql, Consumer<Expression> expressionPrinter) {
59+
appendConstraintPrefixTo(sql);
60+
sql.append(getType()).append(" USING INDEX ").append(existingIndexName);
61+
appendConstraintAttributesTo(sql);
62+
}
63+
}

src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull;
2121
import net.sf.jsqlparser.statement.alter.AlterOperation;
2222
import net.sf.jsqlparser.statement.create.table.DefaultConstraint;
23+
import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex;
2324
import net.sf.jsqlparser.util.TableDefinitionTraversal;
2425
import net.sf.jsqlparser.util.validation.ValidationCapability;
2526
import net.sf.jsqlparser.util.validation.ValidationUtil;
@@ -82,7 +83,12 @@ public void validate(Alter alter, AlterExpression e) {
8283
validateOptionalColumnNames(c, e.getUkColumns(), NamedObject.uniqueConstraint);
8384
}
8485

85-
if (e.getIndex() instanceof DefaultConstraint) {
86+
if (e.getIndex() instanceof ConstraintUsingIndex) {
87+
ConstraintUsingIndex constraint = (ConstraintUsingIndex) e.getIndex();
88+
validateOptionalName(c, NamedObject.constraint, constraint.getName(), null, false,
89+
NamedObject.table);
90+
validateName(c, NamedObject.index, constraint.getExistingIndexName());
91+
} else if (e.getIndex() instanceof DefaultConstraint) {
8692
validateOptionalName(c, NamedObject.constraint, e.getIndex().getName(), null, false,
8793
NamedObject.table);
8894
} else if (e.getIndex() != null) {

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14845,6 +14845,18 @@ Index CreateTableConstraint():
1484514845
{ Index index; }
1484614846
{ index=TableConstraint(true) { return index; } }
1484714847

14848+
/** Attaches an existing PostgreSQL index without declaring new index columns. */
14849+
ConstraintUsingIndex PostgreSqlConstraintUsingIndex():
14850+
{ ConstraintUsingIndex constraint = new ConstraintUsingIndex(); String name; }
14851+
{
14852+
[ <K_CONSTRAINT> name=RelObjectName() { constraint.setName(name); } ]
14853+
( <K_UNIQUE> { constraint.setType("UNIQUE"); }
14854+
| <K_PRIMARY> <K_KEY> { constraint.setType("PRIMARY KEY"); } )
14855+
<K_USING> <K_INDEX> name=RelObjectName() { constraint.setExistingIndexName(name); }
14856+
PostgreSqlConstraintAttributes(constraint)
14857+
{ return constraint; }
14858+
}
14859+
1484814860
/** Shared table-constraint body; index option boundaries depend on CREATE versus ALTER. */
1484914861
Index TableConstraint(boolean createContext):
1485014862
{
@@ -17248,6 +17260,12 @@ AlterExpression AlterExpressionAddAlterModify():
1724817260
alterExp.setIndex(index);
1724917261
}
1725017262
|
17263+
LOOKAHEAD([ <K_CONSTRAINT> RelObjectName() ]
17264+
( <K_UNIQUE> | <K_PRIMARY> <K_KEY> ) <K_USING> <K_INDEX>,
17265+
{ alterExp.getOperation() == AlterOperation.ADD
17266+
&& Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
17267+
index=PostgreSqlConstraintUsingIndex() { alterExp.setIndex(index); }
17268+
|
1725117269
LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ADD
1725217270
&& (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
1725317271
|| Dialect.MYSQL.name().equals(getAsString(Feature.dialect)))

src/site/sphinx/usage.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,3 +1300,26 @@ This API identifies expression metadata rather than performing database
13001300
validation. Precision reports the requested value, without applying defaults,
13011301
server range checks or clamping. For example, PostgreSQL accepts precision 7 with
13021302
a warning and clamps it to 6, whereas MySQL rejects it.
1303+
1304+
Attach a PostgreSQL constraint to an existing index
1305+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1306+
1307+
With ``Dialect.POSTGRESQL``, ``ALTER TABLE ... ADD UNIQUE USING INDEX`` and
1308+
``ADD PRIMARY KEY USING INDEX`` expose a ``ConstraintUsingIndex`` through
1309+
``AlterExpression.getIndex()``. Its ``getName()`` is the optional new constraint
1310+
name, while ``getExistingIndexName()`` identifies the existing index. This is
1311+
separate from an index declaration's name, columns and access method.
1312+
1313+
.. code-block:: java
1314+
1315+
Alter alter = (Alter) CCJSqlParserUtil.parse(
1316+
"ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i",
1317+
parser -> parser.withDialect(Dialect.POSTGRESQL));
1318+
ConstraintUsingIndex constraint = (ConstraintUsingIndex)
1319+
alter.getAlterExpressions().get(0).getIndex();
1320+
constraint.setExistingIndexName("replacement_index");
1321+
constraint.setName("replacement_constraint");
1322+
1323+
``getConstraintAttributes()`` exposes deferrability and initial timing when
1324+
present. The same AST can be constructed with ``new ConstraintUsingIndex()``
1325+
and its fluent ``withName``, ``withType`` and ``withExistingIndexName`` methods.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.alter;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.Set;
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
18+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
19+
import net.sf.jsqlparser.schema.Table;
20+
import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex;
21+
import net.sf.jsqlparser.statement.create.table.Index;
22+
import net.sf.jsqlparser.util.TablesNamesFinder;
23+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
24+
import net.sf.jsqlparser.util.validation.ValidationContext;
25+
import net.sf.jsqlparser.util.validation.metadata.Named;
26+
import net.sf.jsqlparser.util.validation.metadata.NamedObject;
27+
import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation;
28+
import net.sf.jsqlparser.util.validation.validator.AlterValidator;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.params.ParameterizedTest;
31+
import org.junit.jupiter.params.provider.ValueSource;
32+
33+
class PostgreSqlConstraintUsingIndexTest {
34+
@ParameterizedTest
35+
@ValueSource(strings = {"UNIQUE USING INDEX i", "PRIMARY KEY USING INDEX i",
36+
"CONSTRAINT uq UNIQUE USING INDEX i DEFERRABLE INITIALLY DEFERRED",
37+
"CONSTRAINT pk PRIMARY KEY USING INDEX i NOT DEFERRABLE INITIALLY IMMEDIATE",
38+
"CONSTRAINT \"Unique Name\" UNIQUE USING INDEX \"Index Name\""})
39+
void roundTripsExistingIndexConstraints(String body) throws JSQLParserException {
40+
String sql = "ALTER TABLE t ADD " + body;
41+
Alter alter = parse(sql);
42+
ConstraintUsingIndex constraint = assertInstanceOf(ConstraintUsingIndex.class,
43+
alter.getAlterExpressions().get(0).getIndex());
44+
assertNull(constraint.getColumns());
45+
assertNull(constraint.getIndexName());
46+
assertNull(constraint.getUsing());
47+
assertEquals(sql, alter.toString());
48+
roundTrip(alter);
49+
}
50+
51+
@Test
52+
void supportsMutationAndConstruction() throws JSQLParserException {
53+
Alter alter = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i DEFERRABLE");
54+
ConstraintUsingIndex constraint =
55+
(ConstraintUsingIndex) alter.getAlterExpressions().get(0).getIndex();
56+
assertEquals(Boolean.TRUE, constraint.getConstraintAttributes().getDeferrable());
57+
constraint.setName("pk");
58+
constraint.setType("PRIMARY KEY");
59+
constraint.setExistingIndexName("other_index");
60+
assertEquals(Index.Kind.PRIMARY_KEY, constraint.getKind());
61+
assertEquals(
62+
"ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY USING INDEX other_index DEFERRABLE",
63+
alter.toString());
64+
roundTrip(alter);
65+
Alter created = new Alter().withTable(new Table("t"));
66+
created.addAlterExpressions(new AlterExpression().withOperation(AlterOperation.ADD)
67+
.withIndex(new ConstraintUsingIndex().withName("uq").withExistingIndexName("i")));
68+
assertEquals("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i", created.toString());
69+
roundTrip(created);
70+
}
71+
72+
@Test
73+
void distinguishesTableAndIndexAndNewConstraint() throws JSQLParserException {
74+
Alter alter = parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i");
75+
assertEquals(Set.of("t"), new TablesNamesFinder().getTables(alter));
76+
List<Named> visited = new ArrayList<>();
77+
DatabaseMetaDataValidation metadata = named -> {
78+
visited.add(named);
79+
return named.getNamedObject() != NamedObject.constraint;
80+
};
81+
AlterValidator validator = new AlterValidator();
82+
validator.setContext(new ValidationContext().setCapabilities(List.of(metadata)));
83+
validator.validate(alter);
84+
assertTrue(validator.getValidationErrors().isEmpty());
85+
assertTrue(visited.stream()
86+
.anyMatch(n -> n.getNamedObject() == NamedObject.index && "i".equals(n.getFqn())));
87+
assertTrue(visited.stream().anyMatch(
88+
n -> n.getNamedObject() == NamedObject.constraint && "uq".equals(n.getFqn())));
89+
}
90+
91+
@Test
92+
void preservesOrdinaryConstraintsAndActionBoundaries() throws JSQLParserException {
93+
roundTrip(parse("ALTER TABLE t ADD CONSTRAINT uq UNIQUE (id)"));
94+
Alter alter = parse("ALTER TABLE t ADD UNIQUE USING INDEX i, ADD COLUMN extra INT");
95+
assertEquals(2, alter.getAlterExpressions().size());
96+
roundTrip(alter);
97+
String oracle = "ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) USING INDEX i";
98+
assertEquals(oracle, CCJSqlParserUtil.parse(oracle).toString());
99+
}
100+
101+
private static Alter parse(String sql) throws JSQLParserException {
102+
return (Alter) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
103+
}
104+
105+
private static void roundTrip(Alter alter) throws JSQLParserException {
106+
StringBuilder sql = new StringBuilder();
107+
alter.accept(new StatementDeParser(sql), null);
108+
assertEquals(alter.toString(), sql.toString());
109+
assertEquals(sql.toString(), parse(sql.toString()).toString());
110+
}
111+
}

0 commit comments

Comments
 (0)