From 07576db9352bc3d7a7036209cefa7b5ac4261592 Mon Sep 17 00:00:00 2001 From: minleejae Date: Thu, 12 Nov 2026 13:26:43 +0900 Subject: [PATCH] feat: parse PostgreSQL inheritance targets, WHERE CURRENT OF, row sources and MERGE DO NOTHING UPDATE, DELETE and MERGE take PostgreSQL's relation_expr_opt_alias as the target: ONLY t, ONLY (t) and t * with an alias, kept in a TableReference next to getTable(). UPDATE and DELETE read WHERE CURRENT OF cursor into a cursor name. A SET column list takes any expression as its source, such as ROW(...), through one UpdateSet production. MERGE reads DO NOTHING for every WHEN condition, INSERT DEFAULT VALUES and INSERT ... OVERRIDING {SYSTEM | USER} VALUE; the WHEN prefix is rendered by MergeWhen. Signed-off-by: minleejae --- .../sf/jsqlparser/schema/TableReference.java | 26 +- .../jsqlparser/statement/delete/Delete.java | 50 +- .../sf/jsqlparser/statement/merge/Merge.java | 28 +- .../statement/merge/MergeDelete.java | 11 +- .../statement/merge/MergeDoNothing.java | 65 ++ .../statement/merge/MergeInsert.java | 64 +- .../statement/merge/MergeOperation.java | 3 +- .../merge/MergeOperationVisitor.java | 5 + .../merge/MergeOperationVisitorAdapter.java | 6 + .../jsqlparser/statement/merge/MergeSide.java | 5 +- .../statement/merge/MergeUpdate.java | 11 +- .../jsqlparser/statement/merge/MergeWhen.java | 72 ++ .../jsqlparser/statement/update/Update.java | 53 +- .../sf/jsqlparser/util/TablesNamesFinder.java | 9 + .../util/deparser/DeleteDeParser.java | 6 +- .../util/deparser/MergeDeParser.java | 53 +- .../util/deparser/UpdateDeParser.java | 6 +- .../validation/validator/MergeValidator.java | 6 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 215 ++++-- .../statement/PostgreSQLModifyFormsTest.java | 672 ++++++++++++++++++ .../statement/select/SpecialOracleTest.java | 3 +- .../select/oracle-tests/condition17.sql | 3 +- .../select/oracle-tests/condition18.sql | 3 +- 23 files changed, 1230 insertions(+), 145 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/merge/MergeDoNothing.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/merge/MergeWhen.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/PostgreSQLModifyFormsTest.java 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