diff --git a/README.md b/README.md index 8ad991b09..5ff97a386 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **DML** | `INSERT` · `UPDATE` · `UPSERT` · `MERGE` · `DELETE` · `TRUNCATE TABLE` | | **DDL** | `CREATE …` · `ALTER …` · `DROP …` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | +| **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints | | **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` | Beyond statement shapes, the grammar handles nested sub-selects, bind parameters (`?`, diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java index 1972d43a1..faf95a641 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/CheckConstraint.java @@ -54,17 +54,12 @@ public void setEnforced(Boolean enforced) { @Override public String toString() { StringBuilder b = new StringBuilder(); - if (isUseConstraintKeyword() || getName() != null) { - b.append("CONSTRAINT"); - if (getName() != null) { - b.append(" ").append(getName()); - } - b.append(" "); - } + appendConstraintPrefixTo(b); b.append("CHECK (").append(expression).append(")"); if (enforced != null) { b.append(enforced ? " ENFORCED" : " NOT ENFORCED"); } + appendConstraintSuffixTo(b); appendConstraintAttributesTo(b); return b.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java index 344039e82..8ed95f456 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ForeignKeyIndex.java @@ -174,6 +174,7 @@ public String toString() { .append(PlainSelect.getStringList(getReferencedColumnNames(), true, true)); referentialActions.forEach(b::append); } + appendConstraintSuffixTo(b); appendConstraintAttributesTo(b); return b.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java index e6b8ed2f1..d35eaee03 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/NamedConstraint.java @@ -18,6 +18,45 @@ public class NamedConstraint extends Index { private String indexName; private boolean useConstraintKeyword; + private ConstraintNamePosition constraintNamePosition = ConstraintNamePosition.BEFORE; + + /** Position of the constraint symbol relative to its definition. */ + public enum ConstraintNamePosition { + BEFORE, AFTER + } + + public ConstraintNamePosition getConstraintNamePosition() { + return constraintNamePosition; + } + + public void setConstraintNamePosition(ConstraintNamePosition position) { + constraintNamePosition = java.util.Objects.requireNonNull(position, "position"); + } + + public NamedConstraint withConstraintNamePosition(ConstraintNamePosition position) { + setConstraintNamePosition(position); + return this; + } + + /** Appends the leading keyword and, for the usual syntax, the constraint name. */ + public void appendConstraintPrefixTo(StringBuilder builder) { + boolean leadingName = getName() != null + && constraintNamePosition == ConstraintNamePosition.BEFORE; + if (useConstraintKeyword || leadingName) { + builder.append("CONSTRAINT"); + if (leadingName) { + builder.append(' ').append(getName()); + } + builder.append(' '); + } + } + + /** Appends an Informix constraint name after the complete constraint definition. */ + public void appendConstraintSuffixTo(StringBuilder builder) { + if (constraintNamePosition == ConstraintNamePosition.AFTER && getName() != null) { + builder.append(" CONSTRAINT ").append(getName()); + } + } /** * Returns the optional index name declared after the constraint type. This is distinct from @@ -44,9 +83,6 @@ public void setUseConstraintKeyword(boolean useConstraintKeyword) { @Override public String toString() { String idxSpecText = PlainSelect.getStringList(getIndexSpec(), false, false); - String head = useConstraintKeyword || getName() != null - ? "CONSTRAINT" + (getName() != null ? " " + getName() : "") + " " - : ""; String keyword = getIndexKeyword() != null && !getType().toUpperCase(java.util.Locale.ROOT) .endsWith(getIndexKeyword().toUpperCase(java.util.Locale.ROOT)) @@ -61,9 +97,12 @@ public String toString() { : " " + PlainSelect.getStringList(getColumnsNames(), true, true)) + (!"".equals(idxSpecText) ? " " + idxSpecText : ""); - StringBuilder sql = new StringBuilder(head).append(tail); + StringBuilder sql = new StringBuilder(); + appendConstraintPrefixTo(sql); + sql.append(tail); appendConstraintOptionsTo(sql); if (getKind() != Kind.FOREIGN_KEY) { + appendConstraintSuffixTo(sql); appendConstraintAttributesTo(sql); } return sql.toString(); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java index 709997ed3..b96f14f15 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java @@ -94,13 +94,7 @@ private void deParseExclude(ExcludeConstraint constraint) { } private void deParseCheck(CheckConstraint constraint) { - if (constraint.getName() != null || constraint.isUseConstraintKeyword()) { - builder.append("CONSTRAINT"); - if (constraint.getName() != null) { - builder.append(' ').append(constraint.getName()); - } - builder.append(' '); - } + constraint.appendConstraintPrefixTo(builder); builder.append("CHECK ("); if (constraint.getExpression() != null) { constraint.getExpression().accept(expressionVisitor, null); @@ -111,6 +105,7 @@ private void deParseCheck(CheckConstraint constraint) { if (constraint.getEnforced() != null) { builder.append(constraint.getEnforced() ? " ENFORCED" : " NOT ENFORCED"); } + constraint.appendConstraintSuffixTo(builder); constraint.appendConstraintAttributesTo(builder); } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 860ab3821..dfe65ef7f 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14403,6 +14403,36 @@ DefaultConstraint DefaultConstraintSpec(): { return constraint; } } +/** Parses Informix's ADD CONSTRAINT definition [CONSTRAINT name]. */ +NamedConstraint InformixConstraint(): +{ + NamedConstraint constraint; + Token type; + List columns; + String name; +} +{ + + ( + ( type= | type= ) + columns=ColumnsNamesList() { + constraint = new NamedConstraint() + .withType(type.kind == K_PRIMARY ? "PRIMARY KEY" : type.image) + .withColumnsNames(columns); + } + | + constraint=ForeignKeySpec(null) + | + constraint=CheckConstraintSpec(null) + ) + [ name=RelObjectName() { constraint.setName(name); } ] + { + constraint.setUseConstraintKeyword(true); + constraint.setConstraintNamePosition(NamedConstraint.ConstraintNamePosition.AFTER); + return constraint; + } +} + /** * Parses ADD/ALTER CONSTRAINT clause within AlterExpression. * Handles: CONSTRAINT [UNIQUE [KEY|INDEX]] name columns @@ -14703,6 +14733,14 @@ AlterExpression AlterExpressionAddAlterModify(): alterExp.setIndex(index); } | + LOOKAHEAD( ( | + | | ) "(") + index=InformixConstraint() { + requireDdlSyntax(alterExp.getOperation() == AlterOperation.ADD, + "Informix constraint definitions require ADD"); + alterExp.setIndex(index); + } + | LOOKAHEAD({ isTableIndexAhead() }) index=TableIndexSpec(false) { alterExp.setIndex(index); if (index.getKind() == Index.Kind.PRIMARY_KEY) { diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/InformixConstraintTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/InformixConstraintTest.java new file mode 100644 index 000000000..b2db514bf --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/alter/InformixConstraintTest.java @@ -0,0 +1,147 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import net.sf.jsqlparser.statement.create.table.CheckConstraint; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.util.deparser.TableElementDeParser; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.test.TestUtils; +import java.util.Set; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex; +import net.sf.jsqlparser.statement.create.table.NamedConstraint; +import net.sf.jsqlparser.statement.create.table.NamedConstraint.ConstraintNamePosition; +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.MethodSource; + +class InformixConstraintTest { + static Stream definitions() { + return Stream.of("PRIMARY KEY (column)", "UNIQUE (column)", + "FOREIGN KEY (column) REFERENCES referenced_table(referenced_column)", + "CHECK (id > 0)", "PRIMARY KEY (id, tenant_id)", + "FOREIGN KEY (id, tenant_id) REFERENCES parent(id, tenant_id) ON DELETE CASCADE"); + } + + @ParameterizedTest + @MethodSource("definitions") + void preservesNamePlacement(String definition) throws Exception { + for (String suffix : List.of("", " CONSTRAINT constraint_name", + " CONSTRAINT \"constraint name\"")) { + String sql = "ALTER TABLE table_name ADD CONSTRAINT " + definition + suffix; + TestUtils.assertSqlCanBeParsedAndDeparsed(sql); + Alter statement = (Alter) CCJSqlParserUtil.parse(sql); + NamedConstraint constraint = + (NamedConstraint) statement.getAlterExpressions().get(0).getIndex(); + assertEquals(ConstraintNamePosition.AFTER, constraint.getConstraintNamePosition()); + assertTrue(constraint.isUseConstraintKeyword()); + assertEquals(suffix.isEmpty() ? null : suffix.substring(" CONSTRAINT ".length()), + constraint.getName()); + StringBuilder deparsed = new StringBuilder(); + statement.accept(new StatementDeParser(deparsed), null); + assertEquals(statement.toString(), deparsed.toString()); + assertEquals(statement.toString(), + CCJSqlParserUtil.parse(deparsed.toString()).toString()); + assertTrue(statement.toString().contains("ADD CONSTRAINT " + definition)); + if (!suffix.isEmpty()) { + assertTrue(statement.toString().endsWith(suffix)); + } + } + } + + @Test + void exposesForeignKeyAndMutableName() throws Exception { + Alter statement = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE child ADD CONSTRAINT FOREIGN KEY (id) REFERENCES parent(id) CONSTRAINT fk_child"); + ForeignKeyIndex key = (ForeignKeyIndex) statement.getAlterExpressions().get(0).getIndex(); + assertEquals(List.of("id"), key.getColumnsNames()); + assertEquals("parent", key.getTable().getName()); + assertEquals(List.of("id"), key.getReferencedColumnNames()); + key.setName("renamed_fk"); + assertTrue(statement.toString().endsWith("REFERENCES parent(id) CONSTRAINT renamed_fk")); + key.setName((String) null); + assertFalse(statement.toString().contains("fk_child")); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(statement.toString()).toString()); + } + + @Test + void keepsFollowingAlterActionsAndStatements() throws Exception { + String sql = + "ALTER TABLE t ADD CONSTRAINT PRIMARY KEY (id) CONSTRAINT pk_t, ADD COLUMN note INT; SELECT 1;"; + Statements statements = CCJSqlParserUtil.parseStatements(sql); + assertEquals(2, statements.size()); + assertEquals(2, ((Alter) statements.get(0)).getAlterExpressions().size()); + } + + @Test + void retainsLeadingNamesAndBuilders() throws Exception { + NamedConstraint built = new NamedConstraint().withType("PRIMARY KEY").withName("pk_t") + .withColumnsNames(List.of("id")); + assertEquals(ConstraintNamePosition.BEFORE, built.getConstraintNamePosition()); + assertEquals("CONSTRAINT pk_t PRIMARY KEY (id)", built.toString()); + for (String definition : List.of("PRIMARY KEY (id)", "UNIQUE (id)", + "FOREIGN KEY (id) REFERENCES parent(id)", "CHECK (id > 0)")) { + Alter statement = + (Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CONSTRAINT c " + definition); + assertEquals(ConstraintNamePosition.BEFORE, + ((NamedConstraint) statement.getAlterExpressions().get(0).getIndex()) + .getConstraintNamePosition()); + assertTrue(statement.toString().contains("CONSTRAINT c " + definition)); + } + } + + @Test + void keepsCheckExpressionVisitorAndForeignTableTraversal() throws Exception { + Alter statement = (Alter) CCJSqlParserUtil + .parse("ALTER TABLE child ADD CONSTRAINT CHECK (id > 0) CONSTRAINT positive_id"); + CheckConstraint check = + (CheckConstraint) statement + .getAlterExpressions().get(0).getIndex(); + StringBuilder builder = new StringBuilder(); + ExpressionDeParser visitor = + new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, + S context) { + getBuilder().append("renamed_id"); + return getBuilder(); + } + }; + visitor.setBuilder(builder); + new TableElementDeParser(builder, visitor).deParse(check); + assertEquals("CONSTRAINT CHECK (renamed_id > 0) CONSTRAINT positive_id", + builder.toString()); + assertEquals(Set.of("child", "parent"), + TablesNamesFinder.findTables( + "ALTER TABLE child ADD CONSTRAINT FOREIGN KEY (id) REFERENCES parent(id) CONSTRAINT fk")); + } + + @Test + void rejectsMissingOrDuplicateNames() { + for (String sql : List.of( + "ALTER TABLE t ADD CONSTRAINT PRIMARY KEY (id) CONSTRAINT", + "ALTER TABLE t ADD CONSTRAINT UNIQUE (id) CONSTRAINT a CONSTRAINT b", + "ALTER TABLE t ADD CONSTRAINT FOREIGN KEY (id) CONSTRAINT fk", + "ALTER TABLE t MODIFY CONSTRAINT UNIQUE (id) CONSTRAINT uk")) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + } +}