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 index 567b611e3..6c5c03118 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/NotNullConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/NotNullConstraint.java @@ -13,15 +13,30 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.schema.Column; -/** PostgreSQL 18 table-level NOT NULL constraint, optionally named and non-inheritable. */ +/** PostgreSQL 18 column- or table-level NOT NULL constraint. */ public class NotNullConstraint extends NamedConstraint { private Column column; private boolean noInherit; + private boolean columnConstraint; public NotNullConstraint() { setType("NOT NULL"); } + /** Whether the target is implicit in the enclosing column definition. */ + public boolean isColumnConstraint() { + return columnConstraint; + } + + public void setColumnConstraint(boolean columnConstraint) { + this.columnConstraint = columnConstraint; + } + + public NotNullConstraint withColumnConstraint(boolean columnConstraint) { + setColumnConstraint(columnConstraint); + return this; + } + public Column getColumn() { return column; } @@ -61,12 +76,15 @@ public NotNullConstraint withConstraintAttributes(ConstraintAttributes attribute @Override public void appendTo(StringBuilder sql, Consumer expressionPrinter) { - if (column == null) { + if (!columnConstraint && column == null) { throw new IllegalStateException("NOT NULL requires a target column"); } appendConstraintPrefixTo(sql); - sql.append("NOT NULL "); - expressionPrinter.accept(column); + sql.append("NOT NULL"); + if (!columnConstraint) { + sql.append(' '); + expressionPrinter.accept(column); + } if (noInherit) { sql.append(" NO INHERIT"); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0df99f424..7a2e3e807 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14119,6 +14119,11 @@ ColumnOption ColumnDefinitionOption(): { || (getToken(1).kind == K_AS && "(".equals(getToken(2).image)) }) generated=GeneratedColumnDefinition() { option = ColumnOption.generated(generated); } | + LOOKAHEAD([ RelObjectName() ] , + { Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + && (getToken(1).kind == K_CONSTRAINT || getToken(3).kind == K_NO) }) + constraint=PostgreSqlColumnNotNullConstraint() { option = ColumnOption.constraint(constraint); } + | LOOKAHEAD( ) { option = ColumnOption.nullability(false); } | LOOKAHEAD() { option = ColumnOption.nullability(true); } @@ -14892,14 +14897,29 @@ Index TableConstraint(boolean createContext): /** PostgreSQL 18 adds a table-level NOT NULL with a named target column. */ NotNullConstraint PostgreSqlNotNullConstraint(): -{ NotNullConstraint constraint = new NotNullConstraint(); String name; } +{ NotNullConstraint constraint = new NotNullConstraint(); String name; boolean noInherit; } { name=RelObjectName() { constraint.setColumn(createColumn(name)); } - [ LOOKAHEAD({ getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) }) - TypeDdlKeyword("INHERIT") { constraint.setNoInherit(true); } ] + noInherit=PostgreSqlNoInheritClause() { constraint.setNoInherit(noInherit); } { return constraint; } } +NotNullConstraint PostgreSqlColumnNotNullConstraint(): +{ String name = null; boolean noInherit; } +{ + [ name=RelObjectName() ] + noInherit=PostgreSqlNoInheritClause() + { return new NotNullConstraint().withColumnConstraint(true).withName(name).withNoInherit(noInherit); } +} + +boolean PostgreSqlNoInheritClause(): +{ boolean noInherit = false; } +{ + [ LOOKAHEAD({ getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) }) + TypeDdlKeyword("INHERIT") { noInherit = true; } ] + { return noInherit; } +} + ExcludeConstraint PostgreSqlExcludeConstraint(): { ExcludeConstraint constraint = new ExcludeConstraint(); diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnNotNullTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnNotNullTest.java new file mode 100644 index 000000000..0fe67e52d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlColumnNotNullTest.java @@ -0,0 +1,99 @@ +/*- + * #%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; + +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.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; +import net.sf.jsqlparser.statement.create.table.ColumnOption; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.NotNullConstraint; +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 PostgreSqlColumnNotNullTest { + @ParameterizedTest + @ValueSource(strings = {"NOT NULL NO INHERIT", "CONSTRAINT nn NOT NULL", + "CONSTRAINT nn NOT NULL NO INHERIT", "CONSTRAINT \"Named NN\" NOT NULL NO INHERIT"}) + void reusesNotNullConstraintsInCreateAndAlterColumns(String clause) throws JSQLParserException { + for (String prefix : List.of("CREATE TABLE t (", "ALTER TABLE t ADD COLUMN ")) { + Statement statement = parse(prefix + "id INT " + clause + " DEFAULT 7" + + (prefix.startsWith("CREATE") ? ", value INT)" : ", ADD COLUMN value INT")); + ColumnDefinition column = column(statement); + NotNullConstraint constraint = assertInstanceOf(NotNullConstraint.class, + column.getColumnOptions().get(0).getConstraint()); + assertTrue(constraint.isColumnConstraint()); + assertNull(constraint.getColumn()); + assertEquals(clause.endsWith("NO INHERIT"), constraint.isNoInherit()); + assertEquals(ColumnOption.Kind.DEFAULT, column.getColumnOptions().get(1).getKind()); + roundTrip(statement); + constraint.setName("replacement"); + constraint.setNoInherit(!constraint.isNoInherit()); + assertTrue(statement.toString().contains("CONSTRAINT replacement NOT NULL")); + roundTrip(statement); + } + } + + @Test + void retainsPlainNullabilityAndUsesTheExistingTableModel() throws JSQLParserException { + ColumnOption option = + column(parse("CREATE TABLE t (id INT NOT NULL)")).getColumnOptions().get(0); + assertEquals(ColumnOption.Kind.NULLABILITY, option.getKind()); + assertEquals(false, option.getNullable()); + NotNullConstraint table = (NotNullConstraint) ((CreateTable) parse( + "CREATE TABLE t (id INT, CONSTRAINT nn NOT NULL id NO INHERIT)")).getIndexes() + .get(0); + assertFalse(table.isColumnConstraint()); + assertEquals("id", table.getColumn().getColumnName()); + assertEquals("CONSTRAINT nn NOT NULL NO INHERIT", new NotNullConstraint() + .withColumnConstraint(true).withName("nn").withNoInherit(true).toString()); + } + + @Test + void doesNotInventASecondColumnExpressionForAnImplicitTarget() throws JSQLParserException { + CreateTable table = + (CreateTable) parse("CREATE TABLE t (id INT DEFAULT 7 NOT NULL NO INHERIT)"); + List expressions = new ArrayList<>(); + TableDefinitionTraversal.visit(table, expressions::add, t -> { + }); + assertEquals(1, expressions.size()); + assertEquals("7", expressions.get(0).toString()); + roundTrip(table); + assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + private static ColumnDefinition column(Statement statement) { + return statement instanceof CreateTable + ? ((CreateTable) statement).getColumnDefinitions().get(0) + : ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().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 out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString()).toString()); + } +}