From f257070f4e2a32f84168a0282a96a9bb7983a077 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 23 Oct 2026 13:45:40 +0900 Subject: [PATCH 1/3] 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 --- .../parser/AlterConstraintLookahead.java | 80 +++++++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 5 +- .../alter/AlterAddTableConstraintTest.java | 128 ++++++++++++++++++ 3 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/parser/AlterConstraintLookahead.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/AlterConstraintLookahead.java b/src/main/java/net/sf/jsqlparser/parser/AlterConstraintLookahead.java new file mode 100644 index 0000000000..60a3b62486 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/parser/AlterConstraintLookahead.java @@ -0,0 +1,80 @@ +/*- + * #%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.parser; + +import static net.sf.jsqlparser.parser.CCJSqlParserConstants.*; + +/** + * 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. Kept outside + * the generated parser, whose class is close to the JVM size limits. + */ +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 (first.kind == K_CHECK && second.kind == OPENING_BRACKET || first.kind == K_FOREIGN + || isExcludeConstraint(body, source)) { + return true; + } + if (first.kind != K_CONSTRAINT && first.kind != K_PRIMARY && first.kind != K_UNIQUE) { + return false; + } + boolean check = body.kind == K_CHECK; + 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; + } +} diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 755d965fc0..1d08c1edbd 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -21032,13 +21032,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) { diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java new file mode 100644 index 0000000000..ce41db3d31 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterAddTableConstraintTest.java @@ -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 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 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"); + } +} From 95dc95b8058df8beccfb5e3146d1b10bf8faf073 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 23 Oct 2026 13:45:41 +0900 Subject: [PATCH 2/3] fix: parse VALIDATE CONSTRAINT as a structured action in every dialect ALTER TABLE t VALIDATE CONSTRAINT ck was a structured RelationAlterAction only with the PostgreSQL dialect. Elsewhere the remaining text became an UNSPECIFIC expression, so the constraint name could not be read and the action could not follow an ADD that now parses in every dialect. VALIDATE CONSTRAINT starts no other ALTER TABLE action, so recognise it in every dialect. Its output is unchanged. Signed-off-by: minleejae --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 2 ++ .../alter/PostgreSqlRelationAlterTest.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1d08c1edbd..8cae611d78 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3050,6 +3050,8 @@ public class CCJSqlParser extends AbstractJSqlParser { && "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") diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java index bc3f6ecb66..e7d10cddef 100644 --- a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlRelationAlterTest.java @@ -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)); } From 0d91757b67776cd27ed8a85c939aa5c83db9e6a6 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 23 Oct 2026 17:40:18 +0900 Subject: [PATCH 3/3] refactor: keep the ALTER constraint lookahead in the grammar Move AlterConstraintLookahead from its own file into the grammar as a private nested class, as done for TokenNesting in #2849. Split needsSharedTableConstraint into a check for the forms only the shared production accepts and a scan for constraint attributes. Codacy's PMD reported an NPath complexity of 440 for the combined method; the largest part is now 14. The nested class keeps its branches out of CCJSqlParser.class. Behaviour does not change. Signed-off-by: minleejae --- .../parser/AlterConstraintLookahead.java | 80 ------------------- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 79 ++++++++++++++++++ 2 files changed, 79 insertions(+), 80 deletions(-) delete mode 100644 src/main/java/net/sf/jsqlparser/parser/AlterConstraintLookahead.java diff --git a/src/main/java/net/sf/jsqlparser/parser/AlterConstraintLookahead.java b/src/main/java/net/sf/jsqlparser/parser/AlterConstraintLookahead.java deleted file mode 100644 index 60a3b62486..0000000000 --- a/src/main/java/net/sf/jsqlparser/parser/AlterConstraintLookahead.java +++ /dev/null @@ -1,80 +0,0 @@ -/*- - * #%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.parser; - -import static net.sf.jsqlparser.parser.CCJSqlParserConstants.*; - -/** - * 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. Kept outside - * the generated parser, whose class is close to the JVM size limits. - */ -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 (first.kind == K_CHECK && second.kind == OPENING_BRACKET || first.kind == K_FOREIGN - || isExcludeConstraint(body, source)) { - return true; - } - if (first.kind != K_CONSTRAINT && first.kind != K_PRIMARY && first.kind != K_UNIQUE) { - return false; - } - boolean check = body.kind == K_CHECK; - 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; - } -} diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 8cae611d78..eb02b6db01 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2530,6 +2530,85 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * 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