Skip to content

Commit 1c8c29c

Browse files
committed
Support Informix trailing constraint names and share constraint rendering
1 parent b2115ac commit 1c8c29c

7 files changed

Lines changed: 234 additions & 18 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
141141
| **DML** | `INSERT` · `UPDATE` · `UPSERT` · `MERGE` · `DELETE` · `TRUNCATE TABLE` |
142142
| **DDL** | `CREATE …` · `ALTER …` · `DROP …` |
143143
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |
144+
| **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints |
144145
| **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` |
145146

146147
Beyond statement shapes, the grammar handles nested sub-selects, bind parameters (`?`,

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,12 @@ public void setEnforced(Boolean enforced) {
5454
@Override
5555
public String toString() {
5656
StringBuilder b = new StringBuilder();
57-
if (isUseConstraintKeyword() || getName() != null) {
58-
b.append("CONSTRAINT");
59-
if (getName() != null) {
60-
b.append(" ").append(getName());
61-
}
62-
b.append(" ");
63-
}
57+
appendConstraintPrefixTo(b);
6458
b.append("CHECK (").append(expression).append(")");
6559
if (enforced != null) {
6660
b.append(enforced ? " ENFORCED" : " NOT ENFORCED");
6761
}
62+
appendConstraintSuffixTo(b);
6863
appendConstraintAttributesTo(b);
6964
return b.toString();
7065
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ public String toString() {
174174
.append(PlainSelect.getStringList(getReferencedColumnNames(), true, true));
175175
referentialActions.forEach(b::append);
176176
}
177+
appendConstraintSuffixTo(b);
177178
appendConstraintAttributesTo(b);
178179
return b.toString();
179180
}

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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,45 @@ public class NamedConstraint extends Index {
1818

1919
private String indexName;
2020
private boolean useConstraintKeyword;
21+
private ConstraintNamePosition constraintNamePosition = ConstraintNamePosition.BEFORE;
22+
23+
/** Position of the constraint symbol relative to its definition. */
24+
public enum ConstraintNamePosition {
25+
BEFORE, AFTER
26+
}
27+
28+
public ConstraintNamePosition getConstraintNamePosition() {
29+
return constraintNamePosition;
30+
}
31+
32+
public void setConstraintNamePosition(ConstraintNamePosition position) {
33+
constraintNamePosition = java.util.Objects.requireNonNull(position, "position");
34+
}
35+
36+
public NamedConstraint withConstraintNamePosition(ConstraintNamePosition position) {
37+
setConstraintNamePosition(position);
38+
return this;
39+
}
40+
41+
/** Appends the leading keyword and, for the usual syntax, the constraint name. */
42+
public void appendConstraintPrefixTo(StringBuilder builder) {
43+
boolean leadingName = getName() != null
44+
&& constraintNamePosition == ConstraintNamePosition.BEFORE;
45+
if (useConstraintKeyword || leadingName) {
46+
builder.append("CONSTRAINT");
47+
if (leadingName) {
48+
builder.append(' ').append(getName());
49+
}
50+
builder.append(' ');
51+
}
52+
}
53+
54+
/** Appends an Informix constraint name after the complete constraint definition. */
55+
public void appendConstraintSuffixTo(StringBuilder builder) {
56+
if (constraintNamePosition == ConstraintNamePosition.AFTER && getName() != null) {
57+
builder.append(" CONSTRAINT ").append(getName());
58+
}
59+
}
2160

2261
/**
2362
* Returns the optional index name declared after the constraint type. This is distinct from
@@ -44,9 +83,6 @@ public void setUseConstraintKeyword(boolean useConstraintKeyword) {
4483
@Override
4584
public String toString() {
4685
String idxSpecText = PlainSelect.getStringList(getIndexSpec(), false, false);
47-
String head = useConstraintKeyword || getName() != null
48-
? "CONSTRAINT" + (getName() != null ? " " + getName() : "") + " "
49-
: "";
5086
String keyword = getIndexKeyword() != null
5187
&& !getType().toUpperCase(java.util.Locale.ROOT)
5288
.endsWith(getIndexKeyword().toUpperCase(java.util.Locale.ROOT))
@@ -61,9 +97,12 @@ public String toString() {
6197
: " " + PlainSelect.getStringList(getColumnsNames(), true, true))
6298
+
6399
(!"".equals(idxSpecText) ? " " + idxSpecText : "");
64-
StringBuilder sql = new StringBuilder(head).append(tail);
100+
StringBuilder sql = new StringBuilder();
101+
appendConstraintPrefixTo(sql);
102+
sql.append(tail);
65103
appendConstraintOptionsTo(sql);
66104
if (getKind() != Kind.FOREIGN_KEY) {
105+
appendConstraintSuffixTo(sql);
67106
appendConstraintAttributesTo(sql);
68107
}
69108
return sql.toString();

‎src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,7 @@ private void deParseExclude(ExcludeConstraint constraint) {
9494
}
9595

9696
private void deParseCheck(CheckConstraint constraint) {
97-
if (constraint.getName() != null || constraint.isUseConstraintKeyword()) {
98-
builder.append("CONSTRAINT");
99-
if (constraint.getName() != null) {
100-
builder.append(' ').append(constraint.getName());
101-
}
102-
builder.append(' ');
103-
}
97+
constraint.appendConstraintPrefixTo(builder);
10498
builder.append("CHECK (");
10599
if (constraint.getExpression() != null) {
106100
constraint.getExpression().accept(expressionVisitor, null);
@@ -111,6 +105,7 @@ private void deParseCheck(CheckConstraint constraint) {
111105
if (constraint.getEnforced() != null) {
112106
builder.append(constraint.getEnforced() ? " ENFORCED" : " NOT ENFORCED");
113107
}
108+
constraint.appendConstraintSuffixTo(builder);
114109
constraint.appendConstraintAttributesTo(builder);
115110
}
116111
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14403,6 +14403,36 @@ DefaultConstraint DefaultConstraintSpec():
1440314403
{ return constraint; }
1440414404
}
1440514405

14406+
/** Parses Informix's ADD CONSTRAINT definition [CONSTRAINT name]. */
14407+
NamedConstraint InformixConstraint():
14408+
{
14409+
NamedConstraint constraint;
14410+
Token type;
14411+
List<String> columns;
14412+
String name;
14413+
}
14414+
{
14415+
<K_CONSTRAINT>
14416+
(
14417+
( type=<K_PRIMARY> <K_KEY> | type=<K_UNIQUE> )
14418+
columns=ColumnsNamesList() {
14419+
constraint = new NamedConstraint()
14420+
.withType(type.kind == K_PRIMARY ? "PRIMARY KEY" : type.image)
14421+
.withColumnsNames(columns);
14422+
}
14423+
|
14424+
constraint=ForeignKeySpec(null)
14425+
|
14426+
constraint=CheckConstraintSpec(null)
14427+
)
14428+
[ <K_CONSTRAINT> name=RelObjectName() { constraint.setName(name); } ]
14429+
{
14430+
constraint.setUseConstraintKeyword(true);
14431+
constraint.setConstraintNamePosition(NamedConstraint.ConstraintNamePosition.AFTER);
14432+
return constraint;
14433+
}
14434+
}
14435+
1440614436
/**
1440714437
* Parses ADD/ALTER CONSTRAINT clause within AlterExpression.
1440814438
* Handles: CONSTRAINT [UNIQUE [KEY|INDEX]] name columns
@@ -14703,6 +14733,14 @@ AlterExpression AlterExpressionAddAlterModify():
1470314733
alterExp.setIndex(index);
1470414734
}
1470514735
|
14736+
LOOKAHEAD(<K_CONSTRAINT> (<K_PRIMARY> <K_KEY> | <K_FOREIGN> <K_KEY>
14737+
| <K_UNIQUE> | <K_CHECK>) "(")
14738+
index=InformixConstraint() {
14739+
requireDdlSyntax(alterExp.getOperation() == AlterOperation.ADD,
14740+
"Informix constraint definitions require ADD");
14741+
alterExp.setIndex(index);
14742+
}
14743+
|
1470614744
LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(false) {
1470714745
alterExp.setIndex(index);
1470814746
if (index.getKind() == Index.Kind.PRIMARY_KEY) {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2019 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.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
import static org.junit.jupiter.api.Assertions.assertFalse;
15+
import static org.junit.jupiter.api.Assertions.assertThrows;
16+
import net.sf.jsqlparser.statement.create.table.CheckConstraint;
17+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
18+
import net.sf.jsqlparser.schema.Column;
19+
import net.sf.jsqlparser.util.deparser.TableElementDeParser;
20+
import net.sf.jsqlparser.util.TablesNamesFinder;
21+
import net.sf.jsqlparser.test.TestUtils;
22+
import java.util.Set;
23+
import java.util.List;
24+
import java.util.stream.Stream;
25+
import net.sf.jsqlparser.JSQLParserException;
26+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
27+
import net.sf.jsqlparser.statement.Statements;
28+
import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex;
29+
import net.sf.jsqlparser.statement.create.table.NamedConstraint;
30+
import net.sf.jsqlparser.statement.create.table.NamedConstraint.ConstraintNamePosition;
31+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
32+
import org.junit.jupiter.api.Test;
33+
import org.junit.jupiter.params.ParameterizedTest;
34+
import org.junit.jupiter.params.provider.MethodSource;
35+
36+
class InformixConstraintTest {
37+
static Stream<String> definitions() {
38+
return Stream.of("PRIMARY KEY (column)", "UNIQUE (column)",
39+
"FOREIGN KEY (column) REFERENCES referenced_table(referenced_column)",
40+
"CHECK (id > 0)", "PRIMARY KEY (id, tenant_id)",
41+
"FOREIGN KEY (id, tenant_id) REFERENCES parent(id, tenant_id) ON DELETE CASCADE");
42+
}
43+
44+
@ParameterizedTest
45+
@MethodSource("definitions")
46+
void preservesNamePlacement(String definition) throws Exception {
47+
for (String suffix : List.of("", " CONSTRAINT constraint_name",
48+
" CONSTRAINT \"constraint name\"")) {
49+
String sql = "ALTER TABLE table_name ADD CONSTRAINT " + definition + suffix;
50+
TestUtils.assertSqlCanBeParsedAndDeparsed(sql);
51+
Alter statement = (Alter) CCJSqlParserUtil.parse(sql);
52+
NamedConstraint constraint =
53+
(NamedConstraint) statement.getAlterExpressions().get(0).getIndex();
54+
assertEquals(ConstraintNamePosition.AFTER, constraint.getConstraintNamePosition());
55+
assertTrue(constraint.isUseConstraintKeyword());
56+
assertEquals(suffix.isEmpty() ? null : suffix.substring(" CONSTRAINT ".length()),
57+
constraint.getName());
58+
StringBuilder deparsed = new StringBuilder();
59+
statement.accept(new StatementDeParser(deparsed), null);
60+
assertEquals(statement.toString(), deparsed.toString());
61+
assertEquals(statement.toString(),
62+
CCJSqlParserUtil.parse(deparsed.toString()).toString());
63+
assertTrue(statement.toString().contains("ADD CONSTRAINT " + definition));
64+
if (!suffix.isEmpty()) {
65+
assertTrue(statement.toString().endsWith(suffix));
66+
}
67+
}
68+
}
69+
70+
@Test
71+
void exposesForeignKeyAndMutableName() throws Exception {
72+
Alter statement = (Alter) CCJSqlParserUtil.parse(
73+
"ALTER TABLE child ADD CONSTRAINT FOREIGN KEY (id) REFERENCES parent(id) CONSTRAINT fk_child");
74+
ForeignKeyIndex key = (ForeignKeyIndex) statement.getAlterExpressions().get(0).getIndex();
75+
assertEquals(List.of("id"), key.getColumnsNames());
76+
assertEquals("parent", key.getTable().getName());
77+
assertEquals(List.of("id"), key.getReferencedColumnNames());
78+
key.setName("renamed_fk");
79+
assertTrue(statement.toString().endsWith("REFERENCES parent(id) CONSTRAINT renamed_fk"));
80+
key.setName((String) null);
81+
assertFalse(statement.toString().contains("fk_child"));
82+
assertEquals(statement.toString(), CCJSqlParserUtil.parse(statement.toString()).toString());
83+
}
84+
85+
@Test
86+
void keepsFollowingAlterActionsAndStatements() throws Exception {
87+
String sql =
88+
"ALTER TABLE t ADD CONSTRAINT PRIMARY KEY (id) CONSTRAINT pk_t, ADD COLUMN note INT; SELECT 1;";
89+
Statements statements = CCJSqlParserUtil.parseStatements(sql);
90+
assertEquals(2, statements.size());
91+
assertEquals(2, ((Alter) statements.get(0)).getAlterExpressions().size());
92+
}
93+
94+
@Test
95+
void retainsLeadingNamesAndBuilders() throws Exception {
96+
NamedConstraint built = new NamedConstraint().withType("PRIMARY KEY").withName("pk_t")
97+
.withColumnsNames(List.of("id"));
98+
assertEquals(ConstraintNamePosition.BEFORE, built.getConstraintNamePosition());
99+
assertEquals("CONSTRAINT pk_t PRIMARY KEY (id)", built.toString());
100+
for (String definition : List.of("PRIMARY KEY (id)", "UNIQUE (id)",
101+
"FOREIGN KEY (id) REFERENCES parent(id)", "CHECK (id > 0)")) {
102+
Alter statement =
103+
(Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CONSTRAINT c " + definition);
104+
assertEquals(ConstraintNamePosition.BEFORE,
105+
((NamedConstraint) statement.getAlterExpressions().get(0).getIndex())
106+
.getConstraintNamePosition());
107+
assertTrue(statement.toString().contains("CONSTRAINT c " + definition));
108+
}
109+
}
110+
111+
@Test
112+
void keepsCheckExpressionVisitorAndForeignTableTraversal() throws Exception {
113+
Alter statement = (Alter) CCJSqlParserUtil
114+
.parse("ALTER TABLE child ADD CONSTRAINT CHECK (id > 0) CONSTRAINT positive_id");
115+
CheckConstraint check =
116+
(CheckConstraint) statement
117+
.getAlterExpressions().get(0).getIndex();
118+
StringBuilder builder = new StringBuilder();
119+
ExpressionDeParser visitor =
120+
new ExpressionDeParser() {
121+
@Override
122+
public <S> StringBuilder visit(Column column,
123+
S context) {
124+
getBuilder().append("renamed_id");
125+
return getBuilder();
126+
}
127+
};
128+
visitor.setBuilder(builder);
129+
new TableElementDeParser(builder, visitor).deParse(check);
130+
assertEquals("CONSTRAINT CHECK (renamed_id > 0) CONSTRAINT positive_id",
131+
builder.toString());
132+
assertEquals(Set.of("child", "parent"),
133+
TablesNamesFinder.findTables(
134+
"ALTER TABLE child ADD CONSTRAINT FOREIGN KEY (id) REFERENCES parent(id) CONSTRAINT fk"));
135+
}
136+
137+
@Test
138+
void rejectsMissingOrDuplicateNames() {
139+
for (String sql : List.of(
140+
"ALTER TABLE t ADD CONSTRAINT PRIMARY KEY (id) CONSTRAINT",
141+
"ALTER TABLE t ADD CONSTRAINT UNIQUE (id) CONSTRAINT a CONSTRAINT b",
142+
"ALTER TABLE t ADD CONSTRAINT FOREIGN KEY (id) CONSTRAINT fk",
143+
"ALTER TABLE t MODIFY CONSTRAINT UNIQUE (id) CONSTRAINT uk")) {
144+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)