diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java index bf1bbbf40..4eb73e17c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java @@ -24,7 +24,7 @@ public class Index implements TableElement, Serializable { public enum Kind { - PRIMARY_KEY, UNIQUE, INDEX, FULLTEXT, SPATIAL, FOREIGN_KEY, CHECK, EXCLUDE, DEFAULT, OTHER + PRIMARY_KEY, UNIQUE, INDEX, FULLTEXT, SPATIAL, FOREIGN_KEY, CHECK, EXCLUDE, DEFAULT, NOT_NULL, OTHER } public enum Clustering { @@ -243,6 +243,8 @@ private static Kind classifyType(String type) { return Kind.SPATIAL; case "FOREIGN": return Kind.FOREIGN_KEY; + case "NOT": + return "NOT NULL".equals(normalized) ? Kind.NOT_NULL : Kind.OTHER; case "CHECK": return Kind.CHECK; case "EXCLUDE": diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/NotNullConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/NotNullConstraint.java new file mode 100644 index 000000000..567b611e3 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/NotNullConstraint.java @@ -0,0 +1,75 @@ +/*- + * #%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.create.table; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; + +/** PostgreSQL 18 table-level NOT NULL constraint, optionally named and non-inheritable. */ +public class NotNullConstraint extends NamedConstraint { + private Column column; + private boolean noInherit; + + public NotNullConstraint() { + setType("NOT NULL"); + } + + public Column getColumn() { + return column; + } + + public void setColumn(Column column) { + this.column = column; + } + + public boolean isNoInherit() { + return noInherit; + } + + public void setNoInherit(boolean noInherit) { + this.noInherit = noInherit; + } + + public NotNullConstraint withColumn(Column column) { + setColumn(column); + return this; + } + + public NotNullConstraint withNoInherit(boolean noInherit) { + setNoInherit(noInherit); + return this; + } + + @Override + public NotNullConstraint withName(String name) { + setName(name); + return this; + } + + public NotNullConstraint withConstraintAttributes(ConstraintAttributes attributes) { + setConstraintAttributes(attributes); + return this; + } + + @Override + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { + if (column == null) { + throw new IllegalStateException("NOT NULL requires a target column"); + } + appendConstraintPrefixTo(sql); + sql.append("NOT NULL "); + expressionPrinter.accept(column); + if (noInherit) { + sql.append(" NO INHERIT"); + } + appendConstraintAttributesTo(sql); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java index 153d98002..21a6b7361 100644 --- a/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java +++ b/src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java @@ -27,6 +27,7 @@ import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex; import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.NotNullConstraint; import net.sf.jsqlparser.statement.create.table.TableElement; import net.sf.jsqlparser.statement.create.table.TablePartitioning; import net.sf.jsqlparser.statement.create.table.PartitionBound; @@ -165,6 +166,9 @@ public static void visit(TableElement element, Consumer expressions, } } visitOptions(index.getStorageParameters(), expressions); + if (index instanceof NotNullConstraint) { + accept(((NotNullConstraint) index).getColumn(), expressions); + } if (index instanceof CheckConstraint) { accept(((CheckConstraint) index).getExpression(), expressions); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java index 7702630e0..05815c059 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java @@ -19,6 +19,7 @@ import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDropNotNull; import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull; import net.sf.jsqlparser.statement.alter.AlterOperation; +import net.sf.jsqlparser.statement.create.table.NotNullConstraint; import net.sf.jsqlparser.statement.create.table.DefaultConstraint; import net.sf.jsqlparser.util.TableDefinitionTraversal; import net.sf.jsqlparser.util.validation.ValidationCapability; @@ -86,7 +87,12 @@ public void validate(Alter alter, AlterExpression e) { validateOptionalName(c, NamedObject.constraint, e.getIndex().getName(), null, false, NamedObject.table); } else if (e.getIndex() != null) { - validateName(c, NamedObject.index, e.getIndex().getName()); + if (e.getIndex() instanceof NotNullConstraint) { + validateOptionalName(c, NamedObject.constraint, e.getIndex().getName(), null, + false, NamedObject.table); + } else { + validateName(c, NamedObject.index, e.getIndex().getName()); + } if (e.getIndex().getColumns() != null) { validateOptionalColumnNames(c, e.getIndex().getColumns().stream() diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java index caead25b6..bc528b0f7 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java @@ -12,6 +12,7 @@ import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.create.table.CreateTable; import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.NotNullConstraint; import net.sf.jsqlparser.util.TableDefinitionTraversal; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.metadata.NamedObject; @@ -37,7 +38,12 @@ public void validate(CreateTable createTable) { validateOptionalFeature(c, createTable.getSelect(), Feature.createTableFromSelect); if (isNotEmpty(createTable.getIndexes())) { for (Index i : createTable.getIndexes()) { - validateName(c, NamedObject.index, i.getName()); + if (i instanceof NotNullConstraint) { + validateOptionalName(c, NamedObject.constraint, i.getName(), null, false, + NamedObject.table); + } else { + validateName(c, NamedObject.index, i.getName()); + } } } validateName(c, NamedObject.table, createTable.getTable().getFullyQualifiedName(), diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 1411612de..b098e483f 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14859,7 +14859,8 @@ Index TableConstraint(boolean createContext): | [ LOOKAHEAD({ !isTableIndexAhead() && getToken(1).kind != K_FOREIGN - && getToken(1).kind != K_CHECK && getToken(1).kind != K_EXCLUDE }) constraintName=RelObjectName() ] + && getToken(1).kind != K_CHECK && getToken(1).kind != K_EXCLUDE + && !(getToken(1).kind == K_NOT && getToken(2).kind == K_NULL) }) constraintName=RelObjectName() ] ( LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(createContext) { if (index instanceof NamedConstraint) { @@ -14879,6 +14880,10 @@ Index TableConstraint(boolean createContext): } | index=PostgreSqlExcludeConstraint() { index.setName(constraintName); } + | + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_NOT }) + index=PostgreSqlNotNullConstraint() { index.setName(constraintName); } ) | fkIndex=ForeignKeySpec(null) { index = fkIndex; } @@ -14886,11 +14891,25 @@ Index TableConstraint(boolean createContext): checkConstraint=CheckConstraintSpec(null) { index = checkConstraint; } | index=PostgreSqlExcludeConstraint() + | + LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && getToken(1).kind == K_NOT }) + index=PostgreSqlNotNullConstraint() ) PostgreSqlConstraintAttributes(index) { return index; } } +/** PostgreSQL 18 adds a table-level NOT NULL with a named target column. */ +NotNullConstraint PostgreSqlNotNullConstraint(): +{ NotNullConstraint constraint = new NotNullConstraint(); String name; } +{ + name=RelObjectName() { constraint.setColumn(createColumn(name)); } + [ LOOKAHEAD({ getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) }) + TypeDdlKeyword("INHERIT") { constraint.setNoInherit(true); } ] + { return constraint; } +} + ExcludeConstraint PostgreSqlExcludeConstraint(): { ExcludeConstraint constraint = new ExcludeConstraint(); @@ -17253,7 +17272,9 @@ AlterExpression AlterExpressionAddAlterModify(): || 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_CHECK || getToken(1).kind == K_EXCLUDE + || getToken(1).kind == K_NOT + && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) }) index=TableConstraint(false) { setAlterTableIndex(alterExp, index); } | LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(false) { diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 6e4c96feb..fcda667e3 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -1204,6 +1204,30 @@ Parse procedure definitions one SQL Server batch at a time: a procedure consumes remaining batch, including SQL after an ``END``. Client-side ``GO`` batch splitting is not performed by this routine declaration parser. +PostgreSQL table-level NOT NULL constraints +------------------------------------------ + +``Dialect.POSTGRESQL`` supports PostgreSQL 18's table-level +``CONSTRAINT nn NOT NULL id``, in both CREATE TABLE and ALTER TABLE ADD. +``NotNullConstraint`` exposes the constraint name, target ``Column`` and +``noInherit`` flag; ``getConstraintAttributes().isNotValid()`` represents an +ALTER ``NOT VALID`` clause when present. + +.. code-block:: java + + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NOT VALID", + parser -> parser.withDialect(Dialect.POSTGRESQL)); + NotNullConstraint constraint = (NotNullConstraint) + alter.getAlterExpressions().get(0).getIndex(); + constraint.getColumn().setColumnName("other_id"); + constraint.setName("other_nn"); + +The target column participates in expression visitors and deparsers. New nodes +can be built with ``new NotNullConstraint().withName("nn") +.withColumn(new Column("id"))``. This is distinct from column definitions and +``ALTER COLUMN ... SET NOT NULL``; those retain their existing APIs. + SQL Server identity inserts --------------------------- diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlNotNullConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlNotNullConstraintTest.java new file mode 100644 index 000000000..61bcf9ccf --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlNotNullConstraintTest.java @@ -0,0 +1,150 @@ +/*- + * #%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 java.util.Set; +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.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.Index; +import net.sf.jsqlparser.statement.create.table.NotNullConstraint; +import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.AlterDeParser; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationContext; +import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation; +import net.sf.jsqlparser.util.validation.metadata.Named; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import net.sf.jsqlparser.util.validation.validator.AlterValidator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlNotNullConstraintTest { + @ParameterizedTest + @ValueSource(strings = {"NOT NULL id", "CONSTRAINT nn NOT NULL id", + "CONSTRAINT nn NOT NULL id NO INHERIT", "CONSTRAINT \"NN Name\" NOT NULL \"ID\""}) + void roundTripsCreateAndAlter(String body) throws JSQLParserException { + for (String sql : new String[] {"CREATE TABLE t (id INT, " + body + ")", + "ALTER TABLE t ADD " + body + " NOT VALID"}) { + Statement statement = parse(sql); + assertEquals(sql, statement.toString()); + NotNullConstraint constraint = constraint(statement); + assertEquals(Index.Kind.NOT_NULL, constraint.getKind()); + assertNotNull(constraint.getColumn()); + roundTrip(statement); + } + } + + @Test + void supportsEditingAndConstruction() throws JSQLParserException { + Alter alter = + (Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NO INHERIT NOT VALID"); + NotNullConstraint constraint = constraint(alter); + assertTrue(constraint.isNoInherit()); + assertTrue(constraint.getConstraintAttributes().isNotValid()); + constraint.setName("new_nn"); + constraint.getColumn().setColumnName("other_id"); + constraint.setNoInherit(false); + assertEquals("ALTER TABLE t ADD CONSTRAINT new_nn NOT NULL other_id NOT VALID", + alter.toString()); + roundTrip(alter); + ConstraintAttributes attributes = new ConstraintAttributes(); + attributes.setNotValid(true); + alter.getAlterExpressions().get(0).setIndex(new NotNullConstraint().withName("nn") + .withColumn(new Column("id")).withNoInherit(true) + .withConstraintAttributes(attributes)); + assertEquals("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NO INHERIT NOT VALID", + alter.toString()); + roundTrip(alter); + assertEquals(Index.Kind.NOT_NULL, new Index().withType("NOT NULL").getKind()); + } + + @Test + void visitsTargetColumnAndUsesCustomExpressionDeparser() throws JSQLParserException { + Alter alter = (Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id"); + List visited = new ArrayList<>(); + TableDefinitionTraversal.visit(alter.getAlterExpressions().get(0), visited::add, table -> { + }); + assertEquals(List.of(constraint(alter).getColumn()), visited); + CreateTable table = (CreateTable) parse("CREATE TABLE t (id INT, NOT NULL id)"); + visited.clear(); + TableDefinitionTraversal.visit(table, visited::add, source -> { + }); + assertEquals(List.of(constraint(table).getColumn()), visited); + assertEquals(Set.of("t"), new TablesNamesFinder().getTables(alter)); + StringBuilder sql = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("replacement"); + } + }; + expressions.setBuilder(sql); + new AlterDeParser(sql, expressions).deParse(alter); + assertEquals("ALTER TABLE t ADD CONSTRAINT nn NOT NULL replacement", sql.toString()); + } + + @Test + void validatesNewConstraintAndExistingColumn() throws JSQLParserException { + List visited = new ArrayList<>(); + DatabaseMetaDataValidation metadata = named -> { + visited.add(named); + return named.getNamedObject() != NamedObject.constraint; + }; + AlterValidator validator = new AlterValidator(); + validator.setContext(new ValidationContext().setCapabilities(List.of(metadata))); + validator.validate((Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NOT VALID")); + assertTrue(validator.getValidationErrors().isEmpty()); + assertTrue(visited.stream().anyMatch( + n -> n.getNamedObject() == NamedObject.column && "id".equals(n.getFqn()))); + assertTrue(visited.stream().anyMatch( + n -> n.getNamedObject() == NamedObject.constraint && "nn".equals(n.getFqn()))); + assertTrue(visited.stream().noneMatch(n -> n.getNamedObject() == NamedObject.index)); + } + + @Test + void preservesColumnNullabilityAndActionBoundaries() throws JSQLParserException { + roundTrip(parse("ALTER TABLE t ADD NOT NULL id NOT VALID, ADD COLUMN extra INT")); + for (String sql : new String[] {"CREATE TABLE t (id INT NOT NULL)", + "ALTER TABLE t ALTER COLUMN id SET NOT NULL", + "ALTER TABLE t ALTER COLUMN id DROP NOT NULL"}) { + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + roundTrip(parse(sql)); + } + } + + private static NotNullConstraint constraint(Statement statement) { + return (NotNullConstraint) (statement instanceof Alter + ? ((Alter) statement).getAlterExpressions().get(0).getIndex() + : ((CreateTable) statement).getIndexes().get(0)); + } + + 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()); + } +}