Skip to content

Commit e4cf316

Browse files
authored
feat: reuse table constraint grammar for PostgreSQL ALTER ADD (#2612)
1 parent eb27c78 commit e4cf316

2 files changed

Lines changed: 161 additions & 36 deletions

File tree

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

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,45 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15471547
return false;
15481548
}
15491549

1550+
/** Keeps legacy ALTER key accessors populated from the common structured definition. */
1551+
private static void setAlterTableIndex(AlterExpression alterExp, Index index) {
1552+
alterExp.setIndex(index);
1553+
if (index.getKind() == Index.Kind.PRIMARY_KEY) {
1554+
alterExp.setPkColumns(index.getColumnsNames());
1555+
} else if (index.getKind() == Index.Kind.UNIQUE) {
1556+
alterExp.setUkColumns(index.getColumnsNames());
1557+
alterExp.setUkName(index instanceof NamedConstraint
1558+
? ((NamedConstraint) index).getIndexName() : index.getName());
1559+
alterExp.setUk(index.getType().toUpperCase(Locale.ROOT).contains("KEY"));
1560+
alterExp.setUkTypeSpecified(index.getIndexKeyword() != null);
1561+
for (String option : new ArrayList<String>(index.getIndexSpec())) {
1562+
if (option.toUpperCase(Locale.ROOT).startsWith("USING ")) {
1563+
alterExp.addParameters("USING");
1564+
alterExp.addParameters(option.substring("USING ".length()));
1565+
index.getIndexSpec().remove(option);
1566+
} else if (option.toUpperCase(Locale.ROOT).startsWith("COMMENT ")) {
1567+
index.setCommentText(option.substring("COMMENT ".length()));
1568+
index.getIndexSpec().remove(option);
1569+
}
1570+
}
1571+
}
1572+
if (index instanceof ForeignKeyIndex) {
1573+
ForeignKeyIndex foreignKey = (ForeignKeyIndex) index;
1574+
alterExp.setFkColumns(foreignKey.getColumnsNames());
1575+
if (foreignKey.getTable() != null) {
1576+
alterExp.setFkSourceSchema(foreignKey.getTable().getSchemaName());
1577+
alterExp.setFkSourceTable(foreignKey.getTable().getName());
1578+
}
1579+
alterExp.setFkSourceColumns(foreignKey.getReferencedColumnNames());
1580+
for (ReferentialAction.Type type : ReferentialAction.Type.values()) {
1581+
ReferentialAction action = foreignKey.getReferentialAction(type);
1582+
if (action != null) {
1583+
alterExp.setReferentialAction(action.getType(), action.getAction());
1584+
}
1585+
}
1586+
}
1587+
}
1588+
15501589
private static boolean hasStructuredColumnOption(List<ColumnOption> options) {
15511590
for (ColumnOption option : options) {
15521591
if (option.getKind() != ColumnOption.Kind.OTHER) {
@@ -13155,6 +13194,11 @@ void PostgreSqlConstraintAttributes(Index index):
1315513194
* to {@link #TableIndexSpec(boolean)} so CREATE and ALTER expose the same structured AST.
1315613195
*/
1315713196
Index CreateTableConstraint():
13197+
{ Index index; }
13198+
{ index=TableConstraint(true) { return index; } }
13199+
13200+
/** Shared table-constraint body; index option boundaries depend on CREATE versus ALTER. */
13201+
Index TableConstraint(boolean createContext):
1315813202
{
1315913203
String constraintName = null;
1316013204
Index index = null;
@@ -13163,13 +13207,13 @@ Index CreateTableConstraint():
1316313207
}
1316413208
{
1316513209
(
13166-
LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(true)
13210+
LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(createContext)
1316713211
|
1316813212
<K_CONSTRAINT>
1316913213
[ LOOKAHEAD({ !isTableIndexAhead() && getToken(1).kind != K_FOREIGN
1317013214
&& getToken(1).kind != K_CHECK && getToken(1).kind != K_EXCLUDE }) constraintName=RelObjectName() ]
1317113215
(
13172-
LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(true) {
13216+
LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(createContext) {
1317313217
if (index instanceof NamedConstraint) {
1317413218
((NamedConstraint) index).setUseConstraintKeyword(true);
1317513219
index.setName(constraintName);
@@ -13210,7 +13254,7 @@ ExcludeConstraint PostgreSqlExcludeConstraint():
1321013254
{
1321113255
<K_EXCLUDE>
1321213256
[ LOOKAHEAD(2) <K_USING> using=RelObjectName() { constraint.setUsing(using); } ]
13213-
[ "(" column=PostgreSqlExcludeElement() { columns.add(column); }
13257+
[ LOOKAHEAD(2) "(" column=PostgreSqlExcludeElement() { columns.add(column); }
1321413258
( "," column=PostgreSqlExcludeElement() { columns.add(column); } )* ")"
1321513259
{ constraint.setColumns(columns); } ]
1321613260
PostgreSqlConstraintOptions(constraint)
@@ -15415,27 +15459,15 @@ AlterExpression AlterExpressionAddAlterModify():
1541515459
alterExp.setIndex(index);
1541615460
}
1541715461
|
15462+
LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ADD
15463+
&& Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
15464+
&& (getToken(1).kind == K_CONSTRAINT || getToken(1).kind == K_PRIMARY
15465+
|| getToken(1).kind == K_UNIQUE || getToken(1).kind == K_FOREIGN
15466+
|| getToken(1).kind == K_CHECK || getToken(1).kind == K_EXCLUDE) })
15467+
index=TableConstraint(false) { setAlterTableIndex(alterExp, index); }
15468+
|
1541815469
LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(false) {
15419-
alterExp.setIndex(index);
15420-
if (index.getKind() == Index.Kind.PRIMARY_KEY) {
15421-
alterExp.setPkColumns(index.getColumnsNames());
15422-
} else if (index.getKind() == Index.Kind.UNIQUE) {
15423-
alterExp.setUkColumns(index.getColumnsNames());
15424-
alterExp.setUkName(index instanceof NamedConstraint
15425-
? ((NamedConstraint) index).getIndexName() : index.getName());
15426-
alterExp.setUk(index.getType().toUpperCase(Locale.ROOT).contains("KEY"));
15427-
alterExp.setUkTypeSpecified(index.getIndexKeyword() != null);
15428-
for (String option : new ArrayList<String>(index.getIndexSpec())) {
15429-
if (option.toUpperCase(Locale.ROOT).startsWith("USING ")) {
15430-
alterExp.addParameters("USING");
15431-
alterExp.addParameters(option.substring("USING ".length()));
15432-
index.getIndexSpec().remove(option);
15433-
} else if (option.toUpperCase(Locale.ROOT).startsWith("COMMENT ")) {
15434-
index.setCommentText(option.substring("COMMENT ".length()));
15435-
index.getIndexSpec().remove(option);
15436-
}
15437-
}
15438-
}
15470+
setAlterTableIndex(alterExp, index);
1543915471
}
1544015472
constraints=AlterExpressionConstraintState() { alterExp.setConstraints(constraints); }
1544115473
[ AlterExpressionUsingIndex(alterExp) ]
@@ -15485,21 +15517,10 @@ AlterExpression AlterExpressionAddAlterModify():
1548515517
|
1548615518
// Standalone FK now uses ForeignKeyIndex, same as CONSTRAINT FK
1548715519
(
15488-
{ ForeignKeyIndex fkIndex; ReferentialAction ra; }
15520+
{ ForeignKeyIndex fkIndex; }
1548915521
fkIndex = ForeignKeySpec(null)
1549015522
{
15491-
alterExp.setIndex(fkIndex);
15492-
// backward compat: populate deprecated FK fields from ForeignKeyIndex
15493-
alterExp.setFkColumns(fkIndex.getColumnsNames());
15494-
if (fkIndex.getTable() != null) {
15495-
alterExp.setFkSourceSchema(fkIndex.getTable().getSchemaName());
15496-
alterExp.setFkSourceTable(fkIndex.getTable().getName());
15497-
}
15498-
alterExp.setFkSourceColumns(fkIndex.getReferencedColumnNames());
15499-
ra = fkIndex.getReferentialAction(ReferentialAction.Type.DELETE);
15500-
if (ra != null) { alterExp.setReferentialAction(ra.getType(), ra.getAction()); }
15501-
ra = fkIndex.getReferentialAction(ReferentialAction.Type.UPDATE);
15502-
if (ra != null) { alterExp.setReferentialAction(ra.getType(), ra.getAction()); }
15523+
setAlterTableIndex(alterExp, fkIndex);
1550315524
}
1550415525
)
1550515526
|
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.assertj.core.api.Assertions.assertThat;
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
15+
import static org.junit.jupiter.api.Assertions.assertThrows;
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
17+
18+
import net.sf.jsqlparser.JSQLParserException;
19+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
20+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
21+
import net.sf.jsqlparser.statement.create.table.CheckConstraint;
22+
import net.sf.jsqlparser.statement.create.table.ConstraintAttributes;
23+
import net.sf.jsqlparser.statement.create.table.ExcludeConstraint;
24+
import net.sf.jsqlparser.statement.create.table.Index;
25+
import net.sf.jsqlparser.util.TablesNamesFinder;
26+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.ValueSource;
30+
31+
class PostgreSqlAlterConstraintTest {
32+
@ParameterizedTest
33+
@ValueSource(strings = {
34+
"CONSTRAINT c CHECK (id > 0) NOT VALID",
35+
"CHECK (id > 0) NOT VALID",
36+
"CONSTRAINT c UNIQUE NULLS NOT DISTINCT (id) INCLUDE (other) WITH (fillfactor = 70) "
37+
+ "USING INDEX TABLESPACE fast DEFERRABLE INITIALLY DEFERRED",
38+
"UNIQUE (id) NOT DEFERRABLE INITIALLY IMMEDIATE",
39+
"CONSTRAINT c PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED",
40+
"CONSTRAINT c FOREIGN KEY (id) REFERENCES parent(id) ON DELETE CASCADE "
41+
+ "DEFERRABLE INITIALLY DEFERRED NOT VALID",
42+
"FOREIGN KEY (id) REFERENCES parent(id) NOT VALID",
43+
"CONSTRAINT c EXCLUDE USING gist ((id + 1) WITH =) WHERE (id > 0) DEFERRABLE",
44+
"EXCLUDE USING gist (id WITH =)"
45+
})
46+
void sharedConstraintGrammarParsesAndRoundTrips(String definition)
47+
throws JSQLParserException {
48+
Alter alter = parse("ALTER TABLE t ADD " + definition);
49+
StringBuilder buffer = new StringBuilder();
50+
alter.accept(new StatementDeParser(buffer), null);
51+
assertEquals(alter.toString(), buffer.toString());
52+
assertEquals(alter.toString(), parse(buffer.toString()).toString());
53+
assertEquals(1, alter.getAlterExpressions().size());
54+
}
55+
56+
@Test
57+
void constraintAttributesAndPredicatesRemainStructured() throws JSQLParserException {
58+
Alter alter = parse("ALTER TABLE t ADD CONSTRAINT c CHECK (id > 0) NOT VALID, "
59+
+ "ADD CONSTRAINT u UNIQUE (id) DEFERRABLE INITIALLY DEFERRED");
60+
CheckConstraint check = assertInstanceOf(CheckConstraint.class,
61+
alter.getAlterExpressions().get(0).getIndex());
62+
assertTrue(check.getConstraintAttributes().isNotValid());
63+
Index unique = alter.getAlterExpressions().get(1).getIndex();
64+
assertEquals(ConstraintAttributes.Initially.DEFERRED,
65+
unique.getConstraintAttributes().getInitially());
66+
assertEquals(Boolean.TRUE, unique.getConstraintAttributes().getDeferrable());
67+
ExcludeConstraint exclude = assertInstanceOf(ExcludeConstraint.class,
68+
parse("ALTER TABLE t ADD EXCLUDE USING gist ((id + 1) WITH =) WHERE (id > 0)")
69+
.getAlterExpressions().get(0).getIndex());
70+
assertEquals("id > 0", exclude.getExpression().toString());
71+
}
72+
73+
@Test
74+
void commonProjectionPreservesLegacyKeyAccessors() throws JSQLParserException {
75+
AlterExpression primary = parse("ALTER TABLE t ADD PRIMARY KEY (id)")
76+
.getAlterExpressions().get(0);
77+
assertThat(primary.getPkColumns()).containsExactly("id");
78+
AlterExpression foreign =
79+
parse("ALTER TABLE t ADD FOREIGN KEY (id) REFERENCES app.parent(id)")
80+
.getAlterExpressions().get(0);
81+
assertThat(foreign.getFkColumns()).containsExactly("id");
82+
assertEquals("app", foreign.getFkSourceSchema());
83+
assertEquals("parent", foreign.getFkSourceTable());
84+
assertThat(new TablesNamesFinder().getTables(
85+
parse("ALTER TABLE t ADD FOREIGN KEY (id) REFERENCES app.parent(id) NOT VALID")))
86+
.containsExactlyInAnyOrder("t", "app.parent");
87+
}
88+
89+
@ParameterizedTest
90+
@ValueSource(strings = {
91+
"ALTER TABLE t ADD CONSTRAINT c CHECK (id > 0) NOT INVALID",
92+
"ALTER TABLE t ADD UNIQUE (id) INITIALLY UNKNOWN",
93+
"ALTER TABLE t ADD EXCLUDE USING gist (id)",
94+
"ALTER TABLE t ADD CONSTRAINT c CHECK (id > 0), ADD"
95+
})
96+
void invalidConstraintTailsAreRejected(String sql) {
97+
assertThrows(JSQLParserException.class, () -> parse(sql));
98+
}
99+
100+
private static Alter parse(String sql) throws JSQLParserException {
101+
return (Alter) CCJSqlParserUtil.parse(sql,
102+
parser -> parser.withDialect(Dialect.POSTGRESQL));
103+
}
104+
}

0 commit comments

Comments
 (0)