diff --git a/src/main/java/net/sf/jsqlparser/schema/TableReference.java b/src/main/java/net/sf/jsqlparser/schema/TableReference.java index 1584d1dc21..2d64aa10b1 100644 --- a/src/main/java/net/sf/jsqlparser/schema/TableReference.java +++ b/src/main/java/net/sf/jsqlparser/schema/TableReference.java @@ -11,7 +11,10 @@ import java.io.Serializable; -/** A relation target with optional PostgreSQL inheritance scope, shared by LOCK and TRUNCATE. */ +/** + * A relation target with optional PostgreSQL inheritance scope, shared by LOCK, TRUNCATE and the + * targets of UPDATE, DELETE and MERGE. + */ public class TableReference implements Serializable { private Table table; private boolean only; @@ -84,6 +87,27 @@ public StringBuilder appendTo(StringBuilder builder) { return builder; } + /** + * Appends the target of UPDATE, DELETE or MERGE (PostgreSQL's relation_expr_opt_alias): the + * inheritance scope surrounds the table name and the alias of the table follows it. A target + * without a scope is the table as it renders itself, with its alias and hints. + */ + public StringBuilder appendWithAliasTo(StringBuilder builder) { + if (!only && !includeDescendants || table == null) { + return builder.append(table); + } + appendTo(builder); + if (table.getAlias() != null) { + builder.append(table.getAlias()); + } + return builder; + } + + /** Whether ONLY or * describes the inheritance scope of the table. */ + public boolean hasInheritanceScope() { + return only || includeDescendants; + } + @Override public String toString() { return appendTo(new StringBuilder()).toString(); diff --git a/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java b/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java index d82cafe8be..a87ec3bde2 100644 --- a/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java +++ b/src/main/java/net/sf/jsqlparser/statement/delete/Delete.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.PreferringClause; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.schema.TableReference; import net.sf.jsqlparser.statement.ForPortionClause; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.ReturningClause; @@ -32,6 +33,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -40,7 +42,7 @@ public class Delete extends ASTNodeAccessImpl implements Statement { private List> withItemsList; - private Table table; + private TableReference tableReference = new TableReference(null); private ForPortionClause forPortionClause; private OracleHint oracleHint = null; private List tables; @@ -48,6 +50,7 @@ public class Delete extends ASTNodeAccessImpl implements Statement { private List usingJoins; private List joins; private Expression where; + private String cursorName; private PreferringClause preferringClause; private Limit limit; private OptionClause option; @@ -131,11 +134,46 @@ public Delete withForPortionClause(ForPortionClause forPortionClause) { } public Table getTable() { - return table; + return tableReference.getTable(); } + /** Replaces the target table and keeps the inheritance scope of the target. */ public void setTable(Table name) { - table = name; + tableReference.setTable(name); + } + + /** + * The target as PostgreSQL's relation_expr_opt_alias: the table of {@link #getTable()} with its + * alias, and whether ONLY or * describes its inheritance scope. + */ + public TableReference getTableReference() { + return tableReference; + } + + public void setTableReference(TableReference tableReference) { + this.tableReference = Objects.requireNonNull(tableReference); + } + + public Delete withTableReference(TableReference tableReference) { + this.setTableReference(tableReference); + return this; + } + + /** + * The cursor of {@code WHERE CURRENT OF cursor}, which is written in place of the condition of + * {@link #getWhere()}. + */ + public String getCursorName() { + return cursorName; + } + + public void setCursorName(String cursorName) { + this.cursorName = cursorName; + } + + public Delete withCursorName(String cursorName) { + this.setCursorName(cursorName); + return this; } public Expression getWhere() { @@ -277,7 +315,7 @@ public String toString() { if (hasFrom) { b.append(" FROM"); } - b.append(" ").append(table); + tableReference.appendWithAliasTo(b.append(" ")); if (forPortionClause != null) { b.append(' ').append(forPortionClause); } @@ -292,7 +330,9 @@ public String toString() { appendJoinsTo(b, usingJoins); } - if (where != null) { + if (cursorName != null) { + b.append(" WHERE CURRENT OF ").append(cursorName); + } else if (where != null) { b.append(" WHERE ").append(where); } diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java b/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java index e047de72c0..e3ec4f7ae7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/Merge.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.schema.TableReference; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.ReturningClause; import net.sf.jsqlparser.statement.Statement; @@ -28,13 +29,14 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; public class Merge extends ASTNodeAccessImpl implements Statement { private List> withItemsList; - private Table table; + private TableReference tableReference = new TableReference(null); private OracleHint oracleHint = null; private FromItem fromItem; private Expression onCondition; @@ -121,11 +123,29 @@ public Merge addWithItemsList(Collection> withItemsList) { } public Table getTable() { - return table; + return tableReference.getTable(); } + /** Replaces the target table and keeps the inheritance scope of the target. */ public void setTable(Table name) { - table = name; + tableReference.setTable(name); + } + + /** + * The target as PostgreSQL's relation_expr_opt_alias: the table of {@link #getTable()} with its + * alias, and whether ONLY or * describes its inheritance scope. + */ + public TableReference getTableReference() { + return tableReference; + } + + public void setTableReference(TableReference tableReference) { + this.tableReference = Objects.requireNonNull(tableReference); + } + + public Merge withTableReference(TableReference tableReference) { + this.setTableReference(tableReference); + return this; } public OracleHint getOracleHint() { @@ -265,7 +285,7 @@ public String toString() { b.append(oracleHint).append(" "); } b.append("INTO "); - b.append(table); + tableReference.appendWithAliasTo(b); b.append(" USING "); b.append(fromItem); b.append(" ON "); diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeDelete.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeDelete.java index 16bfc495dd..11a19b412b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeDelete.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeDelete.java @@ -34,6 +34,11 @@ public MergeDelete setSide(MergeSide side) { return this; } + /** {@code WHEN MATCHED}, or {@code WHEN NOT MATCHED BY SOURCE} with that side. */ + public MergeWhen getWhen() { + return MergeWhen.ofTargetRow(side); + } + public MergeDelete withAndPredicate(Expression andPredicate) { this.setAndPredicate(andPredicate); return this; @@ -52,11 +57,7 @@ public T accept(MergeOperationVisitor mergeOperationVisitor, S context @Override public String toString() { StringBuilder b = new StringBuilder(); - b.append(side == MergeSide.SOURCE ? " WHEN NOT MATCHED BY SOURCE" : " WHEN MATCHED"); - if (andPredicate != null) { - b.append(" AND ").append(andPredicate.toString()); - } - b.append(" THEN DELETE"); + getWhen().appendTo(b, andPredicate, b::append).append("DELETE"); return b.toString(); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeDoNothing.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeDoNothing.java new file mode 100644 index 0000000000..f6e09f7563 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeDoNothing.java @@ -0,0 +1,65 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2024 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.merge; + +import net.sf.jsqlparser.expression.Expression; + +import java.io.Serializable; +import java.util.Objects; + +/** PostgreSQL's {@code WHEN [NOT] MATCHED [BY SOURCE | BY TARGET] [AND ...] THEN DO NOTHING}. */ +public class MergeDoNothing implements Serializable, MergeOperation { + private MergeWhen when = MergeWhen.MATCHED; + private Expression andPredicate; + + public MergeDoNothing() {} + + public MergeDoNothing(MergeWhen when) { + setWhen(when); + } + + public MergeWhen getWhen() { + return when; + } + + public void setWhen(MergeWhen when) { + this.when = Objects.requireNonNull(when); + } + + public MergeDoNothing withWhen(MergeWhen when) { + this.setWhen(when); + return this; + } + + public Expression getAndPredicate() { + return andPredicate; + } + + public void setAndPredicate(Expression andPredicate) { + this.andPredicate = andPredicate; + } + + public MergeDoNothing withAndPredicate(Expression andPredicate) { + this.setAndPredicate(andPredicate); + return this; + } + + @Override + public T accept(MergeOperationVisitor mergeOperationVisitor, S context) { + return mergeOperationVisitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + when.appendTo(b, andPredicate, b::append).append("DO NOTHING"); + return b.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeInsert.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeInsert.java index 411e5e2bdf..60fe56fbb7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeInsert.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeInsert.java @@ -12,11 +12,14 @@ import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.insert.Insert; import java.io.Serializable; import java.util.Arrays; import java.util.Collection; +import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; public class MergeInsert implements Serializable, MergeOperation { @@ -25,6 +28,7 @@ public class MergeInsert implements Serializable, MergeOperation { private ExpressionList values; private Expression whereCondition; private MergeSide side; + private Insert.OverridingMode overridingMode = Insert.OverridingMode.NONE; public MergeSide getSide() { return side; @@ -66,6 +70,52 @@ public void setWhereCondition(Expression whereCondition) { this.whereCondition = whereCondition; } + /** {@code WHEN NOT MATCHED}, or {@code WHEN NOT MATCHED BY TARGET} with that side. */ + public MergeWhen getWhen() { + return MergeWhen.ofSourceRow(side); + } + + /** The mode of {@code OVERRIDING {SYSTEM | USER} VALUE}, {@code NONE} without the clause. */ + public Insert.OverridingMode getOverridingMode() { + return overridingMode; + } + + public void setOverridingMode(Insert.OverridingMode overridingMode) { + this.overridingMode = Objects.requireNonNull(overridingMode); + } + + public MergeInsert withOverridingMode(Insert.OverridingMode overridingMode) { + this.setOverridingMode(overridingMode); + return this; + } + + /** Whether the clause is {@code INSERT DEFAULT VALUES}: an insert without a values list. */ + public boolean isDefaultValues() { + return values == null; + } + + /** + * Appends {@code INSERT DEFAULT VALUES} or {@code INSERT [(columns)] [OVERRIDING ... VALUE] + * VALUES (...)}, letting the renderer write the column and value lists. + */ + public StringBuilder appendInsertTo(StringBuilder builder, + Consumer expressionRenderer) { + builder.append("INSERT "); + if (isDefaultValues()) { + return builder.append("DEFAULT VALUES"); + } + if (columns != null) { + expressionRenderer.accept(columns); + } + if (overridingMode != Insert.OverridingMode.NONE) { + builder.append(columns != null ? " " : "").append("OVERRIDING ") + .append(overridingMode).append(" VALUE"); + } + builder.append(" VALUES "); + expressionRenderer.accept(values); + return builder; + } + @Override public T accept(MergeOperationVisitor mergeOperationVisitor, S context) { return mergeOperationVisitor.visit(this, context); @@ -74,18 +124,8 @@ public T accept(MergeOperationVisitor mergeOperationVisitor, S context @Override public String toString() { StringBuilder b = new StringBuilder(); - b.append(" WHEN NOT MATCHED"); - if (side == MergeSide.TARGET) { - b.append(" BY TARGET"); - } - if (andPredicate != null) { - b.append(" AND ").append(andPredicate); - } - b.append(" THEN INSERT "); - if (columns != null) { - b.append(columns); - } - b.append(" VALUES ").append(values.toString()); + getWhen().appendTo(b, andPredicate, b::append); + appendInsertTo(b, b::append); if (whereCondition != null) { b.append(" WHERE ").append(whereCondition); } diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperation.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperation.java index 71447d23d6..0bc47fe5de 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperation.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperation.java @@ -10,7 +10,8 @@ package net.sf.jsqlparser.statement.merge; /** - * Marker interface to cover {@link MergeDelete}, {@link MergeUpdate} and {@link MergeInsert} + * Marker interface to cover {@link MergeDelete}, {@link MergeUpdate}, {@link MergeInsert} and + * {@link MergeDoNothing} */ public interface MergeOperation { T accept(MergeOperationVisitor mergeOperationVisitor, S context); diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperationVisitor.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperationVisitor.java index d2439fbd8f..b686f5d3cf 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperationVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperationVisitor.java @@ -19,6 +19,11 @@ public interface MergeOperationVisitor { T visit(MergeInsert mergeInsert, S context); + /** PostgreSQL's DO NOTHING action; existing visitors skip it. */ + default T visit(MergeDoNothing mergeDoNothing, S context) { + return null; + } + default T visit(Collection mergeOperations, S context) { if (mergeOperations != null) { mergeOperations.forEach(operation -> operation.accept(this, context)); diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperationVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperationVisitorAdapter.java index 61ff7a45ab..f15d1054cb 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperationVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeOperationVisitorAdapter.java @@ -52,4 +52,10 @@ public T visit(MergeInsert mergeInsert, S context) { expressionVisitor.visitExpression(mergeInsert.getWhereCondition(), context); return null; } + + @Override + public T visit(MergeDoNothing mergeDoNothing, S context) { + expressionVisitor.visitExpression(mergeDoNothing.getAndPredicate(), context); + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java index 6dc7815fb7..c56090eaf3 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeSide.java @@ -32,7 +32,7 @@ public enum MergeSide { /** * Validates the clause pairing of a {@code WHEN NOT MATCHED} branch: {@code BY TARGET} (the * default) only allows an {@code INSERT} clause, {@code BY SOURCE} only allows {@code UPDATE} - * or {@code DELETE}. + * or {@code DELETE}. PostgreSQL's {@code DO NOTHING} goes with either side. * * @param side the parsed {@code BY TARGET}/{@code BY SOURCE} qualifier, null when absent * @param operation the parsed clause @@ -40,6 +40,9 @@ public enum MergeSide { * @throws IllegalArgumentException when the pairing is not legal in any dialect */ public static MergeOperation validatePairing(MergeSide side, MergeOperation operation) { + if (operation instanceof MergeDoNothing) { + return operation; + } if (side == MergeSide.SOURCE) { if (operation instanceof MergeInsert) { throw new IllegalArgumentException( diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeUpdate.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeUpdate.java index e64cab5133..3a9d226e97 100644 --- a/src/main/java/net/sf/jsqlparser/statement/merge/MergeUpdate.java +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeUpdate.java @@ -71,6 +71,11 @@ public MergeUpdate setSide(MergeSide side) { return this; } + /** {@code WHEN MATCHED}, or {@code WHEN NOT MATCHED BY SOURCE} with that side. */ + public MergeWhen getWhen() { + return MergeWhen.ofTargetRow(side); + } + @Override public T accept(MergeOperationVisitor mergeOperationVisitor, S context) { return mergeOperationVisitor.visit(this, context); @@ -79,11 +84,7 @@ public T accept(MergeOperationVisitor mergeOperationVisitor, S context @Override public String toString() { StringBuilder b = new StringBuilder(); - b.append(side == MergeSide.SOURCE ? " WHEN NOT MATCHED BY SOURCE" : " WHEN MATCHED"); - if (andPredicate != null) { - b.append(" AND ").append(andPredicate.toString()); - } - b.append(" THEN UPDATE SET "); + getWhen().appendTo(b, andPredicate, b::append).append("UPDATE SET "); UpdateSet.appendUpdateSetsTo(b, updateSets); if (whereCondition != null) { diff --git a/src/main/java/net/sf/jsqlparser/statement/merge/MergeWhen.java b/src/main/java/net/sf/jsqlparser/statement/merge/MergeWhen.java new file mode 100644 index 0000000000..bcebd36512 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/merge/MergeWhen.java @@ -0,0 +1,72 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2024 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.merge; + +import net.sf.jsqlparser.expression.Expression; + +import java.util.function.Consumer; + +/** + * The match condition that starts a {@code WHEN} clause of {@code MERGE}. The standard knows + * {@code WHEN [NOT] MATCHED}; PostgreSQL, SQL Server and BigQuery also qualify {@code NOT MATCHED} + * with {@code BY SOURCE} or {@code BY TARGET}. + */ +public enum MergeWhen { + MATCHED("WHEN MATCHED", null), NOT_MATCHED("WHEN NOT MATCHED", null), NOT_MATCHED_BY_SOURCE( + "WHEN NOT MATCHED BY SOURCE", MergeSide.SOURCE), NOT_MATCHED_BY_TARGET( + "WHEN NOT MATCHED BY TARGET", MergeSide.TARGET); + + private final String text; + private final MergeSide side; + + MergeWhen(String text, MergeSide side) { + this.text = text; + this.side = side; + } + + /** The condition of a clause that acts on rows of the target: an UPDATE or a DELETE. */ + public static MergeWhen ofTargetRow(MergeSide side) { + return side == MergeSide.SOURCE ? NOT_MATCHED_BY_SOURCE : MATCHED; + } + + /** The condition of a clause that acts on rows of the source: an INSERT. */ + public static MergeWhen ofSourceRow(MergeSide side) { + return side == MergeSide.TARGET ? NOT_MATCHED_BY_TARGET : NOT_MATCHED; + } + + /** The condition of {@code WHEN NOT MATCHED [BY side]}. */ + public static MergeWhen ofNotMatched(MergeSide side) { + return side == MergeSide.SOURCE ? NOT_MATCHED_BY_SOURCE : ofSourceRow(side); + } + + /** The {@code BY SOURCE} or {@code BY TARGET} qualifier, null when the clause has none. */ + public MergeSide getSide() { + return side; + } + + /** + * Appends {@code WHEN ... [AND predicate] THEN } with a leading and a trailing space, letting + * the renderer write the predicate. + */ + public StringBuilder appendTo(StringBuilder builder, Expression andPredicate, + Consumer expressionRenderer) { + builder.append(' ').append(text); + if (andPredicate != null) { + builder.append(" AND "); + expressionRenderer.accept(andPredicate); + } + return builder.append(" THEN "); + } + + @Override + public String toString() { + return text; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/update/Update.java b/src/main/java/net/sf/jsqlparser/statement/update/Update.java index defe9e4422..3bb4146a09 100644 --- a/src/main/java/net/sf/jsqlparser/statement/update/Update.java +++ b/src/main/java/net/sf/jsqlparser/statement/update/Update.java @@ -15,6 +15,7 @@ import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.schema.TableReference; import net.sf.jsqlparser.statement.ForPortionClause; import net.sf.jsqlparser.statement.OutputClause; import net.sf.jsqlparser.statement.ReturningClause; @@ -35,15 +36,17 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Optional; @SuppressWarnings({"PMD.CyclomaticComplexity"}) public class Update extends ASTNodeAccessImpl implements Statement { private List> withItemsList; - private Table table; + private TableReference tableReference = new TableReference(null); private ForPortionClause forPortionClause; private Expression where; + private String cursorName; private PreferringClause preferringClause; private List updateSets; private FromItem fromItem; @@ -132,11 +135,46 @@ public Update withForPortionClause(ForPortionClause forPortionClause) { } public Table getTable() { - return table; + return tableReference.getTable(); } + /** Replaces the target table and keeps the inheritance scope of the target. */ public void setTable(Table table) { - this.table = table; + tableReference.setTable(table); + } + + /** + * The target as PostgreSQL's relation_expr_opt_alias: the table of {@link #getTable()} with its + * alias, and whether ONLY or * describes its inheritance scope. + */ + public TableReference getTableReference() { + return tableReference; + } + + public void setTableReference(TableReference tableReference) { + this.tableReference = Objects.requireNonNull(tableReference); + } + + public Update withTableReference(TableReference tableReference) { + this.setTableReference(tableReference); + return this; + } + + /** + * The cursor of {@code WHERE CURRENT OF cursor}, which is written in place of the condition of + * {@link #getWhere()}. + */ + public String getCursorName() { + return cursorName; + } + + public void setCursorName(String cursorName) { + this.cursorName = cursorName; + } + + public Update withCursorName(String cursorName) { + this.setCursorName(cursorName); + return this; } public Expression getWhere() { @@ -224,6 +262,7 @@ public Update withFromBeforeSet(boolean fromBeforeSet) { /** Whether the Teradata target names an alias declared in this UPDATE's FROM clause. */ public boolean isTargetTableAlias() { + Table table = getTable(); if (!fromBeforeSet || table == null) { return false; } @@ -236,7 +275,7 @@ public boolean isTargetTableAlias() { private boolean matchesTargetAlias(FromItem source) { return source != null && source.getAlias() != null - && table.getFullyQualifiedName().equalsIgnoreCase(source.getAlias().getName()); + && getTable().getFullyQualifiedName().equalsIgnoreCase(source.getAlias().getName()); } /** Shared FROM rendering for both supported clause positions. */ @@ -399,7 +438,7 @@ public String toString() { if (modifierIgnore) { b.append("IGNORE "); } - b.append(table); + tableReference.appendWithAliasTo(b); if (forPortionClause != null) { b.append(' ').append(forPortionClause); } @@ -427,7 +466,9 @@ public String toString() { appendFromTo(b); } - if (where != null) { + if (cursorName != null) { + b.append(" WHERE CURRENT OF ").append(cursorName); + } else if (where != null) { b.append(" WHERE "); b.append(where); } diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index f65ddf14f6..dc6fccea5f 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -207,6 +207,7 @@ import net.sf.jsqlparser.statement.maintenance.VacuumStatement; import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.merge.MergeDelete; +import net.sf.jsqlparser.statement.merge.MergeDoNothing; import net.sf.jsqlparser.statement.merge.MergeInsert; import net.sf.jsqlparser.statement.merge.MergeOperation; import net.sf.jsqlparser.statement.merge.MergeOperationVisitor; @@ -2151,6 +2152,14 @@ public Void visit(MergeDelete mergeDelete, S context) { return null; } + @Override + public Void visit(MergeDoNothing mergeDoNothing, S context) { + if (mergeDoNothing.getAndPredicate() != null) { + mergeDoNothing.getAndPredicate().accept(this, context); + } + return null; + } + @Override public Void visit(MergeInsert mergeInsert, S context) { if (mergeInsert.getAndPredicate() != null) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java index 398f44cac6..5ffa172fff 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/DeleteDeParser.java @@ -72,7 +72,7 @@ public void deParse(Delete delete) { if (delete.isHasFrom()) { builder.append(" FROM"); } - builder.append(" ").append(delete.getTable().toString()); + delete.getTableReference().appendWithAliasTo(builder.append(" ")); deparseForPortionClause(delete.getForPortionClause(), expressionVisitor); // the target's joins come before USING, as in the grammar @@ -100,7 +100,9 @@ public void deParse(Delete delete) { } protected void deparseWhereClause(Delete delete) { - if (delete.getWhere() != null) { + if (delete.getCursorName() != null) { + builder.append(" WHERE CURRENT OF ").append(delete.getCursorName()); + } else if (delete.getWhere() != null) { builder.append(" WHERE "); delete.getWhere().accept(expressionVisitor, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java index fba2c17754..e281f40af7 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/MergeDeParser.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.statement.merge.*; import net.sf.jsqlparser.statement.select.WithItem; @@ -47,7 +48,11 @@ public void deParse(Merge merge) { builder.append(merge.getOracleHint()).append(" "); } builder.append("INTO "); - merge.getTable().accept(selectDeParser, null); + if (merge.getTableReference().hasInheritanceScope()) { + merge.getTableReference().appendWithAliasTo(builder); + } else { + merge.getTable().accept(selectDeParser, null); + } builder.append(" USING "); merge.getFromItem().accept(selectDeParser, null); @@ -79,13 +84,8 @@ public void deParse(Merge merge) { @Override public StringBuilder visit(MergeDelete mergeDelete, S context) { - builder.append(mergeDelete.getSide() == MergeSide.SOURCE ? " WHEN NOT MATCHED BY SOURCE" - : " WHEN MATCHED"); - if (mergeDelete.getAndPredicate() != null) { - builder.append(" AND "); - mergeDelete.getAndPredicate().accept(expressionDeParser, context); - } - builder.append(" THEN DELETE"); + deparseWhen(mergeDelete.getWhen(), mergeDelete.getAndPredicate(), context) + .append("DELETE"); return builder; } @@ -95,13 +95,8 @@ public void visit(MergeDelete mergeDelete) { @Override public StringBuilder visit(MergeUpdate mergeUpdate, S context) { - builder.append(mergeUpdate.getSide() == MergeSide.SOURCE ? " WHEN NOT MATCHED BY SOURCE" - : " WHEN MATCHED"); - if (mergeUpdate.getAndPredicate() != null) { - builder.append(" AND "); - mergeUpdate.getAndPredicate().accept(expressionDeParser, context); - } - builder.append(" THEN UPDATE SET "); + deparseWhen(mergeUpdate.getWhen(), mergeUpdate.getAndPredicate(), context) + .append("UPDATE SET "); deparseUpdateSets(mergeUpdate.getUpdateSets(), builder, expressionDeParser); if (mergeUpdate.getWhereCondition() != null) { @@ -123,20 +118,9 @@ public void visit(MergeUpdate mergeUpdate) { @Override public StringBuilder visit(MergeInsert mergeInsert, S context) { - builder.append(" WHEN NOT MATCHED"); - if (mergeInsert.getSide() == MergeSide.TARGET) { - builder.append(" BY TARGET"); - } - if (mergeInsert.getAndPredicate() != null) { - builder.append(" AND "); - mergeInsert.getAndPredicate().accept(expressionDeParser, context); - } - builder.append(" THEN INSERT "); - if (mergeInsert.getColumns() != null) { - mergeInsert.getColumns().accept(expressionDeParser, context); - } - builder.append(" VALUES "); - mergeInsert.getValues().accept(expressionDeParser, context); + deparseWhen(mergeInsert.getWhen(), mergeInsert.getAndPredicate(), context); + mergeInsert.appendInsertTo(builder, + expression -> expression.accept(expressionDeParser, context)); if (mergeInsert.getWhereCondition() != null) { builder.append(" WHERE "); @@ -150,6 +134,17 @@ public void visit(MergeInsert mergeInsert) { visit(mergeInsert, null); } + @Override + public StringBuilder visit(MergeDoNothing mergeDoNothing, S context) { + return deparseWhen(mergeDoNothing.getWhen(), mergeDoNothing.getAndPredicate(), context) + .append("DO NOTHING"); + } + + private StringBuilder deparseWhen(MergeWhen when, Expression andPredicate, S context) { + return when.appendTo(builder, andPredicate, + predicate -> predicate.accept(expressionDeParser, context)); + } + public ExpressionDeParser getExpressionDeParser() { return expressionDeParser; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java index a817034ae5..96e2f99563 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/UpdateDeParser.java @@ -58,7 +58,7 @@ public void deParse(Update update) { if (update.isModifierIgnore()) { builder.append("IGNORE "); } - builder.append(update.getTable()); + update.getTableReference().appendWithAliasTo(builder); deparseForPortionClause(update.getForPortionClause(), expressionVisitor); if (update.getStartJoins() != null) { sources.deparseJoins(update.getStartJoins()); @@ -98,7 +98,9 @@ public void deParse(Update update) { } protected void deparseWhereClause(Update update) { - if (update.getWhere() != null) { + if (update.getCursorName() != null) { + builder.append(" WHERE CURRENT OF ").append(update.getCursorName()); + } else if (update.getWhere() != null) { builder.append(" WHERE "); update.getWhere().accept(expressionVisitor, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java index ab5dfa9b1d..d0664e9872 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/MergeValidator.java @@ -80,4 +80,10 @@ public Void visit(MergeInsert mergeInsert, S context) { public void visit(MergeInsert mergeInsert) { visit(mergeInsert, null); } + + @Override + public Void visit(MergeDoNothing mergeDoNothing, S context) { + validateOptionalExpression(mergeDoNothing.getAndPredicate()); + return null; + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e4fcc2e76e..f4487696ab 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2009,6 +2009,40 @@ public class CCJSqlParser extends AbstractJSqlParser { return RelationOptions.isOnlyAhead(prev, getToken(2).kind); } + /** + * Whether the target of UPDATE, DELETE or MERGE carries PostgreSQL's inheritance scope: ONLY + * before the table name or * after it. MySQL and MariaDB have no ONLY there. + */ + private boolean isScopedDmlTargetAhead() { + if (getToken(1).kind == K_ONLY) { + return !isMySqlDialect() && isRelationOnlyAhead(getToken(0)); + } + int i = 2; + while (".".equals(getToken(i).image)) { + i += 2; + } + return "*".equals(getToken(i).image) && !"*".equals(getToken(i - 1).image); + } + + /** + * The targets of (columns) = source, when the source is an expression other than a + * parenthesized list or sub-select: one or more columns, array elements or fields. + */ + private static void requireUpdateSetTargets(ExpressionList targets) throws ParseException { + boolean valid = !targets.isEmpty(); + for (Object target : targets) { + valid &= target instanceof Column || target instanceof ArrayExpression + || target instanceof RowGetExpression; + } + requireSyntax(valid, "A SET column list names one or more columns"); + } + + /** WHERE CURRENT OF cursor, the positioned form of UPDATE and DELETE. */ + private boolean isWhereCurrentOfAhead() { + return getToken(1).kind == K_WHERE && getToken(2).kind == K_CURRENT + && getToken(3).kind == K_OF; + } + /** * DuckDB's FROM-first query: a FROM clause followed by query clauses the pipe syntax cannot * consume. Scanning stops at a pipe operator or the end of the statement, and clause keywords @@ -8535,12 +8569,14 @@ Update UpdateWithWithItems( List> withItems ): Update Update(): { Update update = new Update(); - Table table = null; + TableReference target; + List indexHints; ForPortionClause forPortionClause; List startJoins = null; List> with = null; List updateSets; Expression where = null; + String cursorName; PreferringClause preferringClause = null; Limit limit = null; OptionClause optionClause = null; @@ -8561,7 +8597,8 @@ Update Update(): { update.setConflictAlgorithm(updateOrAlgorithm(algorithm)); } ] [ LOOKAHEAD(2) { modifierPriority = UpdateModifierPriority.LOW_PRIORITY; }] [ LOOKAHEAD(2) { modifierIgnore = true; }] - table=TableWithAliasAndMysqlIndexHint() + target=DmlTarget() { update.setTableReference(target); } + [ LOOKAHEAD(2) indexHints=MySQLIndexHints() { target.getTable().setIndexHints(indexHints); } ] [ forPortionClause=ForPortionClause() { update.setForPortionClause(forPortionClause); } ] [ startJoins=JoinsList() ] [ LOOKAHEAD(, { Dialect.TERADATA.name().equals(getAsString(Feature.dialect)) }) @@ -8573,7 +8610,12 @@ Update Update(): [ LOOKAHEAD(2, { !Dialect.TERADATA.name().equals(getAsString(Feature.dialect)) }) UpdateFromClause(update) ] - [ where=WhereClause() { update.setWhere(where); } ] + [ + LOOKAHEAD({ isWhereCurrentOfAhead() }) + cursorName=WhereCurrentOfClause() { update.setCursorName(cursorName); } + | + where=WhereClause() { update.setWhere(where); } + ] [ preferringClause=PreferringClause() { update.setPreferringClause(preferringClause); } ] [ orderByElements = OrderByElements() { update.setOrderByElements(orderByElements); } ] @@ -8583,7 +8625,6 @@ Update Update(): { return update.withWithItemsList(with) - .withTable(table) .withStartJoins(startJoins) .withModifierPriority(modifierPriority) .withModifierIgnore(modifierIgnore); @@ -8623,64 +8664,89 @@ List UpdateSets(): { ArrayList updateSets = new ArrayList(); UpdateSet updateSet; +} +{ + updateSet=UpdateSet() { updateSets.add(updateSet); } + ( LOOKAHEAD(2) "," updateSet=UpdateSet() { updateSets.add(updateSet); } )* + { + return updateSets; + } +} + +/** + * One assignment of a SET list: column = value, or (columns) = source. PostgreSQL's set_clause + * takes any expression as the source of a column list, such as ROW(...) or a sub-select. + */ +UpdateSet UpdateSet(): +{ + UpdateSet updateSet; Column tableColumn; Expression valueExpression; - ExpressionList columns; - ExpressionListvalues; + ExpressionList values; } { ( - ( - tableColumn=Column() "=" valueExpression=Expression() - { - updateSet = new UpdateSet(tableColumn, valueExpression); - linkElements(updateSet.getColumns()); - linkElements(updateSet.getValues()); - updateSets.add(updateSet); - } - ) + tableColumn=Column() "=" valueExpression=Expression() + { + updateSet = new UpdateSet(tableColumn, valueExpression); + linkElements(updateSet.getColumns()); + linkElements(updateSet.getValues()); + } | ( - { updateSet = new UpdateSet(); updateSets.add(updateSet); } + { updateSet = new UpdateSet(); } columns = ParenthesedExpressionList() { updateSet.setColumns(columns); } "=" ( LOOKAHEAD(3) valueExpression = ParenthesedSelect() { updateSet.setValues(linkElements(new ExpressionList(valueExpression))); } | - values = ParenthesedExpressionList() { updateSet.setValues(values); } + LOOKAHEAD("(") values = ParenthesedExpressionList() { updateSet.setValues(values); } + | + valueExpression = Expression() + { + requireUpdateSetTargets(columns); + updateSet.setValues(linkElements(new ExpressionList(valueExpression))); + } ) ) ) - - ( - LOOKAHEAD(2) "," ( - tableColumn=Column() "=" valueExpression=Expression() - { - updateSet = new UpdateSet(tableColumn, valueExpression); - linkElements(updateSet.getColumns()); - linkElements(updateSet.getValues()); - updateSets.add(updateSet); - } - | - ( - { updateSet = new UpdateSet(); updateSets.add(updateSet); } - columns = ParenthesedExpressionList() { updateSet.setColumns(columns); } - "=" - ( - LOOKAHEAD(3) valueExpression = ParenthesedSelect() { updateSet.setValues(linkElements(new ExpressionList(valueExpression))); } - | - values = ParenthesedExpressionList() { updateSet.setValues(values); } - ) - ) - ) - )* - { - return updateSets; + return updateSet; } } +/** WHERE CURRENT OF cursor_name; returns the cursor name. */ +String WhereCurrentOfClause(): +{ + String cursorName; +} +{ + cursorName=RelObjectName() + { return cursorName; } +} + +/** + * The target of UPDATE, DELETE and MERGE: PostgreSQL's relation_expr_opt_alias with ONLY or * + * around the table name, or a table with its alias and hints. + */ +TableReference DmlTarget(): +{ + TableReference target; + Table table; + Alias alias; +} +{ + ( + LOOKAHEAD({ isScopedDmlTargetAhead() }) + target=RelationTarget() + [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { target.getTable().setAlias(alias); } ] + | + table=TableWithAlias() { target = new TableReference(table); } + ) + { return target; } +} + List Partitions(): { List partitions = new ArrayList(); @@ -9250,6 +9316,8 @@ Delete Delete(): { Delete delete = new Delete(); Table table = null; + TableReference target; + String cursorName; ForPortionClause forPortionClause; List
tables = new ArrayList
(); List> with = null; @@ -9281,7 +9349,7 @@ Delete Delete(): [ outputClause = OutputClause() {delete.setOutputClause(outputClause); } ] | ) { hasFrom = true; }] - [ LOOKAHEAD(3) table=TableWithAlias() + [ LOOKAHEAD(3) target=DmlTarget() { delete.setTableReference(target); table = target.getTable(); } [ forPortionClause=ForPortionClause() { delete.setForPortionClause(forPortionClause); } ] [ LOOKAHEAD(2) joins=JoinsList() ] ] [ usingFromItem=FromItem() { usingFromItemList.add(usingFromItem); } @@ -9289,7 +9357,12 @@ Delete Delete(): // MySQL's table references and PostgreSQL's from items may join further tables [ LOOKAHEAD(2) usingJoins=JoinsList() { delete.setUsingJoins(usingJoins); } ] ] - [where=WhereClause() { delete.setWhere(where); } ] + [ + LOOKAHEAD({ isWhereCurrentOfAhead() }) + cursorName=WhereCurrentOfClause() { delete.setCursorName(cursorName); } + | + where=WhereClause() { delete.setWhere(where); } + ] [preferringClause=PreferringClause() { delete.setPreferringClause(preferringClause);} ] [orderByElements = OrderByElements() { delete.setOrderByElements(orderByElements); } ] [limit=PlainLimit() {delete.setLimit(limit); } ] @@ -9315,7 +9388,7 @@ Delete Delete(): Statement Merge( List> with ) : { Merge merge = new Merge(); - Table table; + TableReference target; FromItem fromItem; Expression condition; List operations; @@ -9324,7 +9397,7 @@ Statement Merge( List> with ) : { OptionClause optionClause = null; } { - { merge.setOracleHint(getOracleHint()); } table=TableWithAlias() { merge.setTable(table); } + { merge.setOracleHint(getOracleHint()); } target=DmlTarget() { merge.setTableReference(target); } fromItem = FromItem() { merge.setFromItem(fromItem); } condition = Expression() { merge.setOnCondition(condition); } @@ -9362,6 +9435,9 @@ MergeOperation MergeWhenMatched() : { ( operation = MergeDeleteClause(predicate) | operation = MergeUpdateClause(predicate) + | + // PostgreSQL + { operation = new MergeDoNothing().withAndPredicate(predicate); } ) { return operation; } } @@ -9394,21 +9470,29 @@ MergeInsert MergeInsertClause(Expression predicate) : { } { - [ open="(" columns = ColumnList() ")" + ( + // PostgreSQL's merge_insert: without a column list or OVERRIDING + + | + [ open="(" columns = ColumnList() ")" + { + ParenthesedExpressionList insertColumns = new ParenthesedExpressionList(columns); + linkAST(insertColumns, open); + mi.setColumns(insertColumns); + } + ] + [ + ( { mi.setOverridingMode(Insert.OverridingMode.SYSTEM); } + | { mi.setOverridingMode(Insert.OverridingMode.USER); } ) ] + open="(" expList = SimpleExpressionList() ")" { - ParenthesedExpressionList insertColumns = new ParenthesedExpressionList(columns); - linkAST(insertColumns, open); - mi.setColumns(insertColumns); + ParenthesedExpressionList insertValues = new ParenthesedExpressionList(expList); + linkAST(insertValues, open); + mi.setValues(insertValues); } - ] - open="(" expList = SimpleExpressionList() ")" - { - ParenthesedExpressionList insertValues = new ParenthesedExpressionList(expList); - linkAST(insertValues, open); - mi.setValues(insertValues); - } - [ condition = Expression() { mi.setWhereCondition(condition); } ] + [ condition = Expression() { mi.setWhereCondition(condition); } ] + ) { mi.setAndPredicate(predicate); @@ -9435,6 +9519,10 @@ MergeOperation MergeWhenNotMatched() : { operation = MergeUpdateClause(predicate) { ((MergeUpdate) operation).setSide(side); } | operation = MergeDeleteClause(predicate) { ((MergeDelete) operation).setSide(side); } + | + // PostgreSQL: allowed for the source and the target side alike + + { operation = new MergeDoNothing(MergeWhen.ofNotMatched(side)).withAndPredicate(predicate); } ) { return MergeSide.validatePairing(side, operation); } } @@ -9648,17 +9736,6 @@ Table TableWithAlias(): { return table; } } -Table TableWithAliasAndMysqlIndexHint(): -{ - Table table = null; - List indexHints = null; -} -{ - table=TableWithAlias() - [ LOOKAHEAD(2) indexHints=MySQLIndexHints() { table.setIndexHints(indexHints); } ] - { return table; } -} - Number Number(): { Token token; diff --git a/src/test/java/net/sf/jsqlparser/statement/PostgreSQLModifyFormsTest.java b/src/test/java/net/sf/jsqlparser/statement/PostgreSQLModifyFormsTest.java new file mode 100644 index 0000000000..70bd156a93 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/PostgreSQLModifyFormsTest.java @@ -0,0 +1,672 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2024 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.schema.TableReference; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.merge.Merge; +import net.sf.jsqlparser.statement.merge.MergeDelete; +import net.sf.jsqlparser.statement.merge.MergeDoNothing; +import net.sf.jsqlparser.statement.merge.MergeInsert; +import net.sf.jsqlparser.statement.merge.MergeOperation; +import net.sf.jsqlparser.statement.merge.MergeOperationVisitor; +import net.sf.jsqlparser.statement.merge.MergeOperationVisitorAdapter; +import net.sf.jsqlparser.statement.merge.MergeSide; +import net.sf.jsqlparser.statement.merge.MergeUpdate; +import net.sf.jsqlparser.statement.merge.MergeWhen; +import net.sf.jsqlparser.statement.select.ParenthesedSelect; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.statement.update.UpdateSet; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * PostgreSQL's relation_expr_opt_alias targets, WHERE CURRENT OF, set_clause sources and + * merge_when_clause actions of UPDATE, DELETE and MERGE. + */ +class PostgreSQLModifyFormsTest { + + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + private static final Consumer MYSQL = parser -> parser.withDialect(Dialect.MYSQL); + private static final Consumer MARIADB = + parser -> parser.withDialect(Dialect.MARIADB); + private static final Consumer ORACLE = + parser -> parser.withDialect(Dialect.ORACLE); + + private static final String MERGE = "MERGE INTO t USING s ON t.id = s.id "; + + /** Parses exactly with PostgreSQL and without a dialect; both render the statement again. */ + private static Statement parse(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL); + assertEquals(sql, statement.toString()); + assertEquals(sql, deparse(statement)); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), POSTGRESQL).toString()); + assertEquals(sql, assertSqlCanBeParsedAndDeparsed(sql, false).toString()); + return statement; + } + + private static String deparse(Statement statement) { + StringBuilder builder = new StringBuilder(); + statement.accept(new StatementDeParser(builder)); + return builder.toString(); + } + + private static void assertRejected(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, POSTGRESQL), sql); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql), sql); + } + + private static void assertTarget(TableReference target, String name, boolean only, + boolean descendants, boolean parenthesized, String alias) { + assertEquals(name, target.getTable().getFullyQualifiedName()); + assertEquals(only, target.isOnly()); + assertEquals(descendants, target.isIncludeDescendants()); + assertEquals(parenthesized, target.isParenthesized()); + assertEquals(only || descendants, target.hasInheritanceScope()); + Alias tableAlias = target.getTable().getAlias(); + assertEquals(alias, tableAlias == null ? null : tableAlias.getName()); + assertNotNull(target.getTable().getASTNode()); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE ONLY t SET a = 1", + "UPDATE ONLY t AS x SET a = 1", + "UPDATE ONLY t x SET a = 1", + "UPDATE t * SET a = 1", + "UPDATE t * AS x SET a = 1", + "UPDATE t * x SET a = 1", + "UPDATE ONLY (t) SET a = 1", + "UPDATE ONLY (t) AS x SET a = 1", + "UPDATE ONLY (sc.t) x SET a = 1", + "UPDATE ONLY sc.t AS x SET a = 1 FROM u WHERE x.a = u.a RETURNING *", + "UPDATE sc.t * AS x SET a = 1 FROM u WHERE x.a = u.a RETURNING x.a", + "WITH w AS (SELECT 1 AS a) UPDATE ONLY t AS x SET a = w.a FROM w", + "DELETE FROM ONLY t", + "DELETE FROM ONLY t AS x WHERE x.a = 1", + "DELETE FROM ONLY t x WHERE x.a = 1", + "DELETE FROM t * WHERE a = 1", + "DELETE FROM t * AS x WHERE x.a = 1", + "DELETE FROM t * x WHERE x.a = 1", + "DELETE FROM t *", + "DELETE FROM ONLY (t)", + "DELETE FROM ONLY (t) AS x WHERE x.a = 1", + "DELETE FROM ONLY t USING u WHERE t.a = u.a", + "DELETE FROM ONLY t AS x USING u WHERE x.a = u.a RETURNING x.a", + "DELETE FROM sc.t * AS x USING u, s WHERE x.a = u.a RETURNING *", + "WITH w AS (SELECT 1 AS a) DELETE FROM ONLY t AS x USING w WHERE x.a = w.a", + "MERGE INTO ONLY t USING s ON t.id = s.id WHEN MATCHED THEN DELETE", + "MERGE INTO ONLY t AS x USING s ON x.id = s.id WHEN MATCHED THEN DELETE", + "MERGE INTO ONLY (t) x USING s ON x.id = s.id WHEN MATCHED THEN DELETE", + "MERGE INTO t * USING s ON t.id = s.id WHEN MATCHED THEN DELETE", + "MERGE INTO t * x USING s ON x.id = s.id WHEN MATCHED THEN UPDATE SET (a, b) = ROW(1, 2)", + "MERGE INTO t * AS x USING s ON x.id = s.id WHEN MATCHED THEN DO NOTHING" + }) + void inheritanceScopeOfTheTarget(String sql) throws JSQLParserException { + parse(sql); + } + + @Test + void updateTargetModel() throws JSQLParserException { + Update update = (Update) parse("UPDATE ONLY t AS x SET a = 1"); + assertTarget(update.getTableReference(), "t", true, false, false, "x"); + assertTrue(update.getTable().getAlias().isUseAs()); + assertEquals(update.getTableReference().getTable(), update.getTable()); + + update = (Update) parse("UPDATE ONLY (sc.t) x SET a = 1"); + assertTarget(update.getTableReference(), "sc.t", true, false, true, "x"); + assertFalse(update.getTable().getAlias().isUseAs()); + + update = (Update) parse("UPDATE t * SET a = 1"); + assertTarget(update.getTableReference(), "t", false, true, false, null); + + update = (Update) parse("UPDATE t AS x SET a = 1"); + assertTarget(update.getTableReference(), "t", false, false, false, "x"); + + // the star needs no space + update = (Update) CCJSqlParserUtil.parse("UPDATE t* SET a = 1", POSTGRESQL); + assertEquals("UPDATE t * SET a = 1", update.toString()); + } + + @Test + void deleteAndMergeTargetModel() throws JSQLParserException { + Delete delete = (Delete) parse("DELETE FROM ONLY t AS x WHERE x.a = 1"); + assertTarget(delete.getTableReference(), "t", true, false, false, "x"); + assertTrue(delete.isHasFrom()); + assertEquals("x.a = 1", delete.getWhere().toString()); + + delete = (Delete) parse("DELETE FROM sc.t * x USING u WHERE x.a = u.a"); + assertTarget(delete.getTableReference(), "sc.t", false, true, false, "x"); + assertEquals(1, delete.getUsingFromItemList().size()); + + delete = (Delete) parse("DELETE FROM t WHERE a = 1"); + assertTarget(delete.getTableReference(), "t", false, false, false, null); + + Merge merge = (Merge) parse( + "MERGE INTO ONLY (t) x USING s ON x.id = s.id WHEN MATCHED THEN DELETE"); + assertTarget(merge.getTableReference(), "t", true, false, true, "x"); + assertEquals("s", merge.getFromItem().toString()); + + merge = (Merge) parse("MERGE INTO t * USING s ON t.id = s.id WHEN MATCHED THEN DELETE"); + assertTarget(merge.getTableReference(), "t", false, true, false, null); + } + + @Test + void targetsAreBuiltThroughTheModel() { + Update update = new Update().withTable(new Table("t")) + .addUpdateSet(new Column("a"), new LongValue(1)); + assertEquals("UPDATE t SET a = 1", update.toString()); + update.getTableReference().setOnly(true); + assertEquals("UPDATE ONLY t SET a = 1", update.toString()); + assertEquals(update.toString(), deparse(update)); + + // replacing the table keeps the scope + update.setTable(new Table("u").withAlias(new Alias("x", true))); + assertEquals("UPDATE ONLY u AS x SET a = 1", update.toString()); + TableReference descendants = new TableReference(new Table("t")); + descendants.setIncludeDescendants(true); + assertEquals("UPDATE t * SET a = 1", update.withTableReference(descendants).toString()); + assertThrows(NullPointerException.class, () -> update.setTableReference(null)); + + Delete delete = new Delete().withTable(new Table("t")).withHasFrom(true); + assertEquals("DELETE FROM t", delete.toString()); + delete.getTableReference().setParenthesized(true); + assertEquals("DELETE FROM ONLY (t)", delete.toString()); + assertEquals(delete.toString(), deparse(delete)); + assertEquals("DELETE FROM t *", delete.withTableReference(descendants).toString()); + assertThrows(NullPointerException.class, () -> delete.setTableReference(null)); + + Merge merge = new Merge().withTableReference(descendants); + assertEquals("t", merge.getTable().getName()); + assertThrows(NullPointerException.class, () -> merge.setTableReference(null)); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE ONLY t * SET a = 1", + "UPDATE (t) SET a = 1", + "UPDATE ONLY (t *) SET a = 1", + "UPDATE ONLY (t AS x) SET a = 1", + "UPDATE ONLY ONLY t SET a = 1", + "UPDATE ONLY SET a = 1", + "UPDATE t * * SET a = 1", + "UPDATE t AS x * SET a = 1", + "DELETE FROM ONLY t * WHERE a = 1", + "DELETE FROM (t) WHERE a = 1", + "DELETE FROM ONLY (t AS x)", + "DELETE ONLY t", + "DELETE FROM t AS x *", + "MERGE INTO ONLY t * USING s ON t.id = s.id WHEN MATCHED THEN DELETE", + "MERGE ONLY t USING s ON t.id = s.id WHEN MATCHED THEN DELETE" + }) + void rejectedTargets(String sql) { + assertRejected(sql); + } + + @Test + void mySqlHasNoOnlyTargets() throws JSQLParserException { + // MySQL's ONLY is no keyword of UPDATE or DELETE; the forms stay as they were + for (Consumer dialect : Arrays.asList(MYSQL, MARIADB)) { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("UPDATE ONLY t SET a = 1", dialect)); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("DELETE FROM ONLY t WHERE a = 1", dialect)); + Delete delete = (Delete) assertSqlCanBeParsedAndDeparsed("DELETE FROM only WHERE a = 1", + false, dialect); + assertEquals("only", delete.getTable().getName()); + assertFalse(delete.getTableReference().isOnly()); + } + Delete delete = (Delete) assertSqlCanBeParsedAndDeparsed("DELETE FROM only", false); + assertEquals("only", delete.getTable().getName()); + + // index hints and multi-table forms keep their readings + Update update = (Update) assertSqlCanBeParsedAndDeparsed( + "UPDATE t AS x USE INDEX (i) SET a = 1", false, MYSQL); + assertEquals(1, update.getTable().getIndexHints().size()); + assertFalse(update.getTableReference().hasInheritanceScope()); + assertSqlCanBeParsedAndDeparsed("UPDATE LOW_PRIORITY IGNORE t, u SET t.a = u.a", false, + MYSQL); + assertSqlCanBeParsedAndDeparsed("DELETE t, u FROM t JOIN u ON t.id = u.id", false, MYSQL); + assertSqlCanBeParsedAndDeparsed("UPDATE OR REPLACE t SET a = 1", false, + parser -> parser.withDialect(Dialect.SQLITE)); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE t SET a = 1 WHERE CURRENT OF c", + "UPDATE t SET a = 1 WHERE CURRENT OF c RETURNING a", + "UPDATE ONLY t AS x SET a = 1 WHERE CURRENT OF c", + "UPDATE t SET a = 1 FROM u WHERE CURRENT OF c", + "UPDATE t SET a = 1 WHERE CURRENT OF \"My Cursor\"", + "UPDATE t SET a = 1 WHERE CURRENT OF current", + "UPDATE t SET (a, b) = (1, 2) WHERE CURRENT OF c", + "DELETE FROM t WHERE CURRENT OF c", + "DELETE FROM t WHERE CURRENT OF c RETURNING *", + "DELETE FROM ONLY t x WHERE CURRENT OF c", + "DELETE FROM t USING u WHERE CURRENT OF c" + }) + void whereCurrentOf(String sql) throws JSQLParserException { + parse(sql); + // the positioned forms are standard SQL + assertSqlCanBeParsedAndDeparsed(sql, false, ORACLE); + } + + @Test + void whereCurrentOfModel() throws JSQLParserException { + Update update = (Update) parse("UPDATE t SET a = 1 WHERE CURRENT OF c RETURNING a"); + assertEquals("c", update.getCursorName()); + assertNull(update.getWhere()); + assertNotNull(update.getReturningClause()); + + Delete delete = (Delete) parse("DELETE FROM t WHERE CURRENT OF \"My Cursor\""); + assertEquals("\"My Cursor\"", delete.getCursorName()); + assertNull(delete.getWhere()); + + // a column named current is still a condition + update = (Update) parse("UPDATE t SET a = 1 WHERE current = 1"); + assertNull(update.getCursorName()); + assertEquals("current = 1", update.getWhere().toString()); + delete = (Delete) parse("DELETE FROM t WHERE current"); + assertNull(delete.getCursorName()); + + update = new Update().withTable(new Table("t")) + .addUpdateSet(new Column("a"), new LongValue(1)).withCursorName("c"); + assertEquals("UPDATE t SET a = 1 WHERE CURRENT OF c", update.toString()); + assertEquals(update.toString(), deparse(update)); + delete = new Delete().withTable(new Table("t")).withHasFrom(true).withCursorName("c"); + assertEquals("DELETE FROM t WHERE CURRENT OF c", delete.toString()); + assertEquals(delete.toString(), deparse(delete)); + delete.setCursorName(null); + assertEquals("DELETE FROM t", delete.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE t SET a = 1 WHERE CURRENT OF c AND a = 1", + "UPDATE t SET a = 1 WHERE CURRENT OF sc.c", + "UPDATE t SET a = 1 WHERE CURRENT OF", + "UPDATE t SET a = 1 WHERE CURRENT c", + "UPDATE t SET a = 1 WHERE CURRENT OF 'c'", + "UPDATE t SET a = 1 WHERE a = 1 AND CURRENT OF c", + "UPDATE t SET a = 1 WHERE CURRENT OF c WHERE a = 1", + "DELETE FROM t WHERE CURRENT OF c OR a = 1", + "DELETE FROM t WHERE CURRENT OF sc.c", + "DELETE FROM t WHERE CURRENT OF", + "SELECT * FROM t WHERE CURRENT OF c", + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DELETE WHERE CURRENT OF c" + }) + void rejectedCursorForms(String sql) { + assertRejected(sql); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE t SET a = DEFAULT", + "UPDATE t SET (a, b) = (1, 2)", + "UPDATE t SET (a, b) = ROW(1, 2)", + "UPDATE t SET (a, b) = ROW(1, DEFAULT)", + "UPDATE t SET (a, b) = (DEFAULT, DEFAULT)", + "UPDATE t SET (a, b) = (SELECT 1, 2)", + "UPDATE t SET (a, b) = (SELECT s.a, s.b FROM s WHERE s.id = t.id)", + "UPDATE t SET (a) = (1)", + "UPDATE t SET (a) = ROW(1)", + "UPDATE t SET (a) = 1", + "UPDATE t SET (a) = (SELECT 1)", + "UPDATE t SET (a) = DEFAULT", + "UPDATE t SET a = 1, (b, c) = ROW(2, 3), d = DEFAULT", + "UPDATE t SET (a, b) = ROW(1, 2), (c, d) = (3, 4)", + "UPDATE t SET (a, b) = (SELECT 1, 2), c = 3", + "UPDATE t SET (arr[1], a) = ROW(1, 2)", + "UPDATE t SET (arr[1][2], arr[1:2]) = ROW(1, 2)", + "UPDATE t SET (a, b) = ((SELECT 1, 2))", + "UPDATE t SET (a, b) = 1", + "UPDATE t SET (a, b) = f(1)", + "UPDATE t SET (a, b) = DEFAULT", + "UPDATE t SET (a, b) = ROW(1, 2) || ROW(3, 4)", + "UPDATE t SET a = ROW(1, 2)", + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET (a, b) = ROW(s.a, DEFAULT), c = DEFAULT", + "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET (a, b) = (SELECT 1, 2)", + "INSERT INTO t VALUES (1, 2) ON CONFLICT (id) DO UPDATE SET (a, b) = ROW(1, 2)" + }) + void setClauseSources(String sql) throws JSQLParserException { + parse(sql); + } + + @Test + void setClauseModel() throws JSQLParserException { + Update update = (Update) parse("UPDATE t SET a = 1, (b, c) = ROW(2, DEFAULT), d = DEFAULT"); + List sets = update.getUpdateSets(); + assertEquals(3, sets.size()); + assertEquals("a", sets.get(0).getColumn(0).getColumnName()); + + // ROW(...) is one source for the column list, the function it is in other expressions + UpdateSet row = sets.get(1); + assertInstanceOf(ParenthesedExpressionList.class, row.getColumns()); + assertEquals(2, row.getColumns().size()); + assertEquals(1, row.getValues().size()); + assertFalse(row.getValues() instanceof ParenthesedExpressionList); + Function constructor = assertInstanceOf(Function.class, row.getValue(0)); + assertEquals("ROW", constructor.getName()); + assertEquals(2, constructor.getParameters().size()); + assertNotNull(row.getColumn(0).getASTNode()); + assertEquals("d = DEFAULT", sets.get(2).toString()); + + // a parenthesized list keeps one value per column, a sub-select is one value + update = (Update) parse("UPDATE t SET (a, b) = (1, 2), (c, d) = (SELECT 3, 4)"); + assertInstanceOf(ParenthesedExpressionList.class, + update.getUpdateSets().get(0).getValues()); + assertEquals(2, update.getUpdateSets().get(0).getValues().size()); + assertInstanceOf(ParenthesedSelect.class, update.getUpdateSets().get(1).getValue(0)); + + // other dialects read ROW(...) as well + assertSqlCanBeParsedAndDeparsed("UPDATE t SET (a, b) = ROW(1, 2)", false, ORACLE); + assertSqlCanBeParsedAndDeparsed("UPDATE t SET (a, b) = ROW(1, 2)", false, MYSQL); + } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE t SET (a, b) = ROW 1, 2", + "UPDATE t SET () = ROW(1, 2)", + "UPDATE t SET (a, b) ROW(1, 2)", + "UPDATE t SET (a, b) = ROW(1, 2),", + "UPDATE t SET (a, b) = ", + "UPDATE t SET ROW(a, b) = ROW(1, 2)", + "UPDATE t SET (a, (b)) = ROW(1, 2)", + "UPDATE t SET (a + 1) = ROW(1)", + "UPDATE t SET (a, 1) = DEFAULT" + }) + void rejectedSetClauses(String sql) { + assertRejected(sql); + } + + @ParameterizedTest + @ValueSource(strings = { + "WHEN MATCHED THEN DO NOTHING", + "WHEN MATCHED AND t.a > 1 THEN DO NOTHING", + "WHEN NOT MATCHED THEN DO NOTHING", + "WHEN NOT MATCHED AND s.a > 1 THEN DO NOTHING", + "WHEN NOT MATCHED BY SOURCE THEN DO NOTHING", + "WHEN NOT MATCHED BY SOURCE AND t.a > 1 THEN DO NOTHING", + "WHEN NOT MATCHED BY TARGET THEN DO NOTHING", + "WHEN NOT MATCHED BY SOURCE THEN DELETE", + "WHEN NOT MATCHED BY SOURCE THEN UPDATE SET a = 1", + "WHEN NOT MATCHED BY TARGET THEN INSERT (id) VALUES (s.id)", + "WHEN MATCHED THEN DO NOTHING WHEN NOT MATCHED THEN INSERT DEFAULT VALUES" + + " WHEN NOT MATCHED BY SOURCE THEN DELETE", + "WHEN NOT MATCHED THEN INSERT DEFAULT VALUES", + "WHEN NOT MATCHED BY TARGET AND s.a > 1 THEN INSERT DEFAULT VALUES", + "WHEN NOT MATCHED THEN INSERT OVERRIDING SYSTEM VALUE VALUES (1, 2)", + "WHEN NOT MATCHED THEN INSERT OVERRIDING USER VALUE VALUES (1, 2)", + "WHEN NOT MATCHED THEN INSERT (id, a) OVERRIDING SYSTEM VALUE VALUES (1, 2)", + "WHEN NOT MATCHED THEN INSERT (id, a) OVERRIDING USER VALUE VALUES (1, 2)", + "WHEN MATCHED THEN DO NOTHING RETURNING merge_action()" + }) + void mergeWhenClauses(String clauses) throws JSQLParserException { + parse(MERGE + clauses); + } + + @Test + void mergeDoNothingModel() throws JSQLParserException { + Merge merge = (Merge) parse(MERGE + "WHEN MATCHED AND t.a > 1 THEN DO NOTHING" + + " WHEN NOT MATCHED AND s.a > 2 THEN DO NOTHING" + + " WHEN NOT MATCHED BY SOURCE AND t.a > 3 THEN DO NOTHING" + + " WHEN NOT MATCHED BY TARGET AND s.a > 1 THEN DO NOTHING" + + " WHEN NOT MATCHED BY SOURCE AND t.a > 4 THEN DELETE" + + " WHEN NOT MATCHED BY SOURCE THEN UPDATE SET a = 1" + + " WHEN MATCHED THEN DELETE" + + " WHEN NOT MATCHED BY TARGET AND s.a > 5 THEN INSERT (id) VALUES (s.id)" + + " WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id)"); + List operations = merge.getOperations(); + assertEquals(9, operations.size()); + + MergeDoNothing matched = assertInstanceOf(MergeDoNothing.class, operations.get(0)); + assertEquals(MergeWhen.MATCHED, matched.getWhen()); + assertEquals("t.a > 1", matched.getAndPredicate().toString()); + assertEquals(" WHEN MATCHED AND t.a > 1 THEN DO NOTHING", matched.toString()); + assertEquals(MergeWhen.NOT_MATCHED, ((MergeDoNothing) operations.get(1)).getWhen()); + assertEquals("s.a > 2", ((MergeDoNothing) operations.get(1)).getAndPredicate().toString()); + assertEquals(MergeWhen.NOT_MATCHED_BY_SOURCE, + ((MergeDoNothing) operations.get(2)).getWhen()); + assertEquals(MergeWhen.NOT_MATCHED_BY_TARGET, + ((MergeDoNothing) operations.get(3)).getWhen()); + + // the other actions derive the condition from their side + assertEquals(MergeWhen.NOT_MATCHED_BY_SOURCE, ((MergeDelete) operations.get(4)).getWhen()); + assertEquals(MergeWhen.NOT_MATCHED_BY_SOURCE, ((MergeUpdate) operations.get(5)).getWhen()); + assertEquals(MergeWhen.MATCHED, ((MergeDelete) operations.get(6)).getWhen()); + assertEquals(MergeWhen.NOT_MATCHED_BY_TARGET, ((MergeInsert) operations.get(7)).getWhen()); + assertEquals(MergeWhen.NOT_MATCHED, ((MergeInsert) operations.get(8)).getWhen()); + assertEquals(MergeWhen.MATCHED, new MergeUpdate().getWhen()); + + assertEquals(MergeSide.SOURCE, MergeWhen.NOT_MATCHED_BY_SOURCE.getSide()); + assertEquals(MergeSide.TARGET, MergeWhen.NOT_MATCHED_BY_TARGET.getSide()); + assertNull(MergeWhen.MATCHED.getSide()); + assertNull(MergeWhen.NOT_MATCHED.getSide()); + assertEquals("WHEN NOT MATCHED BY SOURCE", MergeWhen.NOT_MATCHED_BY_SOURCE.toString()); + + MergeDoNothing built = new MergeDoNothing().withWhen(MergeWhen.NOT_MATCHED) + .withAndPredicate(new Column("a")); + assertEquals(" WHEN NOT MATCHED AND a THEN DO NOTHING", built.toString()); + assertEquals(MergeWhen.MATCHED, new MergeDoNothing().getWhen()); + assertThrows(NullPointerException.class, () -> built.setWhen(null)); + + // DO NOTHING is neither the standard update nor the insert clause + merge = (Merge) parse(MERGE + "WHEN MATCHED THEN DO NOTHING"); + assertNull(merge.getMergeUpdate()); + assertNull(merge.getMergeInsert()); + } + + @Test + void mergeInsertModel() throws JSQLParserException { + Merge merge = (Merge) parse(MERGE + + "WHEN NOT MATCHED AND s.a > 1 THEN INSERT DEFAULT VALUES" + + " WHEN NOT MATCHED AND s.a > 2 THEN INSERT (id, a) OVERRIDING USER VALUE VALUES (1, 2)" + + " WHEN NOT MATCHED THEN INSERT OVERRIDING SYSTEM VALUE VALUES (1, 2)"); + MergeInsert defaults = (MergeInsert) merge.getOperations().get(0); + assertTrue(defaults.isDefaultValues()); + assertNull(defaults.getValues()); + assertNull(defaults.getColumns()); + assertEquals(Insert.OverridingMode.NONE, defaults.getOverridingMode()); + + MergeInsert user = (MergeInsert) merge.getOperations().get(1); + assertFalse(user.isDefaultValues()); + assertEquals(Insert.OverridingMode.USER, user.getOverridingMode()); + assertEquals(2, user.getColumns().size()); + assertEquals(2, user.getValues().size()); + + MergeInsert system = (MergeInsert) merge.getOperations().get(2); + assertEquals(Insert.OverridingMode.SYSTEM, system.getOverridingMode()); + assertNull(system.getColumns()); + + MergeInsert built = new MergeInsert(); + assertEquals(" WHEN NOT MATCHED THEN INSERT DEFAULT VALUES", built.toString()); + built.withOverridingMode(Insert.OverridingMode.SYSTEM).addColumns(new Column("id")) + .withValues(new ParenthesedExpressionList(new LongValue(1))); + assertThrows(NullPointerException.class, () -> built.setOverridingMode(null)); + } + + @ParameterizedTest + @ValueSource(strings = { + "WHEN MATCHED THEN DO", + "WHEN MATCHED THEN NOTHING", + "WHEN MATCHED THEN DO NOTHING WHERE t.a = 1", + "WHEN MATCHED THEN DO UPDATE SET a = 1", + "WHEN MATCHED THEN INSERT DEFAULT VALUES", + "WHEN MATCHED THEN INSERT VALUES (1)", + "WHEN NOT MATCHED THEN UPDATE SET a = 1", + "WHEN NOT MATCHED THEN DELETE", + "WHEN NOT MATCHED BY TARGET THEN DELETE", + "WHEN NOT MATCHED BY SOURCE THEN INSERT VALUES (1)", + "WHEN NOT MATCHED BY SOURCE THEN INSERT DEFAULT VALUES", + "WHEN MATCHED BY SOURCE THEN DELETE", + "WHEN NOT MATCHED BY foo THEN DO NOTHING", + "WHEN NOT MATCHED THEN INSERT (id) DEFAULT VALUES", + "WHEN NOT MATCHED THEN INSERT OVERRIDING SYSTEM VALUE DEFAULT VALUES", + "WHEN NOT MATCHED THEN INSERT OVERRIDING VALUE VALUES (1)", + "WHEN NOT MATCHED THEN INSERT OVERRIDING SYSTEM VALUES (1)", + "WHEN NOT MATCHED THEN INSERT OVERRIDING SYSTEM VALUE (id) VALUES (1)", + "WHEN NOT MATCHED THEN INSERT DEFAULT VALUES (1)", + "WHEN NOT MATCHED THEN INSERT DEFAULT VALUES WHERE s.a = 1", + "WHEN NOT MATCHED THEN INSERT VALUES (1), (2)", + "WHEN NOT MATCHED THEN INSERT (id) SELECT 1" + }) + void rejectedMergeClauses(String clauses) { + assertThrows(Exception.class, () -> CCJSqlParserUtil.parse(MERGE + clauses, POSTGRESQL), + clauses); + assertThrows(Exception.class, () -> CCJSqlParserUtil.parse(MERGE + clauses), clauses); + } + + @Test + void otherMergeDialectsKeepTheirClauses() throws JSQLParserException { + Merge merge = (Merge) assertSqlCanBeParsedAndDeparsed( + "MERGE INTO t USING s ON (t.id = s.id) WHEN MATCHED THEN UPDATE SET a = 1" + + " WHERE t.a = 2 DELETE WHERE t.a = 3" + + " WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id) WHERE s.a = 2", + true, ORACLE); + assertNotNull(merge.getMergeUpdate().getDeleteWhereCondition()); + assertNotNull(merge.getMergeInsert().getWhereCondition()); + assertFalse(merge.getMergeInsert().isDefaultValues()); + assertFalse(merge.getTableReference().hasInheritanceScope()); + + assertSqlCanBeParsedAndDeparsed( + "MERGE INTO t AS x WITH (HOLDLOCK) USING s ON x.id = s.id" + + " WHEN NOT MATCHED BY TARGET THEN INSERT (id) VALUES (s.id)" + + " WHEN NOT MATCHED BY SOURCE THEN DELETE OUTPUT deleted.id", + true, parser -> parser.withDialect(Dialect.SQLSERVER)); + } + + @Test + void visitorsReachTheDoNothingPredicate() throws JSQLParserException { + Merge merge = (Merge) CCJSqlParserUtil.parse(MERGE + + "WHEN MATCHED AND t.a IN (SELECT a FROM u) THEN DO NOTHING" + + " WHEN NOT MATCHED THEN INSERT DEFAULT VALUES", POSTGRESQL); + + List columns = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + columns.add(column.getFullyQualifiedName()); + return super.visit(column, context); + } + }; + merge.getOperations().get(0).accept(new MergeOperationVisitorAdapter<>(expressions), null); + assertEquals(Arrays.asList("t.a"), columns); + merge.getOperations().get(1).accept(new MergeOperationVisitorAdapter<>(expressions), null); + merge.accept(new StatementVisitorAdapter(new SelectVisitorAdapter<>(expressions)), + null); + + // a visitor written before DO NOTHING existed skips the action + MergeOperationVisitor legacy = new MergeOperationVisitor() { + @Override + public String visit(MergeDelete mergeDelete, S context) { + return "delete"; + } + + @Override + public String visit(MergeUpdate mergeUpdate, S context) { + return "update"; + } + + @Override + public String visit(MergeInsert mergeInsert, S context) { + return "insert"; + } + }; + assertNull(merge.getOperations().get(0).accept(legacy, null)); + assertEquals("insert", merge.getOperations().get(1).accept(legacy, null)); + + assertEquals(Arrays.asList("s", "t", "u"), + sorted(new TablesNamesFinder().getTables((Statement) merge))); + } + + private static List sorted(Collection names) { + List list = new ArrayList<>(names); + Collections.sort(list); + return list; + } + + @Test + void tablesNamesFinder() throws JSQLParserException { + assertEquals(Arrays.asList("sc.t", "u"), sorted(TablesNamesFinder.findTables( + "UPDATE ONLY sc.t AS x SET (a, b) = ROW(1, (SELECT max(a) FROM u))" + + " WHERE CURRENT OF c"))); + assertEquals(Arrays.asList("t", "u"), sorted(TablesNamesFinder.findTables( + "DELETE FROM t * AS x USING u WHERE CURRENT OF c"))); + assertEquals(Arrays.asList("s", "t"), sorted(TablesNamesFinder.findTables( + "MERGE INTO ONLY (t) x USING s ON x.id = s.id WHEN NOT MATCHED THEN DO NOTHING"))); + } + + @Test + void statementBoundaries() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + MERGE + "WHEN MATCHED THEN DO NOTHING; " + + "UPDATE t SET (a, b) = ROW(1, 2) WHERE CURRENT OF c; " + + "DELETE FROM ONLY t x WHERE CURRENT OF c; " + + MERGE + "WHEN NOT MATCHED THEN INSERT DEFAULT VALUES; " + + "DO $$ BEGIN NULL; END $$; " + + "UPDATE t * SET a = DEFAULT", + POSTGRESQL); + assertEquals(6, statements.size()); + assertInstanceOf(Merge.class, statements.get(0)); + assertEquals("c", ((Update) statements.get(1)).getCursorName()); + assertEquals("c", ((Delete) statements.get(2)).getCursorName()); + assertTrue(((MergeInsert) ((Merge) statements.get(3)).getOperations().get(0)) + .isDefaultValues()); + assertTrue(((Update) statements.get(5)).getTableReference().isIncludeDescendants()); + } + + @Test + void validation() { + ValidationTestAsserts.validateNoErrors("UPDATE ONLY t AS x SET (a, b) = ROW(1, 2)", 1, + PostgresqlVersion.V14); + ValidationTestAsserts.validateNoErrors("DELETE FROM ONLY t x WHERE CURRENT OF c", 1, + PostgresqlVersion.V14); + ValidationTestAsserts.validateNoErrors( + MERGE + "WHEN MATCHED AND t.a > 1 THEN DO NOTHING" + + " WHEN NOT MATCHED THEN INSERT DEFAULT VALUES", + 1, FeaturesAllowed.DML); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java index 2650c5cd1a..3f74c7a4d0 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java @@ -82,7 +82,8 @@ public class SpecialOracleTest { "condition07.sql", "condition08.sql", "condition09.sql", "condition10.sql", "condition11.sql", "condition12.sql", - "condition14.sql", "condition15.sql", "condition19.sql", "condition20.sql", + "condition14.sql", "condition15.sql", "condition17.sql", "condition18.sql", + "condition19.sql", "condition20.sql", "connect_by01.sql", "connect_by02.sql", "connect_by03.sql", "connect_by04.sql", "connect_by05.sql", "connect_by06.sql", "connect_by07.sql", "connect_by08.sql", "connect_by09.sql", "connect_by10.sql", "datetime01.sql", diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition17.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition17.sql index 29cfb84a0b..ae49ca05c2 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition17.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition17.sql @@ -12,4 +12,5 @@ where current of cursor_name --@FAILURE: Encountered unexpected token: "of" "OF" recorded first on Aug 3, 2021, 7:20:08 AM --@FAILURE: Encountered: / "of", at line 11, column 15, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08 ---@FAILURE: --- recorded first on 11 Apr 2026, 22:18:19 \ No newline at end of file +--@FAILURE: --- recorded first on 11 Apr 2026, 22:18:19 +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 12 Nov 2026, 13:22:49 \ No newline at end of file diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition18.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition18.sql index 904ddb779c..3da545cea4 100644 --- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition18.sql +++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/condition18.sql @@ -14,4 +14,5 @@ where current of c_cur1 --@FAILURE: Encountered unexpected token: "of" "OF" recorded first on Aug 3, 2021, 7:20:08 AM --@FAILURE: Encountered: / "of", at line 12, column 15, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08 ---@FAILURE: --- recorded first on 11 Apr 2026, 22:18:19 \ No newline at end of file +--@FAILURE: --- recorded first on 11 Apr 2026, 22:18:19 +--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 12 Nov 2026, 13:22:49 \ No newline at end of file