From 70bc5af0f1c77a5f391ab83d5b6c5d00d1ec8c9b Mon Sep 17 00:00:00 2001 From: minleejae Date: Tue, 22 Sep 2026 16:35:33 +0900 Subject: [PATCH] feat: support PostgreSQL CHECK NO INHERIT Signed-off-by: minleejae --- .../create/table/CheckConstraint.java | 19 ++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 13 ++- src/site/sphinx/usage.rst | 13 +++ .../alter/PostgreSqlCheckNoInheritTest.java | 88 +++++++++++++++++++ 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlCheckNoInheritTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java index a3047fb42..e3f06db40 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java @@ -24,6 +24,8 @@ public class CheckConstraint extends NamedConstraint { private Boolean enforced; + private boolean noInherit; + public CheckConstraint() { setKind(Kind.CHECK); } @@ -44,6 +46,20 @@ public void setExpression(Expression expression) { this.expression = expression; } + /** Whether PostgreSQL should keep this CHECK from being inherited by child tables. */ + public boolean isNoInherit() { + return noInherit; + } + + public void setNoInherit(boolean noInherit) { + this.noInherit = noInherit; + } + + public CheckConstraint withNoInherit(boolean noInherit) { + setNoInherit(noInherit); + return this; + } + public Boolean getEnforced() { return enforced; } @@ -62,6 +78,9 @@ public void appendTo(StringBuilder b, Consumer expressionPrinter) { expressionPrinter.accept(expression); } b.append(')'); + if (noInherit) { + b.append(" NO INHERIT"); + } if (enforced != null) { b.append(enforced ? " ENFORCED" : " NOT ENFORCED"); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1411612de..8a8e024a8 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14132,6 +14132,7 @@ ColumnOption ColumnDefinitionOption(): { ColumnOption option; IdentityDefinition identity; NamedConstraint constraint; + String constraintName = null; Expression defaultExpression; GeneratedColumnDefinition generated; ObjectNames collationNames; @@ -14171,6 +14172,12 @@ ColumnOption ColumnDefinitionOption(): { LOOKAHEAD( ) { option = ColumnOption.constraint(new NamedConstraint().withType("PRIMARY KEY")); } | + LOOKAHEAD([ RelObjectName() ] , + { Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) }) + [ constraintName=RelObjectName() ] + constraint=CheckConstraintSpec(constraintName) + { option = ColumnOption.constraint(constraint); } + | LOOKAHEAD() constraint=ColumnUniqueConstraint() { option = ColumnOption.constraint(constraint); } | @@ -15759,17 +15766,21 @@ CheckConstraint CheckConstraintSpec(String constraintName): { Expression exp = null; Boolean enforced = null; + boolean noInherit = false; CheckConstraint checkConstraint; } { "(" exp = Expression() ")" + [ LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) }) + TypeDdlKeyword("INHERIT") { noInherit = true; } ] [ LOOKAHEAD(2) [ { enforced = false; } ] { if (enforced == null) { enforced = true; } } ] { checkConstraint = new CheckConstraint().withName(constraintName).withExpression(exp) - .withEnforced(enforced); + .withEnforced(enforced).withNoInherit(noInherit); checkConstraint.setKind(Index.Kind.CHECK); return checkConstraint; } diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 6e4c96feb..cb1d5d3b1 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -231,6 +231,19 @@ The fastest way to learn the object model is to look at it. Paste your SQL into Read that as a map: each line is a getter away. ``select.getSelectItems()``, ``select.getFromItem()``, ``select.getWhere()``. Once the tree gets deeper than a couple of levels, stop casting by hand and use :ref:`Use the Visitor Patterns`. +PostgreSQL CHECK inheritance +---------------------------- + +With ``Dialect.POSTGRESQL``, CREATE and ALTER CHECK constraints support +``NO INHERIT``. ``CheckConstraint.isNoInherit()`` reports this flag; use +``setNoInherit`` or ``withNoInherit`` to change it. The check expression remains +an editable ``Expression`` visited by the existing table traversal and deparsers. + +For ``ALTER TABLE t ADD CHECK (id > 0) NO INHERIT NOT VALID``, the two options +are independent: ``isNoInherit()`` is true and +``getConstraintAttributes().isNotValid()`` is true. Clearing one flag preserves +the other when the statement is rendered. + DROP INDEX owners ----------------- diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlCheckNoInheritTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlCheckNoInheritTest.java new file mode 100644 index 000000000..5abd264e6 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlCheckNoInheritTest.java @@ -0,0 +1,88 @@ +/*- + * #%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 org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlCheckNoInheritTest { + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t ADD CONSTRAINT ck CHECK (id > 0) NO INHERIT", + "ALTER TABLE t ADD CHECK (id > 0) NO INHERIT NOT VALID", + "ALTER TABLE t ADD CONSTRAINT ck CHECK (id > 0) NO INHERIT NOT VALID, ADD COLUMN extra INT", + "CREATE TABLE t (id INT, CONSTRAINT ck CHECK (id > 0) NO INHERIT)", + "CREATE TABLE t (id INT CHECK (id > 0) NO INHERIT)", + "CREATE TABLE t (id INT CONSTRAINT ck CHECK (id > 0) NO INHERIT)"}) + void roundTripsSharedCreateAndAlterCheck(String sql) throws JSQLParserException { + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + roundTrip(statement); + } + + @Test + void exposesIndependentInheritanceAndValidationFlags() throws JSQLParserException { + Alter alter = (Alter) parse( + "ALTER TABLE t ADD CONSTRAINT ck CHECK (id > 0) NO INHERIT NOT VALID"); + CheckConstraint constraint = + (CheckConstraint) alter.getAlterExpressions().get(0).getIndex(); + assertTrue(constraint.isNoInherit()); + assertTrue(constraint.getConstraintAttributes().isNotValid()); + constraint.setNoInherit(false); + constraint.setExpression(CCJSqlParserUtil.parseExpression("id > 10")); + assertEquals("ALTER TABLE t ADD CONSTRAINT ck CHECK (id > 10) NOT VALID", alter.toString()); + roundTrip(alter); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(alter.getAlterExpressions().get(0), visited::add, table -> { + }); + assertEquals(List.of(constraint.getExpression()), visited); + alter.getAlterExpressions().get(0).setIndex(new CheckConstraint().withName("new_ck") + .withExpression(CCJSqlParserUtil.parseExpression("id > 20")).withNoInherit(true)); + assertEquals("ALTER TABLE t ADD CONSTRAINT new_ck CHECK (id > 20) NO INHERIT", + alter.toString()); + roundTrip(alter); + } + + @Test + void preservesExistingCheckOptions() throws JSQLParserException { + CreateTable table = (CreateTable) parse("CREATE TABLE t (id INT, CHECK (id > 0))"); + assertFalse(((CheckConstraint) table.getIndexes().get(0)).isNoInherit()); + for (String suffix : new String[] {"", " ENFORCED", " NOT ENFORCED"}) { + String sql = "CREATE TABLE t (id INT, CHECK (id > 0)" + suffix + ")"; + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + assertEquals(sql, + CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)).toString()); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(sql.toString(), parse(sql.toString()).toString()); + } +}