Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions presto-docs/src/main/sphinx/sql/merge.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,26 @@ Synopsis
ON search_condition
WHEN MATCHED THEN
UPDATE SET ( column = expression [, ...] )
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED THEN
INSERT [ column_list ]
VALUES (expression, ...)

Description
-----------

The ``MERGE`` statement inserts or updates rows in a ``target_table`` based on the contents of the ``source_table``.
The ``MERGE`` statement conditionally inserts, updates, or deletes rows in a ``target_table`` based on the contents of the ``source_table``.
The ``search_condition`` defines a relation between the source and target tables.
When the condition is met, the target row is updated. When the condition is not met, a new row is inserted into the target table.
In the ``MATCHED`` case, the ``UPDATE`` column value expressions can depend on any field of the target or the source.
In the ``NOT MATCHED`` case, the ``INSERT`` expressions can depend on any field of the source.

When the condition is met, one of the following ``MATCHED`` actions can be taken:

* ``UPDATE``: The target row is updated. The ``UPDATE`` column value expressions can depend on any field of the target or the source.
* ``DELETE``: The target row is deleted from the target table.

When the condition is not met, the ``NOT MATCHED`` action inserts a new row into the target table. The ``INSERT`` expressions can depend on any field of the source.

A ``MERGE`` statement can contain any combination of ``WHEN MATCHED`` and ``WHEN NOT MATCHED`` clauses. For example, you can use ``WHEN MATCHED THEN DELETE`` together with ``WHEN NOT MATCHED THEN INSERT`` to delete existing matched rows and insert new unmatched rows in a single atomic operation.

The ``MERGE`` command requires each target row to match at most one source row. An exception is raised when a single target table row matches more than one source row.
If a source row is not matched by the ``WHEN MATCHED`` clause and there is no ``WHEN NOT MATCHED`` clause, the source row is ignored.
Expand All @@ -39,12 +47,16 @@ MERGE Command Privileges
The ``MERGE`` statement does not have a dedicated privilege. Instead, executing a ``MERGE`` statement requires the privileges associated with the individual actions it performs:

* ``UPDATE`` actions: require the ``UPDATE`` privilege on the target table columns referenced in the ``SET`` clause.
* ``DELETE`` actions: require the ``DELETE`` privilege on the target table.
* ``INSERT`` actions: require the ``INSERT`` privilege on the target table.

Each privilege must be granted to the user executing the ``MERGE`` command, based on the specific operations included in the statement.

Example
-------
Examples
--------

Update and insert
^^^^^^^^^^^^^^^^^

Update the sales information for existing products and insert the sales information for the new products in the market.

Expand All @@ -62,6 +74,35 @@ Update the sales information for existing products and insert the sales informat
INSERT (product_id, sales, last_sale, current_price)
VALUES (ms.product_id, ms.sales, ms.sale_date, ms.price)

Delete and insert
^^^^^^^^^^^^^^^^^

Delete matched rows from the target table and insert unmatched rows from the source. This is useful for replacing existing records with new data in a single atomic operation.

.. code-block:: text

MERGE INTO product_sales AS s
USING monthly_sales AS ms
ON s.product_id = ms.product_id
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED THEN
INSERT (product_id, sales, last_sale, current_price)
VALUES (ms.product_id, ms.sales, ms.sale_date, ms.price)

Delete only
^^^^^^^^^^^

Delete all rows in the target table that match the source. Rows in the target table that have no match in the source remain unchanged.

.. code-block:: text

MERGE INTO product_sales AS s
USING discontinued_products AS d
ON s.product_id = d.product_id
WHEN MATCHED THEN
DELETE

Limitations
-----------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3315,6 +3315,85 @@ public void testMergeSimpleQuery(String partitioning)
}
}

@Test
public void testMergeDeleteWithInsert()
{
String targetTable = "merge_delete_insert_" + randomTableSuffix();
try {
assertUpdate(format("CREATE TABLE %s (customer VARCHAR, purchases INT, address VARCHAR)", targetTable));
assertUpdate(format("INSERT INTO %s (customer, purchases, address) VALUES ('Aaron', 5, 'Antioch'), ('Bill', 7, 'Buena'), ('Carol', 3, 'Cambridge'), ('Dave', 11, 'Devon')", targetTable), 4);

@Language("SQL") String sqlMergeCommand =
format("MERGE INTO %s t USING ", targetTable) +
"(VALUES ('Aaron', 6, 'Arches'), ('Carol', 9, 'Centreville'), ('Ed', 7, 'Etherville')) AS s(customer, purchases, address) " +
"ON (t.customer = s.customer) " +
"WHEN MATCHED THEN" +
" DELETE " +
"WHEN NOT MATCHED THEN" +
" INSERT (customer, purchases, address) VALUES(s.customer, s.purchases, s.address)";

assertUpdate(sqlMergeCommand, 3);

assertQuery("SELECT * FROM " + targetTable,
"VALUES ('Bill', 7, 'Buena'), ('Dave', 11, 'Devon'), ('Ed', 7, 'Etherville')");
}
finally {
assertUpdate("DROP TABLE " + targetTable);
}
}

@Test
public void testMergeDeleteOnly()
{
String targetTable = "merge_delete_only_" + randomTableSuffix();
try {
assertUpdate(format("CREATE TABLE %s (customer VARCHAR, purchases INT, address VARCHAR)", targetTable));
assertUpdate(format("INSERT INTO %s (customer, purchases, address) VALUES ('Aaron', 5, 'Antioch'), ('Bill', 7, 'Buena'), ('Carol', 3, 'Cambridge'), ('Dave', 11, 'Devon')", targetTable), 4);

@Language("SQL") String sqlMergeCommand =
format("MERGE INTO %s t USING ", targetTable) +
"(VALUES ('Aaron', 6, 'Arches'), ('Carol', 9, 'Centreville')) AS s(customer, purchases, address) " +
"ON (t.customer = s.customer) " +
"WHEN MATCHED THEN" +
" DELETE";

assertUpdate(sqlMergeCommand, 2);

assertQuery("SELECT * FROM " + targetTable,
"VALUES ('Bill', 7, 'Buena'), ('Dave', 11, 'Devon')");
}
finally {
assertUpdate("DROP TABLE " + targetTable);
}
}

@Test(dataProvider = "partitionedProvider")
public void testMergeDeletePartitioned(String partitioning)
{
String targetTable = "merge_delete_partitioned_" + randomTableSuffix();
try {
assertUpdate(format("CREATE TABLE %s (customer VARCHAR, purchases INT, address VARCHAR) %s", targetTable, partitioning));
assertUpdate(format("INSERT INTO %s (customer, purchases, address) VALUES ('Aaron', 5, 'Antioch'), ('Bill', 7, 'Buena'), ('Carol', 3, 'Cambridge'), ('Dave', 11, 'Devon')", targetTable), 4);

@Language("SQL") String sqlMergeCommand =
format("MERGE INTO %s t USING ", targetTable) +
"(VALUES ('Aaron', 6, 'Arches'), ('Carol', 9, 'Centreville'), ('Ed', 7, 'Etherville')) AS s(customer, purchases, address) " +
"ON (t.customer = s.customer) " +
"WHEN MATCHED THEN" +
" DELETE " +
"WHEN NOT MATCHED THEN" +
" INSERT (customer, purchases, address) VALUES(s.customer, s.purchases, s.address)";

assertUpdate(sqlMergeCommand, 3);

assertQuery("SELECT * FROM " + targetTable,
"VALUES ('Bill', 7, 'Buena'), ('Dave', 11, 'Devon'), ('Ed', 7, 'Etherville')");
}
finally {
assertUpdate("DROP TABLE " + targetTable);
}
}

@Test
public void testMergeSimpleQueryPartitioned()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
import com.facebook.presto.sql.tree.LongLiteral;
import com.facebook.presto.sql.tree.Merge;
import com.facebook.presto.sql.tree.MergeCase;
import com.facebook.presto.sql.tree.MergeDelete;
import com.facebook.presto.sql.tree.MergeInsert;
import com.facebook.presto.sql.tree.MergeUpdate;
import com.facebook.presto.sql.tree.NaturalJoin;
Expand Down Expand Up @@ -3546,6 +3547,13 @@ else if (mergeCase instanceof MergeInsert && setColumnNames.isEmpty()) {
.ifPresent(mergeCase -> accessControl.checkCanInsertIntoTable(session.getRequiredTransactionId(),
session.getIdentity(), session.getAccessControlContext(), targetTableQualifiedName));

// Check if the user has permission to delete from the target table
merge.getMergeCases().stream()
.filter(mergeCase -> mergeCase instanceof MergeDelete)
.findFirst()
.ifPresent(mergeCase -> accessControl.checkCanDeleteFromTable(session.getRequiredTransactionId(),
session.getIdentity(), session.getAccessControlContext(), targetTableQualifiedName));

// If there are any columns to update then verify the user has permission to update these columns.
if (!allUpdateColumnNames.isEmpty()) {
accessControl.checkCanUpdateTableColumns(session.getRequiredTransactionId(), session.getIdentity(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
import com.facebook.presto.sql.tree.LongLiteral;
import com.facebook.presto.sql.tree.Merge;
import com.facebook.presto.sql.tree.MergeCase;
import com.facebook.presto.sql.tree.MergeDelete;
import com.facebook.presto.sql.tree.MergeInsert;
import com.facebook.presto.sql.tree.MergeUpdate;
import com.facebook.presto.sql.tree.Node;
Expand Down Expand Up @@ -135,6 +136,7 @@
import static com.facebook.presto.common.type.TinyintType.TINYINT;
import static com.facebook.presto.common.type.VarbinaryType.VARBINARY;
import static com.facebook.presto.common.type.VarcharType.VARCHAR;
import static com.facebook.presto.spi.ConnectorMergeSink.DELETE_OPERATION_NUMBER;
import static com.facebook.presto.spi.ConnectorMergeSink.INSERT_OPERATION_NUMBER;
import static com.facebook.presto.spi.ConnectorMergeSink.UPDATE_OPERATION_NUMBER;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_LIMIT_CLAUSE;
Expand Down Expand Up @@ -727,6 +729,9 @@ private static Expression checkNotNullColumns(

private static int getMergeCaseOperationNumber(MergeCase mergeCase)
{
if (mergeCase instanceof MergeDelete) {
return DELETE_OPERATION_NUMBER;
}
if (mergeCase instanceof MergeInsert) {
return INSERT_OPERATION_NUMBER;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ mergeCase
: WHEN MATCHED THEN
UPDATE SET targetColumns+=identifier EQ values+=expression
(',' targetColumns+=identifier EQ values+=expression)* #mergeUpdate
| WHEN MATCHED THEN DELETE #mergeDelete
| WHEN NOT MATCHED THEN
INSERT ('(' columns+=identifier (',' columns+=identifier)* ')')?
VALUES '(' values+=expression (',' values+=expression)* ')' #mergeInsert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import com.facebook.presto.sql.tree.LikeClause;
import com.facebook.presto.sql.tree.Merge;
import com.facebook.presto.sql.tree.MergeCase;
import com.facebook.presto.sql.tree.MergeDelete;
import com.facebook.presto.sql.tree.MergeInsert;
import com.facebook.presto.sql.tree.MergeUpdate;
import com.facebook.presto.sql.tree.NaturalJoin;
Expand Down Expand Up @@ -759,6 +760,14 @@ protected Void visitMergeUpdate(MergeUpdate node, Integer indent)
return null;
}

@Override
protected Void visitMergeDelete(MergeDelete node, Integer indent)
{
appendMergeCaseWhen(true);
append(indent + 1, "DELETE");
return null;
}

private void appendMergeCaseWhen(boolean matched)
{
builder.append(matched ? "WHEN MATCHED" : "WHEN NOT MATCHED").append(" THEN\n");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
import com.facebook.presto.sql.tree.LongLiteral;
import com.facebook.presto.sql.tree.Merge;
import com.facebook.presto.sql.tree.MergeCase;
import com.facebook.presto.sql.tree.MergeDelete;
import com.facebook.presto.sql.tree.MergeInsert;
import com.facebook.presto.sql.tree.MergeUpdate;
import com.facebook.presto.sql.tree.NaturalJoin;
Expand Down Expand Up @@ -555,6 +556,12 @@ public Node visitMergeUpdate(SqlBaseParser.MergeUpdateContext context)
return new MergeUpdate(getLocation(context), assignments.build());
}

@Override
public Node visitMergeDelete(SqlBaseParser.MergeDeleteContext context)
{
return new MergeDelete(getLocation(context));
}

@Override
public Node visitRenameTable(SqlBaseParser.RenameTableContext context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,11 @@ protected R visitMergeUpdate(MergeUpdate node, C context)
return visitMergeCase(node, context);
}

protected R visitMergeDelete(MergeDelete node, C context)
{
return visitMergeCase(node, context);
}

protected R visitTableElement(TableElement node, C context)
{
return visitNode(node, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,12 @@ protected R visitMergeUpdate(MergeUpdate node, C context)
return null;
}

@Override
protected R visitMergeDelete(MergeDelete node, C context)
{
return null;
}

@Override
protected R visitCreateTableAsSelect(CreateTableAsSelect node, C context)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.presto.sql.tree;

import com.google.common.collect.ImmutableList;

import java.util.List;
import java.util.Optional;

public class MergeDelete
extends MergeCase
{
public MergeDelete()
{
this(Optional.empty());
}

public MergeDelete(NodeLocation location)
{
this(Optional.of(location));
}

public MergeDelete(Optional<NodeLocation> location)
{
super(location);
}

@Override
public <R, C> R accept(AstVisitor<R, C> visitor, C context)
{
return visitor.visitMergeDelete(this, context);
}

@Override
public List<Identifier> getSetColumns()
{
return ImmutableList.of();
}

@Override
public List<Expression> getSetExpressions()
{
return ImmutableList.of();
}

@Override
public List<? extends Node> getChildren()
{
return ImmutableList.of();
}

@Override
public int hashCode()
{
return MergeDelete.class.hashCode();
}

@Override
public boolean equals(Object obj)
{
if (this == obj) {
return true;
}
return obj != null && getClass() == obj.getClass();
}

@Override
public String toString()
{
return "MergeDelete{}";
}
}
Loading
Loading