Skip to content

Commit b47e6af

Browse files
authored
feat: reuse NOT NULL constraints for PostgreSQL column definitions (#2696)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 63d65ed commit b47e6af

3 files changed

Lines changed: 144 additions & 7 deletions

File tree

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,30 @@
1313
import net.sf.jsqlparser.expression.Expression;
1414
import net.sf.jsqlparser.schema.Column;
1515

16-
/** PostgreSQL 18 table-level NOT NULL constraint, optionally named and non-inheritable. */
16+
/** PostgreSQL 18 column- or table-level NOT NULL constraint. */
1717
public class NotNullConstraint extends NamedConstraint {
1818
private Column column;
1919
private boolean noInherit;
20+
private boolean columnConstraint;
2021

2122
public NotNullConstraint() {
2223
setType("NOT NULL");
2324
}
2425

26+
/** Whether the target is implicit in the enclosing column definition. */
27+
public boolean isColumnConstraint() {
28+
return columnConstraint;
29+
}
30+
31+
public void setColumnConstraint(boolean columnConstraint) {
32+
this.columnConstraint = columnConstraint;
33+
}
34+
35+
public NotNullConstraint withColumnConstraint(boolean columnConstraint) {
36+
setColumnConstraint(columnConstraint);
37+
return this;
38+
}
39+
2540
public Column getColumn() {
2641
return column;
2742
}
@@ -61,12 +76,15 @@ public NotNullConstraint withConstraintAttributes(ConstraintAttributes attribute
6176

6277
@Override
6378
public void appendTo(StringBuilder sql, Consumer<Expression> expressionPrinter) {
64-
if (column == null) {
79+
if (!columnConstraint && column == null) {
6580
throw new IllegalStateException("NOT NULL requires a target column");
6681
}
6782
appendConstraintPrefixTo(sql);
68-
sql.append("NOT NULL ");
69-
expressionPrinter.accept(column);
83+
sql.append("NOT NULL");
84+
if (!columnConstraint) {
85+
sql.append(' ');
86+
expressionPrinter.accept(column);
87+
}
7088
if (noInherit) {
7189
sql.append(" NO INHERIT");
7290
}

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14138,6 +14138,11 @@ ColumnOption ColumnDefinitionOption(): {
1413814138
|| (getToken(1).kind == K_AS && "(".equals(getToken(2).image)) })
1413914139
generated=GeneratedColumnDefinition() { option = ColumnOption.generated(generated); }
1414014140
|
14141+
LOOKAHEAD([ <K_CONSTRAINT> RelObjectName() ] <K_NOT> <K_NULL>,
14142+
{ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
14143+
&& (getToken(1).kind == K_CONSTRAINT || getToken(3).kind == K_NO) })
14144+
constraint=PostgreSqlColumnNotNullConstraint() { option = ColumnOption.constraint(constraint); }
14145+
|
1414114146
LOOKAHEAD(<K_NOT> <K_NULL>) <K_NOT> <K_NULL> { option = ColumnOption.nullability(false); }
1414214147
|
1414314148
LOOKAHEAD(<K_NULL>) <K_NULL> { option = ColumnOption.nullability(true); }
@@ -14958,14 +14963,29 @@ Index TableConstraint(boolean createContext):
1495814963

1495914964
/** PostgreSQL 18 adds a table-level NOT NULL with a named target column. */
1496014965
NotNullConstraint PostgreSqlNotNullConstraint():
14961-
{ NotNullConstraint constraint = new NotNullConstraint(); String name; }
14966+
{ NotNullConstraint constraint = new NotNullConstraint(); String name; boolean noInherit; }
1496214967
{
1496314968
<K_NOT> <K_NULL> name=RelObjectName() { constraint.setColumn(createColumn(name)); }
14964-
[ LOOKAHEAD({ getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) })
14965-
<K_NO> TypeDdlKeyword("INHERIT") { constraint.setNoInherit(true); } ]
14969+
noInherit=PostgreSqlNoInheritClause() { constraint.setNoInherit(noInherit); }
1496614970
{ return constraint; }
1496714971
}
1496814972

14973+
NotNullConstraint PostgreSqlColumnNotNullConstraint():
14974+
{ String name = null; boolean noInherit; }
14975+
{
14976+
[ <K_CONSTRAINT> name=RelObjectName() ]
14977+
<K_NOT> <K_NULL> noInherit=PostgreSqlNoInheritClause()
14978+
{ return new NotNullConstraint().withColumnConstraint(true).withName(name).withNoInherit(noInherit); }
14979+
}
14980+
14981+
boolean PostgreSqlNoInheritClause():
14982+
{ boolean noInherit = false; }
14983+
{
14984+
[ LOOKAHEAD({ getToken(1).kind == K_NO && "INHERIT".equalsIgnoreCase(getToken(2).image) })
14985+
<K_NO> TypeDdlKeyword("INHERIT") { noInherit = true; } ]
14986+
{ return noInherit; }
14987+
}
14988+
1496914989
ExcludeConstraint PostgreSqlExcludeConstraint():
1497014990
{
1497114991
ExcludeConstraint constraint = new ExcludeConstraint();
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import net.sf.jsqlparser.JSQLParserException;
16+
import net.sf.jsqlparser.expression.Expression;
17+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
18+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
19+
import net.sf.jsqlparser.statement.Statement;
20+
import net.sf.jsqlparser.statement.alter.Alter;
21+
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
22+
import net.sf.jsqlparser.statement.create.table.ColumnOption;
23+
import net.sf.jsqlparser.statement.create.table.CreateTable;
24+
import net.sf.jsqlparser.statement.create.table.NotNullConstraint;
25+
import net.sf.jsqlparser.util.TableDefinitionTraversal;
26+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.ValueSource;
30+
31+
class PostgreSqlColumnNotNullTest {
32+
@ParameterizedTest
33+
@ValueSource(strings = {"NOT NULL NO INHERIT", "CONSTRAINT nn NOT NULL",
34+
"CONSTRAINT nn NOT NULL NO INHERIT", "CONSTRAINT \"Named NN\" NOT NULL NO INHERIT"})
35+
void reusesNotNullConstraintsInCreateAndAlterColumns(String clause) throws JSQLParserException {
36+
for (String prefix : List.of("CREATE TABLE t (", "ALTER TABLE t ADD COLUMN ")) {
37+
Statement statement = parse(prefix + "id INT " + clause + " DEFAULT 7"
38+
+ (prefix.startsWith("CREATE") ? ", value INT)" : ", ADD COLUMN value INT"));
39+
ColumnDefinition column = column(statement);
40+
NotNullConstraint constraint = assertInstanceOf(NotNullConstraint.class,
41+
column.getColumnOptions().get(0).getConstraint());
42+
assertTrue(constraint.isColumnConstraint());
43+
assertNull(constraint.getColumn());
44+
assertEquals(clause.endsWith("NO INHERIT"), constraint.isNoInherit());
45+
assertEquals(ColumnOption.Kind.DEFAULT, column.getColumnOptions().get(1).getKind());
46+
roundTrip(statement);
47+
constraint.setName("replacement");
48+
constraint.setNoInherit(!constraint.isNoInherit());
49+
assertTrue(statement.toString().contains("CONSTRAINT replacement NOT NULL"));
50+
roundTrip(statement);
51+
}
52+
}
53+
54+
@Test
55+
void retainsPlainNullabilityAndUsesTheExistingTableModel() throws JSQLParserException {
56+
ColumnOption option =
57+
column(parse("CREATE TABLE t (id INT NOT NULL)")).getColumnOptions().get(0);
58+
assertEquals(ColumnOption.Kind.NULLABILITY, option.getKind());
59+
assertEquals(false, option.getNullable());
60+
NotNullConstraint table = (NotNullConstraint) ((CreateTable) parse(
61+
"CREATE TABLE t (id INT, CONSTRAINT nn NOT NULL id NO INHERIT)")).getIndexes()
62+
.get(0);
63+
assertFalse(table.isColumnConstraint());
64+
assertEquals("id", table.getColumn().getColumnName());
65+
assertEquals("CONSTRAINT nn NOT NULL NO INHERIT", new NotNullConstraint()
66+
.withColumnConstraint(true).withName("nn").withNoInherit(true).toString());
67+
}
68+
69+
@Test
70+
void doesNotInventASecondColumnExpressionForAnImplicitTarget() throws JSQLParserException {
71+
CreateTable table =
72+
(CreateTable) parse("CREATE TABLE t (id INT DEFAULT 7 NOT NULL NO INHERIT)");
73+
List<Expression> expressions = new ArrayList<>();
74+
TableDefinitionTraversal.visit(table, expressions::add, t -> {
75+
});
76+
assertEquals(1, expressions.size());
77+
assertEquals("7", expressions.get(0).toString());
78+
roundTrip(table);
79+
assertEquals(2, CCJSqlParserUtil.parseStatements(table + "; SELECT 1",
80+
p -> p.withDialect(Dialect.POSTGRESQL)).size());
81+
}
82+
83+
private static ColumnDefinition column(Statement statement) {
84+
return statement instanceof CreateTable
85+
? ((CreateTable) statement).getColumnDefinitions().get(0)
86+
: ((Alter) statement).getAlterExpressions().get(0).getColDataTypeList().get(0);
87+
}
88+
89+
private static Statement parse(String sql) throws JSQLParserException {
90+
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
91+
}
92+
93+
private static void roundTrip(Statement statement) throws JSQLParserException {
94+
StringBuilder out = new StringBuilder();
95+
statement.accept(new StatementDeParser(out));
96+
assertEquals(statement.toString(), out.toString());
97+
assertEquals(out.toString(), parse(out.toString()).toString());
98+
}
99+
}

0 commit comments

Comments
 (0)