Skip to content

Commit b62bb62

Browse files
authored
feat: model PostgreSQL table-level NOT NULL constraints (#2680)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 0e6f8ed commit b62bb62

8 files changed

Lines changed: 293 additions & 5 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/create/table/Index.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
public class Index implements TableElement, Serializable {
2525

2626
public enum Kind {
27-
PRIMARY_KEY, UNIQUE, INDEX, FULLTEXT, SPATIAL, FOREIGN_KEY, CHECK, EXCLUDE, DEFAULT, OTHER
27+
PRIMARY_KEY, UNIQUE, INDEX, FULLTEXT, SPATIAL, FOREIGN_KEY, CHECK, EXCLUDE, DEFAULT, NOT_NULL, OTHER
2828
}
2929

3030
public enum Clustering {
@@ -243,6 +243,8 @@ private static Kind classifyType(String type) {
243243
return Kind.SPATIAL;
244244
case "FOREIGN":
245245
return Kind.FOREIGN_KEY;
246+
case "NOT":
247+
return "NOT NULL".equals(normalized) ? Kind.NOT_NULL : Kind.OTHER;
246248
case "CHECK":
247249
return Kind.CHECK;
248250
case "EXCLUDE":
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.table;
11+
12+
import java.util.function.Consumer;
13+
import net.sf.jsqlparser.expression.Expression;
14+
import net.sf.jsqlparser.schema.Column;
15+
16+
/** PostgreSQL 18 table-level NOT NULL constraint, optionally named and non-inheritable. */
17+
public class NotNullConstraint extends NamedConstraint {
18+
private Column column;
19+
private boolean noInherit;
20+
21+
public NotNullConstraint() {
22+
setType("NOT NULL");
23+
}
24+
25+
public Column getColumn() {
26+
return column;
27+
}
28+
29+
public void setColumn(Column column) {
30+
this.column = column;
31+
}
32+
33+
public boolean isNoInherit() {
34+
return noInherit;
35+
}
36+
37+
public void setNoInherit(boolean noInherit) {
38+
this.noInherit = noInherit;
39+
}
40+
41+
public NotNullConstraint withColumn(Column column) {
42+
setColumn(column);
43+
return this;
44+
}
45+
46+
public NotNullConstraint withNoInherit(boolean noInherit) {
47+
setNoInherit(noInherit);
48+
return this;
49+
}
50+
51+
@Override
52+
public NotNullConstraint withName(String name) {
53+
setName(name);
54+
return this;
55+
}
56+
57+
public NotNullConstraint withConstraintAttributes(ConstraintAttributes attributes) {
58+
setConstraintAttributes(attributes);
59+
return this;
60+
}
61+
62+
@Override
63+
public void appendTo(StringBuilder sql, Consumer<Expression> expressionPrinter) {
64+
if (column == null) {
65+
throw new IllegalStateException("NOT NULL requires a target column");
66+
}
67+
appendConstraintPrefixTo(sql);
68+
sql.append("NOT NULL ");
69+
expressionPrinter.accept(column);
70+
if (noInherit) {
71+
sql.append(" NO INHERIT");
72+
}
73+
appendConstraintAttributesTo(sql);
74+
}
75+
}

‎src/main/java/net/sf/jsqlparser/util/TableDefinitionTraversal.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import net.sf.jsqlparser.statement.create.table.ExcludeConstraint;
2828
import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex;
2929
import net.sf.jsqlparser.statement.create.table.Index;
30+
import net.sf.jsqlparser.statement.create.table.NotNullConstraint;
3031
import net.sf.jsqlparser.statement.create.table.TableElement;
3132
import net.sf.jsqlparser.statement.create.table.TablePartitioning;
3233
import net.sf.jsqlparser.statement.create.table.PartitionBound;
@@ -165,6 +166,9 @@ public static void visit(TableElement element, Consumer<Expression> expressions,
165166
}
166167
}
167168
visitOptions(index.getStorageParameters(), expressions);
169+
if (index instanceof NotNullConstraint) {
170+
accept(((NotNullConstraint) index).getColumn(), expressions);
171+
}
168172
if (index instanceof CheckConstraint) {
169173
accept(((CheckConstraint) index).getExpression(), expressions);
170174
}

‎src/main/java/net/sf/jsqlparser/util/validation/validator/AlterValidator.java‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnDropNotNull;
2020
import net.sf.jsqlparser.statement.alter.AlterExpression.ColumnSetNotNull;
2121
import net.sf.jsqlparser.statement.alter.AlterOperation;
22+
import net.sf.jsqlparser.statement.create.table.NotNullConstraint;
2223
import net.sf.jsqlparser.statement.create.table.DefaultConstraint;
2324
import net.sf.jsqlparser.statement.create.table.ConstraintUsingIndex;
2425
import net.sf.jsqlparser.util.TableDefinitionTraversal;
@@ -92,7 +93,12 @@ public void validate(Alter alter, AlterExpression e) {
9293
validateOptionalName(c, NamedObject.constraint, e.getIndex().getName(), null, false,
9394
NamedObject.table);
9495
} else if (e.getIndex() != null) {
95-
validateName(c, NamedObject.index, e.getIndex().getName());
96+
if (e.getIndex() instanceof NotNullConstraint) {
97+
validateOptionalName(c, NamedObject.constraint, e.getIndex().getName(), null,
98+
false, NamedObject.table);
99+
} else {
100+
validateName(c, NamedObject.index, e.getIndex().getName());
101+
}
96102
if (e.getIndex().getColumns() != null) {
97103
validateOptionalColumnNames(c,
98104
e.getIndex().getColumns().stream()

‎src/main/java/net/sf/jsqlparser/util/validation/validator/CreateTableValidator.java‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import net.sf.jsqlparser.parser.feature.Feature;
1313
import net.sf.jsqlparser.statement.create.table.CreateTable;
1414
import net.sf.jsqlparser.statement.create.table.Index;
15+
import net.sf.jsqlparser.statement.create.table.NotNullConstraint;
1516
import net.sf.jsqlparser.util.TableDefinitionTraversal;
1617
import net.sf.jsqlparser.util.validation.ValidationCapability;
1718
import net.sf.jsqlparser.util.validation.metadata.NamedObject;
@@ -37,7 +38,12 @@ public void validate(CreateTable createTable) {
3738
validateOptionalFeature(c, createTable.getSelect(), Feature.createTableFromSelect);
3839
if (isNotEmpty(createTable.getIndexes())) {
3940
for (Index i : createTable.getIndexes()) {
40-
validateName(c, NamedObject.index, i.getName());
41+
if (i instanceof NotNullConstraint) {
42+
validateOptionalName(c, NamedObject.constraint, i.getName(), null, false,
43+
NamedObject.table);
44+
} else {
45+
validateName(c, NamedObject.index, i.getName());
46+
}
4147
}
4248
}
4349
validateName(c, NamedObject.table, createTable.getTable().getFullyQualifiedName(),

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14879,7 +14879,8 @@ Index TableConstraint(boolean createContext):
1487914879
|
1488014880
<K_CONSTRAINT>
1488114881
[ LOOKAHEAD({ !isTableIndexAhead() && getToken(1).kind != K_FOREIGN
14882-
&& getToken(1).kind != K_CHECK && getToken(1).kind != K_EXCLUDE }) constraintName=RelObjectName() ]
14882+
&& getToken(1).kind != K_CHECK && getToken(1).kind != K_EXCLUDE
14883+
&& !(getToken(1).kind == K_NOT && getToken(2).kind == K_NULL) }) constraintName=RelObjectName() ]
1488314884
(
1488414885
LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(createContext) {
1488514886
if (index instanceof NamedConstraint) {
@@ -14899,18 +14900,36 @@ Index TableConstraint(boolean createContext):
1489914900
}
1490014901
|
1490114902
index=PostgreSqlExcludeConstraint() { index.setName(constraintName); }
14903+
|
14904+
LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
14905+
&& getToken(1).kind == K_NOT })
14906+
index=PostgreSqlNotNullConstraint() { index.setName(constraintName); }
1490214907
)
1490314908
|
1490414909
fkIndex=ForeignKeySpec(null) { index = fkIndex; }
1490514910
|
1490614911
checkConstraint=CheckConstraintSpec(null) { index = checkConstraint; }
1490714912
|
1490814913
index=PostgreSqlExcludeConstraint()
14914+
|
14915+
LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
14916+
&& getToken(1).kind == K_NOT })
14917+
index=PostgreSqlNotNullConstraint()
1490914918
)
1491014919
PostgreSqlConstraintAttributes(index)
1491114920
{ return index; }
1491214921
}
1491314922

14923+
/** PostgreSQL 18 adds a table-level NOT NULL with a named target column. */
14924+
NotNullConstraint PostgreSqlNotNullConstraint():
14925+
{ NotNullConstraint constraint = new NotNullConstraint(); String name; }
14926+
{
14927+
<K_NOT> <K_NULL> name=RelObjectName() { constraint.setColumn(createColumn(name)); }
14928+
[ LOOKAHEAD({ getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) })
14929+
<K_NO> TypeDdlKeyword("INHERIT") { constraint.setNoInherit(true); } ]
14930+
{ return constraint; }
14931+
}
14932+
1491414933
ExcludeConstraint PostgreSqlExcludeConstraint():
1491514934
{
1491614935
ExcludeConstraint constraint = new ExcludeConstraint();
@@ -17309,7 +17328,9 @@ AlterExpression AlterExpressionAddAlterModify():
1730917328
|| Dialect.MYSQL.name().equals(getAsString(Feature.dialect)))
1731017329
&& (getToken(1).kind == K_CONSTRAINT || getToken(1).kind == K_PRIMARY
1731117330
|| getToken(1).kind == K_UNIQUE || getToken(1).kind == K_FOREIGN
17312-
|| getToken(1).kind == K_CHECK || getToken(1).kind == K_EXCLUDE) })
17331+
|| getToken(1).kind == K_CHECK || getToken(1).kind == K_EXCLUDE
17332+
|| getToken(1).kind == K_NOT
17333+
&& Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))) })
1731317334
index=TableConstraint(false) { setAlterTableIndex(alterExp, index); }
1731417335
|
1731517336
LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(false) {

‎src/site/sphinx/usage.rst‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,30 @@ Parse procedure definitions one SQL Server batch at a time: a procedure consumes
12751275
remaining batch, including SQL after an ``END``. Client-side ``GO`` batch splitting is
12761276
not performed by this routine declaration parser.
12771277

1278+
PostgreSQL table-level NOT NULL constraints
1279+
------------------------------------------
1280+
1281+
``Dialect.POSTGRESQL`` supports PostgreSQL 18's table-level
1282+
``CONSTRAINT nn NOT NULL id``, in both CREATE TABLE and ALTER TABLE ADD.
1283+
``NotNullConstraint`` exposes the constraint name, target ``Column`` and
1284+
``noInherit`` flag; ``getConstraintAttributes().isNotValid()`` represents an
1285+
ALTER ``NOT VALID`` clause when present.
1286+
1287+
.. code-block:: java
1288+
1289+
Alter alter = (Alter) CCJSqlParserUtil.parse(
1290+
"ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NOT VALID",
1291+
parser -> parser.withDialect(Dialect.POSTGRESQL));
1292+
NotNullConstraint constraint = (NotNullConstraint)
1293+
alter.getAlterExpressions().get(0).getIndex();
1294+
constraint.getColumn().setColumnName("other_id");
1295+
constraint.setName("other_nn");
1296+
1297+
The target column participates in expression visitors and deparsers. New nodes
1298+
can be built with ``new NotNullConstraint().withName("nn")
1299+
.withColumn(new Column("id"))``. This is distinct from column definitions and
1300+
``ALTER COLUMN ... SET NOT NULL``; those retain their existing APIs.
1301+
12781302
SQL Server identity inserts
12791303
---------------------------
12801304

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.alter;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.Set;
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.expression.Expression;
18+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
19+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
20+
import net.sf.jsqlparser.schema.Column;
21+
import net.sf.jsqlparser.statement.Statement;
22+
import net.sf.jsqlparser.statement.create.table.ConstraintAttributes;
23+
import net.sf.jsqlparser.statement.create.table.CreateTable;
24+
import net.sf.jsqlparser.statement.create.table.Index;
25+
import net.sf.jsqlparser.statement.create.table.NotNullConstraint;
26+
import net.sf.jsqlparser.util.TableDefinitionTraversal;
27+
import net.sf.jsqlparser.util.TablesNamesFinder;
28+
import net.sf.jsqlparser.util.deparser.AlterDeParser;
29+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
30+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
31+
import net.sf.jsqlparser.util.validation.ValidationContext;
32+
import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation;
33+
import net.sf.jsqlparser.util.validation.metadata.Named;
34+
import net.sf.jsqlparser.util.validation.metadata.NamedObject;
35+
import net.sf.jsqlparser.util.validation.validator.AlterValidator;
36+
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.ValueSource;
39+
40+
class PostgreSqlNotNullConstraintTest {
41+
@ParameterizedTest
42+
@ValueSource(strings = {"NOT NULL id", "CONSTRAINT nn NOT NULL id",
43+
"CONSTRAINT nn NOT NULL id NO INHERIT", "CONSTRAINT \"NN Name\" NOT NULL \"ID\""})
44+
void roundTripsCreateAndAlter(String body) throws JSQLParserException {
45+
for (String sql : new String[] {"CREATE TABLE t (id INT, " + body + ")",
46+
"ALTER TABLE t ADD " + body + " NOT VALID"}) {
47+
Statement statement = parse(sql);
48+
assertEquals(sql, statement.toString());
49+
NotNullConstraint constraint = constraint(statement);
50+
assertEquals(Index.Kind.NOT_NULL, constraint.getKind());
51+
assertNotNull(constraint.getColumn());
52+
roundTrip(statement);
53+
}
54+
}
55+
56+
@Test
57+
void supportsEditingAndConstruction() throws JSQLParserException {
58+
Alter alter =
59+
(Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NO INHERIT NOT VALID");
60+
NotNullConstraint constraint = constraint(alter);
61+
assertTrue(constraint.isNoInherit());
62+
assertTrue(constraint.getConstraintAttributes().isNotValid());
63+
constraint.setName("new_nn");
64+
constraint.getColumn().setColumnName("other_id");
65+
constraint.setNoInherit(false);
66+
assertEquals("ALTER TABLE t ADD CONSTRAINT new_nn NOT NULL other_id NOT VALID",
67+
alter.toString());
68+
roundTrip(alter);
69+
ConstraintAttributes attributes = new ConstraintAttributes();
70+
attributes.setNotValid(true);
71+
alter.getAlterExpressions().get(0).setIndex(new NotNullConstraint().withName("nn")
72+
.withColumn(new Column("id")).withNoInherit(true)
73+
.withConstraintAttributes(attributes));
74+
assertEquals("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NO INHERIT NOT VALID",
75+
alter.toString());
76+
roundTrip(alter);
77+
assertEquals(Index.Kind.NOT_NULL, new Index().withType("NOT NULL").getKind());
78+
}
79+
80+
@Test
81+
void visitsTargetColumnAndUsesCustomExpressionDeparser() throws JSQLParserException {
82+
Alter alter = (Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id");
83+
List<Expression> visited = new ArrayList<>();
84+
TableDefinitionTraversal.visit(alter.getAlterExpressions().get(0), visited::add, table -> {
85+
});
86+
assertEquals(List.of(constraint(alter).getColumn()), visited);
87+
CreateTable table = (CreateTable) parse("CREATE TABLE t (id INT, NOT NULL id)");
88+
visited.clear();
89+
TableDefinitionTraversal.visit(table, visited::add, source -> {
90+
});
91+
assertEquals(List.of(constraint(table).getColumn()), visited);
92+
assertEquals(Set.of("t"), new TablesNamesFinder().getTables(alter));
93+
StringBuilder sql = new StringBuilder();
94+
ExpressionDeParser expressions = new ExpressionDeParser() {
95+
@Override
96+
public <S> StringBuilder visit(Column column, S context) {
97+
return getBuilder().append("replacement");
98+
}
99+
};
100+
expressions.setBuilder(sql);
101+
new AlterDeParser(sql, expressions).deParse(alter);
102+
assertEquals("ALTER TABLE t ADD CONSTRAINT nn NOT NULL replacement", sql.toString());
103+
}
104+
105+
@Test
106+
void validatesNewConstraintAndExistingColumn() throws JSQLParserException {
107+
List<Named> visited = new ArrayList<>();
108+
DatabaseMetaDataValidation metadata = named -> {
109+
visited.add(named);
110+
return named.getNamedObject() != NamedObject.constraint;
111+
};
112+
AlterValidator validator = new AlterValidator();
113+
validator.setContext(new ValidationContext().setCapabilities(List.of(metadata)));
114+
validator.validate((Alter) parse("ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NOT VALID"));
115+
assertTrue(validator.getValidationErrors().isEmpty());
116+
assertTrue(visited.stream().anyMatch(
117+
n -> n.getNamedObject() == NamedObject.column && "id".equals(n.getFqn())));
118+
assertTrue(visited.stream().anyMatch(
119+
n -> n.getNamedObject() == NamedObject.constraint && "nn".equals(n.getFqn())));
120+
assertTrue(visited.stream().noneMatch(n -> n.getNamedObject() == NamedObject.index));
121+
}
122+
123+
@Test
124+
void preservesColumnNullabilityAndActionBoundaries() throws JSQLParserException {
125+
roundTrip(parse("ALTER TABLE t ADD NOT NULL id NOT VALID, ADD COLUMN extra INT"));
126+
for (String sql : new String[] {"CREATE TABLE t (id INT NOT NULL)",
127+
"ALTER TABLE t ALTER COLUMN id SET NOT NULL",
128+
"ALTER TABLE t ALTER COLUMN id DROP NOT NULL"}) {
129+
assertEquals(sql, CCJSqlParserUtil.parse(sql).toString());
130+
roundTrip(parse(sql));
131+
}
132+
}
133+
134+
private static NotNullConstraint constraint(Statement statement) {
135+
return (NotNullConstraint) (statement instanceof Alter
136+
? ((Alter) statement).getAlterExpressions().get(0).getIndex()
137+
: ((CreateTable) statement).getIndexes().get(0));
138+
}
139+
140+
private static Statement parse(String sql) throws JSQLParserException {
141+
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
142+
}
143+
144+
private static void roundTrip(Statement statement) throws JSQLParserException {
145+
StringBuilder sql = new StringBuilder();
146+
statement.accept(new StatementDeParser(sql), null);
147+
assertEquals(statement.toString(), sql.toString());
148+
assertEquals(sql.toString(), parse(sql.toString()).toString());
149+
}
150+
}

0 commit comments

Comments
 (0)