Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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":
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Expression> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -165,6 +166,9 @@ public static void visit(TableElement element, Consumer<Expression> expressions,
}
}
visitOptions(index.getStorageParameters(), expressions);
if (index instanceof NotNullConstraint) {
accept(((NotNullConstraint) index).getColumn(), expressions);
}
if (index instanceof CheckConstraint) {
accept(((CheckConstraint) index).getExpression(), expressions);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(),
Expand Down
25 changes: 23 additions & 2 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -14859,7 +14859,8 @@ Index TableConstraint(boolean createContext):
|
<K_CONSTRAINT>
[ 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) {
Expand All @@ -14879,18 +14880,36 @@ 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; }
|
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; }
{
<K_NOT> <K_NULL> name=RelObjectName() { constraint.setColumn(createColumn(name)); }
[ LOOKAHEAD({ getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) })
<K_NO> TypeDdlKeyword("INHERIT") { constraint.setNoInherit(true); } ]
{ return constraint; }
}

ExcludeConstraint PostgreSqlExcludeConstraint():
{
ExcludeConstraint constraint = new ExcludeConstraint();
Expand Down Expand Up @@ -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) {
Expand Down
24 changes: 24 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------------

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Expression> 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 <S> 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<Named> 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());
}
}
Loading