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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`?`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public String toString() {
.append(PlainSelect.getStringList(getReferencedColumnNames(), true, true));
referentialActions.forEach(b::append);
}
appendConstraintSuffixTo(b);
appendConstraintAttributesTo(b);
return b.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}
38 changes: 38 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -14403,6 +14403,36 @@ DefaultConstraint DefaultConstraintSpec():
{ return constraint; }
}

/** Parses Informix's ADD CONSTRAINT definition [CONSTRAINT name]. */
NamedConstraint InformixConstraint():
{
NamedConstraint constraint;
Token type;
List<String> columns;
String name;
}
{
<K_CONSTRAINT>
(
( type=<K_PRIMARY> <K_KEY> | type=<K_UNIQUE> )
columns=ColumnsNamesList() {
constraint = new NamedConstraint()
.withType(type.kind == K_PRIMARY ? "PRIMARY KEY" : type.image)
.withColumnsNames(columns);
}
|
constraint=ForeignKeySpec(null)
|
constraint=CheckConstraintSpec(null)
)
[ <K_CONSTRAINT> 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
Expand Down Expand Up @@ -14703,6 +14733,14 @@ AlterExpression AlterExpressionAddAlterModify():
alterExp.setIndex(index);
}
|
LOOKAHEAD(<K_CONSTRAINT> (<K_PRIMARY> <K_KEY> | <K_FOREIGN> <K_KEY>
| <K_UNIQUE> | <K_CHECK>) "(")
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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 <S> 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));
}
}
}
Loading