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
86 changes: 84 additions & 2 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -2530,6 +2530,85 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
}
}

/**
* Decides whether an ALTER TABLE ADD table constraint needs the table-constraint production
* shared with CREATE TABLE in dialects that otherwise keep the generic ALTER representation. A
* nested class keeps its branches out of CCJSqlParser.class, whose constant pool is close to
* what JaCoCo can still instrument.
*/
private static final class AlterConstraintLookahead {
private AlterConstraintLookahead() {}

/**
* Whether the action starting at {@code first} is an unnamed CHECK, a FOREIGN KEY,
* EXCLUDE, or uses a constraint attribute (INITIALLY IMMEDIATE|DEFERRED, NOT VALID, or
* ENFORCED outside CHECK) before the end of the action.
*/
static boolean needsSharedTableConstraint(Token first, CCJSqlParserTokenManager source) {
Token second = next(first, source);
Token body = first.kind == K_CONSTRAINT ? next(second, source) : first;
if (isSharedOnlyConstraint(first, second, body, source)) {
return true;
}
return (first.kind == K_CONSTRAINT || first.kind == K_PRIMARY
|| first.kind == K_UNIQUE)
&& hasConstraintAttribute(first, body.kind == K_CHECK, source);
}

/** An unnamed CHECK, a FOREIGN KEY or an EXCLUDE constraint. */
private static boolean isSharedOnlyConstraint(Token first, Token second, Token body,
CCJSqlParserTokenManager source) {
return first.kind == K_CHECK && second.kind == OPENING_BRACKET
|| first.kind == K_FOREIGN || isExcludeConstraint(body, source);
}

/** Whether a constraint attribute follows at the top level before the end of the action. */
private static boolean hasConstraintAttribute(Token first, boolean check,
CCJSqlParserTokenManager source) {
int depth = 0;
for (Token token = first; token.kind != EOF; token = next(token, source)) {
if (token.kind == OPENING_BRACKET) {
depth++;
} else if (token.kind == CLOSING_BRACKET) {
if (depth-- == 0) {
return false;
}
} else if (depth == 0) {
if (token.kind == ST_SEMICOLON || token.kind == K_COMMA) {
return false;
}
if (isConstraintAttribute(token, next(token, source), check)) {
return true;
}
}
}
return false;
}

private static boolean isConstraintAttribute(Token token, Token next, boolean check) {
if ("INITIALLY".equalsIgnoreCase(token.image)) {
return "IMMEDIATE".equalsIgnoreCase(next.image)
|| "DEFERRED".equalsIgnoreCase(next.image);
}
return token.kind == K_NOT && "VALID".equalsIgnoreCase(next.image)
|| token.kind == K_ENFORCED && !check;
}

/** EXCLUDE starts an exclusion constraint only before USING, its elements or WHERE. */
private static boolean isExcludeConstraint(Token token, CCJSqlParserTokenManager source) {
int following = next(token, source).kind;
return token.kind == K_EXCLUDE && (following == K_USING
|| following == OPENING_BRACKET || following == K_WHERE);
}

private static Token next(Token token, CCJSqlParserTokenManager source) {
if (token.next == null) {
token.next = source.getNextToken();
}
return token.next;
}
}

/**
* True when an IS after a condition opens a postfix test
* (IS [NOT] TRUE/FALSE/UNKNOWN/NULL/JSON) rather than IS DISTINCT FROM or
Expand Down Expand Up @@ -3050,6 +3129,8 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
&& "EXPRESSION".equalsIgnoreCase(getToken(columnActionOffset + 1).image)) {
return true;
}
// VALIDATE CONSTRAINT starts no other ALTER TABLE action in any dialect.
if (first.kind == K_VALIDATE && getToken(2).kind == K_CONSTRAINT) { return true; }
if (!Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) { return false; }
if (first.kind == K_VALIDATE || first.kind == K_RESET || isKeywordAhead("INHERIT")
|| isKeywordAhead("REPLICA") || isKeywordAhead("OWNER") || isKeywordAhead("CLUSTER")
Expand Down Expand Up @@ -21032,13 +21113,14 @@ AlterExpression AlterExpressionAddAlterModify():
constraint=PostgreSqlConstraintUsingIndex() { alterExp.setConstraint(constraint); }
|
LOOKAHEAD({ alterExp.getOperation() == AlterOperation.ADD
&& (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
&& ((Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
|| Dialect.MYSQL.name().equals(getAsString(Feature.dialect)))
&& (getToken(1).kind == K_CONSTRAINT || getToken(1).kind == K_PRIMARY
|| getToken(1).kind == K_UNIQUE || getToken(1).kind == K_FOREIGN
|| getToken(1).kind == K_CHECK || getToken(1).kind == K_EXCLUDE
|| getToken(1).kind == K_NOT
&& Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) })
&& Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)))
|| AlterConstraintLookahead.needsSharedTableConstraint(getToken(1), token_source)) })
element=TableConstraint(false) { setAlterTableElement(alterExp, element); }
|
LOOKAHEAD({ isTableIndexAhead() }) element=TableIndexSpec(false) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*-
* #%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 net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParser;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.create.table.CheckConstraint;
import net.sf.jsqlparser.statement.create.table.ConstraintAttributes;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.create.table.ForeignKeyConstraint;
import net.sf.jsqlparser.statement.create.table.NamedConstraint;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
* ALTER TABLE ADD accepts the table constraint forms of CREATE TABLE in every dialect, not only
* with the MySQL and PostgreSQL dialects.
*/
class AlterAddTableConstraintTest {
private static final List<Dialect> DIALECTS =
Arrays.asList(null, Dialect.ANSI_SQL, Dialect.ORACLE, Dialect.SQLSERVER, Dialect.H2);

@ParameterizedTest
@ValueSource(strings = {
"CHECK (id > 0)",
"CHECK (id > 0) NOT VALID",
"CONSTRAINT ck CHECK (id > 0) NOT VALID",
"FOREIGN KEY (id) REFERENCES parent (id) DEFERRABLE INITIALLY DEFERRED",
"CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent (id) NOT VALID",
"CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE INITIALLY DEFERRED",
"CONSTRAINT uq UNIQUE (id) DEFERRABLE INITIALLY IMMEDIATE",
"CONSTRAINT ex EXCLUDE USING gist (id WITH =)"})
void createAndAlterExposeTheSameConstraintInEveryDialect(String definition)
throws JSQLParserException {
for (Dialect dialect : DIALECTS) {
Consumer<CCJSqlParser> configuration = parser -> {
if (dialect != null) {
parser.withDialect(dialect);
}
};
CreateTable create = (CreateTable) CCJSqlParserUtil
.parse("CREATE TABLE t (id INT, " + definition + ")", configuration);
Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed(
"ALTER TABLE t ADD " + definition + ", ADD COLUMN other INT", true,
configuration);
NamedConstraint expected = create.getTableConstraints().get(0);
NamedConstraint actual = alter.getAlterExpressions().get(0).getConstraint();
assertEquals(expected.getClass(), actual.getClass(), String.valueOf(dialect));
assertEquals(expected.toString(), actual.toString(), String.valueOf(dialect));
assertEquals(2, alter.getAlterExpressions().size());
}
}

@Test
void attributesAndReferencesAreStructuredWithoutADialect() throws JSQLParserException {
String sql = "ALTER TABLE t ADD CONSTRAINT fk FOREIGN KEY (id) REFERENCES parent (id) "
+ "DEFERRABLE INITIALLY DEFERRED";
Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
ForeignKeyConstraint foreignKey = assertInstanceOf(ForeignKeyConstraint.class,
alter.getAlterExpressions().get(0).getConstraint());
ConstraintAttributes attributes = foreignKey.getConstraintAttributes();
assertEquals(Boolean.TRUE, attributes.getDeferrable());
assertEquals(ConstraintAttributes.Initially.DEFERRED, attributes.getInitially());
assertThat(TablesNamesFinder.findTables(sql)).containsExactlyInAnyOrder("t", "parent");

CheckConstraint check = assertInstanceOf(CheckConstraint.class,
((Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CHECK (id > 0) NOT VALID"))
.getAlterExpressions().get(0).getConstraint());
assertNull(check.getName());
assertTrue(check.getConstraintAttributes().isNotValid());
}

@Test
void formsThatAlreadyParsedKeepTheGenericRepresentation() throws JSQLParserException {
AlterExpression deferrable = ((Alter) CCJSqlParserUtil
.parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) DEFERRABLE"))
.getAlterExpressions().get(0);
assertThat(deferrable.getConstraints()).hasSize(1);
assertInstanceOf(DeferrableConstraint.class, deferrable.getConstraints().get(0));

AlterExpression unique = ((Alter) CCJSqlParserUtil
.parse("ALTER TABLE t ADD CONSTRAINT UNIQUE KEY uk (id)"))
.getAlterExpressions().get(0);
assertEquals("UNIQUE KEY", unique.getConstraintType());
}

@ParameterizedTest
@ValueSource(strings = {"ALTER TABLE t ADD CONSTRAINT nn NOT NULL id",
"ALTER TABLE t ADD CONSTRAINT uk UNIQUE USING INDEX t_idx"})
void postgreSqlOnlyFormsStillRequireThePostgreSqlDialect(String sql) {
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
assertDoesNotThrow(
() -> CCJSqlParserUtil.parse(sql,
parser -> parser.withDialect(Dialect.POSTGRESQL)));
}

@Test
void enforcedPrimaryKeyIsRejected() {
JSQLParserException exception = assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil
.parse("ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (id) ENFORCED"));
assertThat(exception).hasStackTraceContaining(
"ENFORCED is supported only for CHECK and foreign key constraints");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,23 @@ void bulkTablespaceMovesShareOneModel(String objectType) throws JSQLParserExcept
assertEquals(2, CCJSqlParserUtil.parseStatements(statement + "; SELECT 1").size());
}

@Test
void validateConstraintIsStructuredWithoutThePostgreSqlDialect() throws JSQLParserException {
for (Dialect dialect : new Dialect[] {null, Dialect.ANSI_SQL, Dialect.MYSQL}) {
Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed(
"ALTER TABLE t ADD CHECK (c > 0) NOT VALID, VALIDATE CONSTRAINT ck", true,
p -> {
if (dialect != null) {
p.withDialect(dialect);
}
});
RelationAlterAction validate = assertInstanceOf(RelationAlterAction.class,
alter.getAlterExpressions().get(1));
assertEquals(RelationAlterAction.Kind.VALIDATE_CONSTRAINT, validate.getKind());
assertEquals("ck", validate.getConstraintName());
}
}

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