Skip to content

Commit f257070

Browse files
committed
fix: reuse table constraints for ALTER TABLE ADD in every dialect
ALTER TABLE ADD parsed table constraints with the shared CREATE TABLE production only for the MySQL and PostgreSQL dialects (#2612, #2654). Without one of them, the generic ALTER branches rejected forms that CREATE TABLE accepts in every dialect: an unnamed CHECK (c > 0), NOT VALID, DEFERRABLE INITIALLY DEFERRED, an unnamed FOREIGN KEY with attributes and EXCLUDE. Route an ADD to the shared production in every dialect when it uses a form that only that production accepts: an unnamed CHECK, FOREIGN KEY, EXCLUDE, or INITIALLY IMMEDIATE|DEFERRED, NOT VALID or ENFORCED outside a CHECK before the end of the action. Forms that already parsed keep their generic representation, accessors and output; the PostgreSQL-only UNIQUE USING INDEX and NOT NULL constraints still need that dialect. The scan lives in the package-private AlterConstraintLookahead, like RoutineBodyBoundary, because the generated CCJSqlParser class is close to the size JaCoCo can still instrument. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 90ac3a7 commit f257070

3 files changed

Lines changed: 211 additions & 2 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.parser;
11+
12+
import static net.sf.jsqlparser.parser.CCJSqlParserConstants.*;
13+
14+
/**
15+
* Decides whether an ALTER TABLE ADD table constraint needs the table-constraint production shared
16+
* with CREATE TABLE in dialects that otherwise keep the generic ALTER representation. Kept outside
17+
* the generated parser, whose class is close to the JVM size limits.
18+
*/
19+
final class AlterConstraintLookahead {
20+
private AlterConstraintLookahead() {}
21+
22+
/**
23+
* Whether the action starting at {@code first} is an unnamed CHECK, a FOREIGN KEY, EXCLUDE, or
24+
* uses a constraint attribute (INITIALLY IMMEDIATE|DEFERRED, NOT VALID, or ENFORCED outside
25+
* CHECK) before the end of the action.
26+
*/
27+
static boolean needsSharedTableConstraint(Token first, CCJSqlParserTokenManager source) {
28+
Token second = next(first, source);
29+
Token body = first.kind == K_CONSTRAINT ? next(second, source) : first;
30+
if (first.kind == K_CHECK && second.kind == OPENING_BRACKET || first.kind == K_FOREIGN
31+
|| isExcludeConstraint(body, source)) {
32+
return true;
33+
}
34+
if (first.kind != K_CONSTRAINT && first.kind != K_PRIMARY && first.kind != K_UNIQUE) {
35+
return false;
36+
}
37+
boolean check = body.kind == K_CHECK;
38+
int depth = 0;
39+
for (Token token = first; token.kind != EOF; token = next(token, source)) {
40+
if (token.kind == OPENING_BRACKET) {
41+
depth++;
42+
} else if (token.kind == CLOSING_BRACKET) {
43+
if (depth-- == 0) {
44+
return false;
45+
}
46+
} else if (depth == 0) {
47+
if (token.kind == ST_SEMICOLON || token.kind == K_COMMA) {
48+
return false;
49+
}
50+
if (isConstraintAttribute(token, next(token, source), check)) {
51+
return true;
52+
}
53+
}
54+
}
55+
return false;
56+
}
57+
58+
private static boolean isConstraintAttribute(Token token, Token next, boolean check) {
59+
if ("INITIALLY".equalsIgnoreCase(token.image)) {
60+
return "IMMEDIATE".equalsIgnoreCase(next.image)
61+
|| "DEFERRED".equalsIgnoreCase(next.image);
62+
}
63+
return token.kind == K_NOT && "VALID".equalsIgnoreCase(next.image)
64+
|| token.kind == K_ENFORCED && !check;
65+
}
66+
67+
/** EXCLUDE starts an exclusion constraint only before USING, its elements or WHERE. */
68+
private static boolean isExcludeConstraint(Token token, CCJSqlParserTokenManager source) {
69+
int following = next(token, source).kind;
70+
return token.kind == K_EXCLUDE && (following == K_USING
71+
|| following == OPENING_BRACKET || following == K_WHERE);
72+
}
73+
74+
private static Token next(Token token, CCJSqlParserTokenManager source) {
75+
if (token.next == null) {
76+
token.next = source.getNextToken();
77+
}
78+
return token.next;
79+
}
80+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21032,13 +21032,14 @@ AlterExpression AlterExpressionAddAlterModify():
2103221032
constraint=PostgreSqlConstraintUsingIndex() { alterExp.setConstraint(constraint); }
2103321033
|
2103421034
LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ADD
21035-
&& (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
21035+
&& ((Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
2103621036
|| Dialect.MYSQL.name().equals(getAsString(Feature.dialect)))
2103721037
&& (getToken(1).kind == K_CONSTRAINT || getToken(1).kind == K_PRIMARY
2103821038
|| getToken(1).kind == K_UNIQUE || getToken(1).kind == K_FOREIGN
2103921039
|| getToken(1).kind == K_CHECK || getToken(1).kind == K_EXCLUDE
2104021040
|| getToken(1).kind == K_NOT
21041-
&& Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) })
21041+
&& Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)))
21042+
|| AlterConstraintLookahead.needsSharedTableConstraint(getToken(1), token_source)) })
2104221043
element=TableConstraint(false) { setAlterTableElement(alterExp, element); }
2104321044
|
2104421045
LOOKAHEAD({ isTableIndexAhead() }) element=TableIndexSpec(false) {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
17+
import static org.junit.jupiter.api.Assertions.assertNull;
18+
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.function.Consumer;
24+
import net.sf.jsqlparser.JSQLParserException;
25+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
26+
import net.sf.jsqlparser.parser.CCJSqlParser;
27+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
28+
import net.sf.jsqlparser.statement.create.table.CheckConstraint;
29+
import net.sf.jsqlparser.statement.create.table.ConstraintAttributes;
30+
import net.sf.jsqlparser.statement.create.table.CreateTable;
31+
import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint;
32+
import net.sf.jsqlparser.statement.create.table.NamedConstraint;
33+
import net.sf.jsqlparser.util.TablesNamesFinder;
34+
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.params.ParameterizedTest;
36+
import org.junit.jupiter.params.provider.ValueSource;
37+
38+
/**
39+
* ALTER TABLE ADD accepts the table constraint forms of CREATE TABLE in every dialect, not only
40+
* with the MySQL and PostgreSQL dialects.
41+
*/
42+
class AlterAddTableConstraintTest {
43+
private static final List<Dialect> DIALECTS =
44+
Arrays.asList(null, Dialect.ANSI_SQL, Dialect.ORACLE, Dialect.SQLSERVER, Dialect.H2);
45+
46+
@ParameterizedTest
47+
@ValueSource(strings = {
48+
"CHECK (id > 0)",
49+
"CHECK (id > 0) NOT VALID",
50+
"CONSTRAINT ck CHECK (id > 0) NOT VALID",
51+
"FOREIGN KEY (id) REFERENCES parent (id) DEFERRABLE INITIALLY DEFERRED",
52+
"CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent (id) NOT VALID",
53+
"CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED",
54+
"CONSTRAINT uq UNIQUE (id) DEFERRABLE INITIALLY IMMEDIATE",
55+
"CONSTRAINT ex EXCLUDE USING gist (id WITH =)"})
56+
void createAndAlterExposeTheSameConstraintInEveryDialect(String definition)
57+
throws JSQLParserException {
58+
for (Dialect dialect : DIALECTS) {
59+
Consumer<CCJSqlParser> configuration = parser -> {
60+
if (dialect != null) {
61+
parser.withDialect(dialect);
62+
}
63+
};
64+
CreateTable create = (CreateTable) CCJSqlParserUtil
65+
.parse("CREATE TABLE t (id INT, " + definition + ")", configuration);
66+
Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed(
67+
"ALTER TABLE t ADD " + definition + ", ADD COLUMN other INT", true,
68+
configuration);
69+
NamedConstraint expected = create.getTableConstraints().get(0);
70+
NamedConstraint actual = alter.getAlterExpressions().get(0).getConstraint();
71+
assertEquals(expected.getClass(), actual.getClass(), String.valueOf(dialect));
72+
assertEquals(expected.toString(), actual.toString(), String.valueOf(dialect));
73+
assertEquals(2, alter.getAlterExpressions().size());
74+
}
75+
}
76+
77+
@Test
78+
void attributesAndReferencesAreStructuredWithoutADialect() throws JSQLParserException {
79+
String sql = "ALTER TABLE t ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent (id) "
80+
+ "DEFERRABLE INITIALLY DEFERRED";
81+
Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
82+
ForeignKeyConstraint foreignKey = assertInstanceOf(ForeignKeyConstraint.class,
83+
alter.getAlterExpressions().get(0).getConstraint());
84+
ConstraintAttributes attributes = foreignKey.getConstraintAttributes();
85+
assertEquals(Boolean.TRUE, attributes.getDeferrable());
86+
assertEquals(ConstraintAttributes.Initially.DEFERRED, attributes.getInitially());
87+
assertThat(TablesNamesFinder.findTables(sql)).containsExactlyInAnyOrder("t", "parent");
88+
89+
CheckConstraint check = assertInstanceOf(CheckConstraint.class,
90+
((Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CHECK (id > 0) NOT VALID"))
91+
.getAlterExpressions().get(0).getConstraint());
92+
assertNull(check.getName());
93+
assertTrue(check.getConstraintAttributes().isNotValid());
94+
}
95+
96+
@Test
97+
void formsThatAlreadyParsedKeepTheGenericRepresentation() throws JSQLParserException {
98+
AlterExpression deferrable = ((Alter) CCJSqlParserUtil
99+
.parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE"))
100+
.getAlterExpressions().get(0);
101+
assertThat(deferrable.getConstraints()).hasSize(1);
102+
assertInstanceOf(DeferrableConstraint.class, deferrable.getConstraints().get(0));
103+
104+
AlterExpression unique = ((Alter) CCJSqlParserUtil
105+
.parse("ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uk (id)"))
106+
.getAlterExpressions().get(0);
107+
assertEquals("UNIQUE KEY", unique.getConstraintType());
108+
}
109+
110+
@ParameterizedTest
111+
@ValueSource(strings = {"ALTER TABLE t ADD CONSTRAINT nn NOT NULL id",
112+
"ALTER TABLE t ADD CONSTRAINT uk UNIQUE USING INDEX t_idx"})
113+
void postgreSqlOnlyFormsStillRequireThePostgreSqlDialect(String sql) {
114+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
115+
assertDoesNotThrow(
116+
() -> CCJSqlParserUtil.parse(sql,
117+
parser -> parser.withDialect(Dialect.POSTGRESQL)));
118+
}
119+
120+
@Test
121+
void enforcedPrimaryKeyIsRejected() {
122+
JSQLParserException exception = assertThrows(JSQLParserException.class,
123+
() -> CCJSqlParserUtil
124+
.parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) ENFORCED"));
125+
assertThat(exception).hasStackTraceContaining(
126+
"ENFORCED is supported only for CHECK and foreign key constraints");
127+
}
128+
}

0 commit comments

Comments
 (0)