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 faf95a641..a3047fb42 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 @@ -11,6 +11,7 @@ import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.schema.Table; @@ -52,16 +53,20 @@ public void setEnforced(Boolean enforced) { } @Override - public String toString() { - StringBuilder b = new StringBuilder(); + public void appendTo(StringBuilder b, Consumer expressionPrinter) { appendConstraintPrefixTo(b); - b.append("CHECK (").append(expression).append(")"); + b.append("CHECK ("); + if (expression == null) { + b.append("null"); + } else { + expressionPrinter.accept(expression); + } + b.append(')'); if (enforced != null) { b.append(enforced ? " ENFORCED" : " NOT ENFORCED"); } appendConstraintSuffixTo(b); appendConstraintAttributesTo(b); - return b.toString(); } public CheckConstraint withTable(Table table) { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ExcludeConstraint.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ExcludeConstraint.java index 2539e0d23..f8ed3a836 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ExcludeConstraint.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ExcludeConstraint.java @@ -11,9 +11,9 @@ import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.statement.select.PlainSelect; public class ExcludeConstraint extends Index { @@ -33,8 +33,7 @@ public void setExpression(Expression expression) { } @Override - public String toString() { - StringBuilder exclusionStatement = new StringBuilder(); + public void appendTo(StringBuilder exclusionStatement, Consumer expressionPrinter) { if (getName() != null) { exclusionStatement.append("CONSTRAINT ").append(getName()).append(' '); } @@ -43,15 +42,16 @@ public String toString() { exclusionStatement.append(" USING ").append(getUsing()); } if (getColumns() != null) { - exclusionStatement.append(' ') - .append(PlainSelect.getStringList(getColumns(), true, true)); + exclusionStatement.append(' '); + appendColumnsTo(exclusionStatement, expressionPrinter); } - appendConstraintOptionsTo(exclusionStatement); + appendConstraintOptionsTo(exclusionStatement, expressionPrinter); if (expression != null) { - exclusionStatement.append(" WHERE (").append(expression).append(')'); + exclusionStatement.append(" WHERE ("); + expressionPrinter.accept(expression); + exclusionStatement.append(')'); } appendConstraintAttributesTo(exclusionStatement); - return exclusionStatement.toString(); } public ExcludeConstraint withExpression(Expression expression) { 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 8ed95f456..869bf7969 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 @@ -20,6 +20,8 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; import java.util.Optional; import java.util.Set; @@ -165,8 +167,9 @@ public void setOnUpdateReferenceOption(String onUpdateReferenceOption) { } @Override - public String toString() { - StringBuilder b = new StringBuilder(super.toString()).append(" "); + public void appendTo(StringBuilder b, Consumer expressionPrinter) { + super.appendTo(b, expressionPrinter); + b.append(' '); if (reference != null) { b.append(reference); } else { @@ -176,7 +179,6 @@ public String toString() { } appendConstraintSuffixTo(b); appendConstraintAttributesTo(b); - return b.toString(); } public ForeignKeyIndex withTable(Table table) { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java index cab867601..d66833d59 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/Index.java @@ -111,11 +111,17 @@ public String nullsDistinctClause() { } public void appendConstraintOptionsTo(StringBuilder sql) { + appendConstraintOptionsTo(sql, sql::append); + } + + public void appendConstraintOptionsTo(StringBuilder sql, + Consumer expressionPrinter) { if (includeColumns != null) { sql.append(" INCLUDE ").append(PlainSelect.getStringList(includeColumns, true, true)); } if (storageParameters != null) { - sql.append(" WITH ").append(PlainSelect.getStringList(storageParameters, true, true)); + sql.append(" WITH "); + Option.appendListTo(sql, storageParameters, expressionPrinter); } if (tableSpace != null) { sql.append(" USING INDEX TABLESPACE ").append(tableSpace); @@ -283,31 +289,49 @@ public Index withIndexKeyword(String indexKeyword) { @Override public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } + + /** Renders index definitions through the supplied expression writer. */ + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { String idxSpecText = PlainSelect.getStringList(idxSpec, false, false); String keyword = indexKeyword != null && (type == null || !type.toUpperCase(java.util.Locale.ROOT) .endsWith(indexKeyword.toUpperCase(java.util.Locale.ROOT))) ? " " + indexKeyword : ""; - String head = - (type != null ? type : "") + - keyword + - (!name.isEmpty() ? " " + getName() : "") + - (using != null ? " USING " + using : ""); - - String tail = (columns != null && !columns.isEmpty() - ? PlainSelect.getStringList(columns, true, true) - : "") - + (!idxSpecText.isEmpty() ? " " + idxSpecText : ""); - - StringBuilder sql = new StringBuilder(head).append(nullsDistinctClause()) - .append(clusteringClause()); - if (!tail.isEmpty()) { - sql.append(' ').append(tail); - } - appendConstraintOptionsTo(sql); + sql.append(type != null ? type : "").append(keyword); + if (!name.isEmpty()) { + sql.append(' ').append(getName()); + } + if (using != null) { + sql.append(" USING ").append(using); + } + sql.append(nullsDistinctClause()).append(clusteringClause()); + boolean hasColumns = columns != null && !columns.isEmpty(); + if (hasColumns) { + sql.append(' '); + appendColumnsTo(sql, expressionPrinter); + } + if (!idxSpecText.isEmpty()) { + sql.append(hasColumns ? " " : " ").append(idxSpecText); + } + appendConstraintOptionsTo(sql, expressionPrinter); appendConstraintAttributesTo(sql); - return sql.toString(); + } + + /** Appends a parenthesized list of keys, including expression keys and operator options. */ + protected void appendColumnsTo(StringBuilder sql, Consumer expressionPrinter) { + sql.append('('); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) { + sql.append(", "); + } + columns.get(i).appendTo(sql, expressionPrinter); + } + sql.append(')'); } public Index withType(String type) { 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 b53ed6dbd..d34dd5a26 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 @@ -11,6 +11,8 @@ import java.util.Collection; import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -81,32 +83,34 @@ public void setUseConstraintKeyword(boolean useConstraintKeyword) { } @Override - public String toString() { + public void appendTo(StringBuilder sql, Consumer expressionPrinter) { String idxSpecText = PlainSelect.getStringList(getIndexSpec(), false, false); String keyword = getIndexKeyword() != null && !getType().toUpperCase(java.util.Locale.ROOT) .endsWith(getIndexKeyword().toUpperCase(java.util.Locale.ROOT)) ? " " + getIndexKeyword() : ""; - String tail = getType() - + nullsDistinctClause() - + keyword - + clusteringClause() - + (indexName != null ? " " + indexName : "") - + (getUsing() != null ? " USING " + getUsing() : "") - + (getColumns() == null ? "" - : " " + PlainSelect.getStringList(getColumnsNames(), true, true)) - + - (!"".equals(idxSpecText) ? " " + idxSpecText : ""); - StringBuilder sql = new StringBuilder(); appendConstraintPrefixTo(sql); - sql.append(tail); - appendConstraintOptionsTo(sql); + sql.append(getType()).append(nullsDistinctClause()).append(keyword) + .append(clusteringClause()); + if (indexName != null) { + sql.append(' ').append(indexName); + } + if (getUsing() != null) { + sql.append(" USING ").append(getUsing()); + } + if (getColumns() != null) { + sql.append(' '); + appendColumnsTo(sql, expressionPrinter); + } + if (!idxSpecText.isEmpty()) { + sql.append(' ').append(idxSpecText); + } + appendConstraintOptionsTo(sql, expressionPrinter); if (getKind() != Kind.FOREIGN_KEY) { appendConstraintSuffixTo(sql); appendConstraintAttributesTo(sql); } - return sql.toString(); } public NamedConstraint withIndexName(String indexName) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java index 6922525d9..6e795227c 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterDeParser.java @@ -13,7 +13,7 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.alter.AlterExpression; import net.sf.jsqlparser.statement.alter.AlterExpressionPrimaryKey; -import net.sf.jsqlparser.statement.create.table.DefaultConstraint; +import net.sf.jsqlparser.statement.alter.AlterOperation; import net.sf.jsqlparser.statement.select.PlainSelect; import java.util.Iterator; @@ -54,9 +54,17 @@ private void deParseAction(AlterExpression action) { expression -> expression.accept(expressionVisitor, null)); return; } - if (action.getIndex() instanceof DefaultConstraint) { + if (action.getOperation() == AlterOperation.ADD && action.getIndex() != null + && action.getConstraintType() == null) { builder.append(action.getOperation()).append(' '); new TableElementDeParser(builder, expressionVisitor).deParse(action.getIndex()); + if (action.getConstraints() != null && !action.getConstraints().isEmpty()) { + builder.append(' ') + .append(PlainSelect.getStringList(action.getConstraints(), false, false)); + } + if (action.getUseEqual()) { + builder.append('='); + } deParseTail(action); return; } 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 b96f14f15..3fc899998 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/TableElementDeParser.java @@ -9,13 +9,9 @@ */ package net.sf.jsqlparser.util.deparser; -import java.util.Iterator; import net.sf.jsqlparser.expression.ExpressionVisitor; -import net.sf.jsqlparser.statement.create.table.CheckConstraint; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.create.table.ColumnOption; -import net.sf.jsqlparser.statement.create.table.DefaultConstraint; -import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; import net.sf.jsqlparser.statement.create.table.Index; import net.sf.jsqlparser.statement.create.table.TableElement; @@ -31,13 +27,9 @@ public TableElementDeParser(StringBuilder builder, @Override public void deParse(TableElement element) { - if (element instanceof DefaultConstraint) { - ((DefaultConstraint) element).appendTo(builder, + if (element instanceof Index) { + ((Index) element).appendTo(builder, expression -> expression.accept(expressionVisitor, null)); - } else if (element instanceof ExcludeConstraint) { - deParseExclude((ExcludeConstraint) element); - } else if (element instanceof CheckConstraint) { - deParseCheck((CheckConstraint) element); } else if (element instanceof ColumnDefinition && ((ColumnDefinition) element).getColumnOptions() != null) { deParseColumn((ColumnDefinition) element); @@ -64,48 +56,4 @@ private void deParseColumn(ColumnDefinition column) { } } - private void deParseExclude(ExcludeConstraint constraint) { - if (constraint.getName() != null) { - builder.append("CONSTRAINT ").append(constraint.getName()).append(' '); - } - builder.append("EXCLUDE"); - if (constraint.getUsing() != null) { - builder.append(" USING ").append(constraint.getUsing()); - } - if (constraint.getColumns() != null) { - builder.append(" ("); - for (Iterator iterator = - constraint.getColumns().iterator(); iterator.hasNext();) { - iterator.next().appendTo(builder, - expression -> expression.accept(expressionVisitor, null)); - if (iterator.hasNext()) { - builder.append(", "); - } - } - builder.append(')'); - } - constraint.appendConstraintOptionsTo(builder); - if (constraint.getExpression() != null) { - builder.append(" WHERE ("); - constraint.getExpression().accept(expressionVisitor, null); - builder.append(')'); - } - constraint.appendConstraintAttributesTo(builder); - } - - private void deParseCheck(CheckConstraint constraint) { - constraint.appendConstraintPrefixTo(builder); - builder.append("CHECK ("); - if (constraint.getExpression() != null) { - constraint.getExpression().accept(expressionVisitor, null); - } else { - builder.append("null"); - } - builder.append(')'); - if (constraint.getEnforced() != null) { - builder.append(constraint.getEnforced() ? " ENFORCED" : " NOT ENFORCED"); - } - constraint.appendConstraintSuffixTo(builder); - constraint.appendConstraintAttributesTo(builder); - } } diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/TableConstraintExpressionDeParserTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/TableConstraintExpressionDeParserTest.java new file mode 100644 index 000000000..f3973613e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/TableConstraintExpressionDeParserTest.java @@ -0,0 +1,115 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.util.deparser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.Index; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TableConstraintExpressionDeParserTest { + @ParameterizedTest + @ValueSource(strings = {"", " NOT ENFORCED"}) + void alterCheckUsesExpressionVisitorAndPreservesAttributes(String attributes) + throws JSQLParserException { + assertRewrite("ALTER TABLE t ADD CONSTRAINT c CHECK (id > 0)" + attributes, + "ALTER TABLE t ADD CONSTRAINT c CHECK (mapped_id > 100)" + attributes); + } + + @Test + void createAndAlterFunctionalIndexesUseExpressionVisitor() throws JSQLParserException { + assertRewrite("CREATE TABLE t (id INT, INDEX idx ((id + 1)))", + "CREATE TABLE t (id INT, INDEX idx ((mapped_id + 101)))"); + assertRewrite("ALTER TABLE t ADD INDEX idx ((id + 1))", + "ALTER TABLE t ADD INDEX idx ((mapped_id + 101))"); + } + + @Test + void alterExcludeUsesVisitorForKeysAndPredicate() throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT, CONSTRAINT c EXCLUDE USING gist " + + "((id + 1) WITH =) WHERE (id > 0) DEFERRABLE)"); + Alter alter = (Alter) CCJSqlParserUtil.parse( + "ALTER TABLE t ADD CONSTRAINT placeholder CHECK (id > 0)"); + alter.getAlterExpressions().get(0).setIndex(table.getIndexes().get(0)); + assertEquals("ALTER TABLE t ADD CONSTRAINT c EXCLUDE USING gist " + + "((mapped_id + 101) WITH =) WHERE (mapped_id > 100) DEFERRABLE", rewrite(alter)); + } + + @Test + void checkAttributesRemainAfterExpressionRewrite() throws JSQLParserException { + Alter alter = + (Alter) CCJSqlParserUtil.parse("ALTER TABLE t ADD CONSTRAINT c CHECK (id > 0)"); + ConstraintAttributes attributes = new ConstraintAttributes(); + attributes.setNotValid(true); + alter.getAlterExpressions().get(0).getIndex().setConstraintAttributes(attributes); + assertEquals("ALTER TABLE t ADD CONSTRAINT c CHECK (mapped_id > 100) NOT VALID", + rewrite(alter)); + } + + @Test + void constraintStorageOptionsUseExpressionVisitor() throws JSQLParserException { + CreateTable table = (CreateTable) CCJSqlParserUtil.parse( + "CREATE TABLE t (id INT, CONSTRAINT c UNIQUE (id))"); + table.getIndexes().get(0).setStorageParameters( + Arrays.asList(new Index.Option("fillfactor", new LongValue(70), true))); + assertEquals("CREATE TABLE t (id INT, CONSTRAINT c UNIQUE (id) WITH (fillfactor = 170))", + rewrite(table)); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER TABLE t ADD CONSTRAINT c FOREIGN KEY (id) REFERENCES p(id) ON DELETE CASCADE", + "ALTER TABLE t ADD CONSTRAINT c UNIQUE (id) DEFERRABLE", + "CREATE TABLE t (id INT, CONSTRAINT c PRIMARY KEY pk (id), FOREIGN KEY (id) REFERENCES p(id))", + "ALTER TABLE t ADD INDEX idx (id) COMMENT 'lookup'", + "ALTER TABLE t ADD CONSTRAINT c DEFAULT 1 FOR id", + "ALTER TABLE t ALTER INDEX idx INVISIBLE" + }) + void defaultDeparserPreservesCompleteConstraint(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + StringBuilder buffer = new StringBuilder(); + statement.accept(new StatementDeParser(buffer), null); + assertEquals(statement.toString(), buffer.toString()); + assertEquals(statement.toString(), CCJSqlParserUtil.parse(buffer.toString()).toString()); + } + + private static void assertRewrite(String sql, String expected) throws JSQLParserException { + assertEquals(expected, rewrite(CCJSqlParserUtil.parse(sql))); + } + + private static String rewrite(Statement statement) { + StringBuilder buffer = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + return getBuilder().append("mapped_").append(column.getColumnName()); + } + + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append(value.getValue() + 100); + } + }; + statement.accept(new StatementDeParser(expressions, new SelectDeParser(), buffer), null); + return buffer.toString(); + } +}