diff --git a/presto-accumulo/src/test/java/com/facebook/presto/accumulo/TestAccumuloDistributedQueries.java b/presto-accumulo/src/test/java/com/facebook/presto/accumulo/TestAccumuloDistributedQueries.java index 55366a88abdd4..ed5a897afb45e 100644 --- a/presto-accumulo/src/test/java/com/facebook/presto/accumulo/TestAccumuloDistributedQueries.java +++ b/presto-accumulo/src/test/java/com/facebook/presto/accumulo/TestAccumuloDistributedQueries.java @@ -113,6 +113,12 @@ public void testDelete() // Deletes are not supported by the connector } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + @Override public void testInsert() { diff --git a/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java b/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java index 9de41a85b09c8..8ccde0ef7a9c9 100644 --- a/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java +++ b/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java @@ -18,6 +18,7 @@ import com.facebook.presto.common.transaction.TransactionId; import com.facebook.presto.common.type.Type; import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; import com.facebook.presto.spi.TableHandle; import com.facebook.presto.spi.analyzer.AccessControlInfo; import com.facebook.presto.spi.analyzer.AccessControlInfoForTable; @@ -168,6 +169,8 @@ public class Analysis private Optional refreshMaterializedViewAnalysis = Optional.empty(); private Optional analyzeTarget = Optional.empty(); + private Optional> updatedColumns = Optional.empty(); + // for describe input and describe output private final boolean isDescribe; @@ -684,6 +687,16 @@ public Optional getInsert() return insert; } + public void setUpdatedColumns(List updatedColumns) + { + this.updatedColumns = Optional.of(updatedColumns); + } + + public Optional> getUpdatedColumns() + { + return updatedColumns; + } + public void setRefreshMaterializedViewAnalysis(RefreshMaterializedViewAnalysis refreshMaterializedViewAnalysis) { this.refreshMaterializedViewAnalysis = Optional.of(refreshMaterializedViewAnalysis); diff --git a/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/utils/StatementUtils.java b/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/utils/StatementUtils.java index 193b7931a548e..a5df968b4c8b2 100644 --- a/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/utils/StatementUtils.java +++ b/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/utils/StatementUtils.java @@ -69,6 +69,7 @@ import com.facebook.presto.sql.tree.StartTransaction; import com.facebook.presto.sql.tree.Statement; import com.facebook.presto.sql.tree.TruncateTable; +import com.facebook.presto.sql.tree.Update; import com.facebook.presto.sql.tree.Use; import com.google.common.collect.ImmutableMap; @@ -98,6 +99,7 @@ private StatementUtils() {} builder.put(RefreshMaterializedView.class, QueryType.INSERT); builder.put(Delete.class, QueryType.DELETE); + builder.put(Update.class, QueryType.UPDATE); builder.put(ShowCatalogs.class, QueryType.DESCRIBE); builder.put(ShowCreate.class, QueryType.DESCRIBE); diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraDistributed.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraDistributed.java index 98dca40cfdcae..7307428bb0b50 100644 --- a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraDistributed.java +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraDistributed.java @@ -94,6 +94,12 @@ public void testDelete() // Cassandra connector currently does not support delete } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + @Override public void testShowColumns() { diff --git a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseDistributedQueries.java b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseDistributedQueries.java index 401f02bfba694..9a0ced08fcb4b 100755 --- a/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseDistributedQueries.java +++ b/presto-clickhouse/src/test/java/com/facebook/presto/plugin/clickhouse/TestClickHouseDistributedQueries.java @@ -108,6 +108,12 @@ public void testDelete() throw new SkipException("TODO: test not implemented yet"); } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + @Test @Override public void testInsert() diff --git a/presto-common/src/main/java/com/facebook/presto/common/resourceGroups/QueryType.java b/presto-common/src/main/java/com/facebook/presto/common/resourceGroups/QueryType.java index e0353c6989223..3a732de316454 100644 --- a/presto-common/src/main/java/com/facebook/presto/common/resourceGroups/QueryType.java +++ b/presto-common/src/main/java/com/facebook/presto/common/resourceGroups/QueryType.java @@ -27,6 +27,7 @@ public enum QueryType INSERT(6), SELECT(7), CONTROL(8), + UPDATE(9) /**/; private final int value; diff --git a/presto-docs/src/main/sphinx/develop.rst b/presto-docs/src/main/sphinx/develop.rst index 6c19038e10884..1aadcc6418143 100644 --- a/presto-docs/src/main/sphinx/develop.rst +++ b/presto-docs/src/main/sphinx/develop.rst @@ -10,6 +10,7 @@ This guide is intended for Presto contributors and plugin developers. develop/spi-overview develop/connectors develop/example-http + develop/delete-and-update develop/types develop/functions develop/system-access-control diff --git a/presto-docs/src/main/sphinx/develop/delete-and-update.rst b/presto-docs/src/main/sphinx/develop/delete-and-update.rst new file mode 100644 index 0000000000000..9e31630eaa5c0 --- /dev/null +++ b/presto-docs/src/main/sphinx/develop/delete-and-update.rst @@ -0,0 +1,181 @@ +==================================== +Supporting ``DELETE`` and ``UPDATE`` +==================================== + +The Presto engine provides APIs to support row-level SQL ``DELETE`` and ``UPDATE``. +To implement ``DELETE`` or ``UPDATE``, a connector must: + +* Layer an ``UpdatablePageSource`` on top of the connector's ``ConnectorPageSource`` +* Define ``ConnectorMetadata`` methods to get a rowId column handle +* Start the operation using ``beginUpdate()`` or ``beginDelete()`` +* Finish the operation using ``finishUpdate()`` or ``finishDelete()`` + +``DELETE`` and ``UPDATE`` Data Flow +=================================== + +``DELETE`` and ``UPDATE`` have a similar flow: + +* For each split, the connector will create an ``UpdatablePageSource`` instance, layered over the + connector's ``ConnectorPageSource``, to read pages on behalf of the Presto engine, and to + write deletions or updates to the underlying data store. +* The connector's ``UpdatablePageSource.getNextPage()`` implementation fetches the next page + from the underlying ``ConnectorPageSource``, optionally reformats the page, and returns it + to the Presto engine. +* The Presto engine performs filtering and projection on the page read, producing a page of filtered, + projected results. +* The Presto engine passes that filtered, projected page of results to the connector's + ``UpdatablePageSource`` ``deleteRows()`` or ``updateRows()`` method. Those methods persist + the deletions or updates in the underlying data store. +* When all the pages for a specific split have been processed, the Presto engine calls + ``UpdatablePageSource.finish()``, which returns a ``Collection`` of fragments + representing connector-specific information about the rows processed by the calls to + ``deleteRows`` or ``updateRows``. +* When all pages for all splits have been processed, the Presto engine calls ``ConnectorMetadata.finishDelete()`` or + ``finishUpdate``, passing a collection containing all the fragments from all the splits. The connector + does what is required to finalize the operation, for example, committing the transaction. + +The rowId Column Handle Abstraction +=================================== + +The Presto engine and connectors use a rowId column handle abstraction to agree on the identities of rows +to be updated or deleted. The rowId column handle is opaque to the Presto engine. Depending on the connector, +the rowId column handle abstraction could represent several physical columns. + +The rowId Column Handle for ``DELETE`` +-------------------------------------- + +The Presto engine identifies the rows to be deleted using a connector-specific +rowId column handle, returned by the connector's ``ConnectorMetadata.getDeleteRowIdColumnHandle()`` +method, whose full signature is:: + + ColumnHandle getDeleteRowIdColumnHandle( + ConnectorSession session, + ConnectorTableHandle tableHandle) + +The rowId Column Handle for ``UPDATE`` +-------------------------------------- + +The Presto engine identifies rows to be updated using a connector-specific rowId column handle, +returned by the connector's ``ConnectorMetadata.getUpdateRowIdColumnHandle()`` +method. In addition to the columns that identify the row, for ``UPDATE`` the rowId column will contain +any columns that the connector requires in order to perform the ``UPDATE`` operation. + +UpdatablePageSource API +======================= + +As mentioned above, to support ``DELETE`` or ``UPDATE``, the connector must define a subclass of +``UpdatablePageSource``, layered over the connector's ``ConnectorPageSource``. The interesting methods are: + +* ``Page getNextPage()``. When the Presto engine calls ``getNextPage()``, the ``UpdatablePageSource`` calls + its underlying ``ConnectorPageSource.getNextPage()`` method to get a page. Some connectors will reformat + the page before returning it to the Presto engine. + +* ``void deleteRows(Block rowIds)``. The Presto engine calls the ``deleteRows()`` method of the same ``UpdatablePageSource`` + instance that supplied the original page, passing a block of rowIds, created by the Presto engine based on the column + handle returned by ``ConnectorMetadata.getDeleteRowIdColumnHandle()`` + +* ``void updateRows(Page page, List columnValueAndRowIdChannels)``. The Presto engine calls the ``updateRows()`` + method of the same ``UpdatablePageSource`` instance that supplied the original page, passing a page of projected columns, + one for each updated column and the last one for the rowId column. The order of projected columns is defined by the Presto engine, + and that order is reflected in the ``columnValueAndRowIdChannels`` argument. The job of ``updateRows()`` is to: + + * Extract the updated column blocks and the rowId block from the projected page. + * Assemble them in whatever order is required by the connector for storage. + * Store the update result in the underlying file store. + +* ``CompletableFuture> finish()``. The Presto engine calls ``finish()`` when all the pages + of a split have been processed. The connector returns a future containing a collection of ``Slice``, representing + connector-specific information about the rows processed. Usually this will include the row count, and might + include information like the files or partitions created or changed. + +``ConnectorMetadata`` ``DELETE`` API +==================================== + +A connector implementing ``DELETE`` must specify three ``ConnectorMetadata`` methods. + +* ``getDeleteRowIdColumnHandle()``:: + + ColumnHandle getDeleteRowIdColumnHandle( + ConnectorSession session, + ConnectorTableHandle tableHandle) + + The ColumnHandle returned by this method provides the rowId column handle used by the connector to identify rows + to be deleted, as well as any other fields of the row that the connector will need to complete the ``DELETE`` operation. + For a JDBC connector, that rowId is usually the primary key for the table and no other fields are required. + For other connectors, the information needed to identify a row usually consists of multiple physical columns. + +* ``beginDelete()``:: + + ConnectorTableHandle beginDelete( + ConnectorSession session, + ConnectorTableHandle tableHandle) + + As the last step in creating the ``DELETE`` execution plan, the connector's ``beginDelete()`` method is called, + passing the ``session`` and ``tableHandle``. + + ``beginDelete()`` performs any orchestration needed in the connector to start processing the ``DELETE``. + This orchestration varies from connector to connector. + + ``beginDelete()`` returns a ``ConnectorTableHandle`` with any added information the connector needs when the handle + is passed back to ``finishDelete()`` and the split generation machinery. For most connectors, the returned table + handle contains a flag identifying the table handle as a table handle for a ``DELETE`` operation. + +* ``finishDelete()``:: + + void finishDelete( + ConnectorSession session, + ConnectorTableHandle tableHandle, + Collection fragments) + + During ``DELETE`` processing, the Presto engine accumulates the ``Slice`` collections returned by ``UpdatablePageSource.finish()``. + After all splits have been processed, the engine calls ``finishDelete()``, passing the table handle and that + collection of ``Slice`` fragments. In response, the connector takes appropriate actions to complete the ``Delete`` operation. + Those actions might include committing the transaction, assuming the connector supports a transaction paradigm. + +``ConnectorMetadata`` ``UPDATE`` API +==================================== + +A connector implementing ``UPDATE`` must specify three ``ConnectorMetadata`` methods. + +* ``getUpdateRowIdColumnHandle``:: + + ColumnHandle getUpdateRowIdColumnHandle( + ConnectorSession session, + ConnectorTableHandle tableHandle, + List updatedColumns) + + The ``updatedColumns`` list contains column handles for all columns updated by the ``UPDATE`` operation in table column order. + + The ColumnHandle returned by this method provides the rowId used by the connector to identify rows to be updated, as + well as any other fields of the row that the connector will need to complete the ``UPDATE`` operation. + +* ``beginUpdate``:: + + ConnectorTableHandle beginUpdate( + ConnectorSession session, + ConnectorTableHandle tableHandle, + List updatedColumns) + + As the last step in creating the ``UPDATE`` execution plan, the connector's ``beginUpdate()`` method is called, + passing arguments that define the ``UPDATE`` to the connector. In addition to the ``session`` + and ``tableHandle``, the arguments includes the list of the updated columns handles, in table column order. + + ``beginUpdate()`` performs any orchestration needed in the connector to start processing the ``UPDATE``. + This orchestration varies from connector to connector. + + ``beginUpdate`` returns a ``ConnectorTableHandle`` with any added information the connector needs when the handle + is passed back to ``finishUpdate()`` and the split generation machinery. For most connectors, the returned table + handle contains a flag identifying the table handle as a table handle for a ``UPDATE`` operation. For some connectors + that support partitioning, the table handle will reflect that partitioning. + +* ``finishUpdate``:: + + void finishUpdate( + ConnectorSession session, + ConnectorTableHandle tableHandle, + Collection fragments) + + During ``UPDATE`` processing, the Presto engine accumulates the ``Slice`` collections returned by ``UpdatablePageSource.finish()``. + After all splits have been processed, the engine calls ``finishUpdate()``, passing the table handle and that + collection of ``Slice`` fragments. In response, the connector takes appropriate actions to complete the ``UPDATE`` operation. + Those actions might include committing the transaction, assuming the connector supports a transaction paradigm. diff --git a/presto-docs/src/main/sphinx/sql.rst b/presto-docs/src/main/sphinx/sql.rst index 23fab43e6891c..88a4613a27512 100644 --- a/presto-docs/src/main/sphinx/sql.rst +++ b/presto-docs/src/main/sphinx/sql.rst @@ -58,5 +58,6 @@ This chapter describes the SQL syntax used in Presto. sql/show-tables sql/start-transaction sql/truncate + sql/update sql/use sql/values diff --git a/presto-docs/src/main/sphinx/sql/update.rst b/presto-docs/src/main/sphinx/sql/update.rst new file mode 100644 index 0000000000000..6f4e7e0293dbb --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/update.rst @@ -0,0 +1,33 @@ +====== +UPDATE +====== + +Synopsis +-------- + +.. code-block:: text + UPDATE table_name SET [ ( column = expression [, ... ] ) ] [ WHERE condition ] +Description +----------- + +Update selected columns values in existing rows in a table. + +The columns named in the ``column = expression`` assignments will be updated +for all rows that match the ``WHERE`` condition. The values of all column update +expressions for a matching row are evaluated before any column value is changed. +When the type of the expression and the type of the column differ, the usual implicit +CASTs, such as widening numeric fields, are applied to the ``UPDATE`` expression values. + + +Examples +-------- + +Update the status of all purchases that haven't been assigned a ship date:: + + UPDATE purchases SET status = 'OVERDUE' WHERE ship_date IS NULL; + +Update the account manager and account assign date for all customers:: + + UPDATE customers SET + account_manager = 'John Henry', + assign_date = DATE '2007-01-01'; diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetadata.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetadata.java index b42d511ae62d6..f2fd669d18085 100644 --- a/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetadata.java +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetadata.java @@ -2484,7 +2484,7 @@ public ConnectorTableHandle beginDelete(ConnectorSession session, ConnectorTable } @Override - public ColumnHandle getUpdateRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) + public ColumnHandle getDeleteRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) { return updateRowIdHandle(); } diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/security/LegacyAccessControl.java b/presto-hive/src/main/java/com/facebook/presto/hive/security/LegacyAccessControl.java index e3820e8f03298..0c5a3afcde723 100644 --- a/presto-hive/src/main/java/com/facebook/presto/hive/security/LegacyAccessControl.java +++ b/presto-hive/src/main/java/com/facebook/presto/hive/security/LegacyAccessControl.java @@ -179,6 +179,11 @@ public void checkCanDeleteFromTable(ConnectorTransactionHandle transaction, Conn { } + @Override + public void checkCanUpdateTableColumns(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + } + @Override public void checkCanCreateView(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName viewName) { diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/security/SqlStandardAccessControl.java b/presto-hive/src/main/java/com/facebook/presto/hive/security/SqlStandardAccessControl.java index e1364e860afdd..94e9eb12652ea 100644 --- a/presto-hive/src/main/java/com/facebook/presto/hive/security/SqlStandardAccessControl.java +++ b/presto-hive/src/main/java/com/facebook/presto/hive/security/SqlStandardAccessControl.java @@ -42,6 +42,7 @@ import static com.facebook.presto.hive.metastore.HivePrivilegeInfo.HivePrivilege.INSERT; import static com.facebook.presto.hive.metastore.HivePrivilegeInfo.HivePrivilege.OWNERSHIP; import static com.facebook.presto.hive.metastore.HivePrivilegeInfo.HivePrivilege.SELECT; +import static com.facebook.presto.hive.metastore.HivePrivilegeInfo.HivePrivilege.UPDATE; import static com.facebook.presto.hive.metastore.HivePrivilegeInfo.toHivePrivilege; import static com.facebook.presto.hive.metastore.thrift.ThriftMetastoreUtil.isRoleApplicable; import static com.facebook.presto.hive.metastore.thrift.ThriftMetastoreUtil.isRoleEnabled; @@ -73,6 +74,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denySetRole; import static com.facebook.presto.spi.security.AccessDeniedException.denyShowRoles; import static com.facebook.presto.spi.security.AccessDeniedException.denyTruncateTable; +import static com.facebook.presto.spi.security.AccessDeniedException.denyUpdateTableColumns; import static com.facebook.presto.spi.security.PrincipalType.ROLE; import static com.facebook.presto.spi.security.PrincipalType.USER; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -239,6 +241,15 @@ public void checkCanTruncateTable(ConnectorTransactionHandle transaction, Connec } } + @Override + public void checkCanUpdateTableColumns(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + MetastoreContext metastoreContext = new MetastoreContext(identity, context.getQueryId().getId(), context.getClientInfo(), context.getSource(), Optional.empty(), false, HiveColumnConverterProvider.DEFAULT_COLUMN_CONVERTER_PROVIDER); + if (!checkTablePermission(transaction, identity, metastoreContext, tableName, UPDATE, false)) { + denyUpdateTableColumns(tableName.toString(), updatedColumns); + } + } + @Override public void checkCanCreateView(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName viewName) { diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/security/SystemTableAwareAccessControl.java b/presto-hive/src/main/java/com/facebook/presto/hive/security/SystemTableAwareAccessControl.java index 79c44c4a36132..34fbc9c54dd64 100644 --- a/presto-hive/src/main/java/com/facebook/presto/hive/security/SystemTableAwareAccessControl.java +++ b/presto-hive/src/main/java/com/facebook/presto/hive/security/SystemTableAwareAccessControl.java @@ -154,6 +154,12 @@ public void checkCanDeleteFromTable(ConnectorTransactionHandle transactionHandle delegate.checkCanDeleteFromTable(transactionHandle, identity, context, tableName); } + @Override + public void checkCanUpdateTableColumns(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + delegate.checkCanUpdateTableColumns(transactionHandle, identity, context, tableName, updatedColumns); + } + @Override public void checkCanCreateView(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName viewName) { diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueries.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueries.java index f76c8862786f8..1c69802371710 100644 --- a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueries.java +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueries.java @@ -80,6 +80,12 @@ public void testDelete() // Hive connector currently does not support row-by-row delete } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + @Test public void testExplainOfCreateTableAs() { diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithExchangeMaterialization.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithExchangeMaterialization.java index 454f010d17f51..aa19c2aa6be2d 100644 --- a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithExchangeMaterialization.java +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithExchangeMaterialization.java @@ -189,6 +189,12 @@ public void testDelete() // Hive connector currently does not support row-by-row delete } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + @Override public void testExcept() { diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithOptimizedRepartitioning.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithOptimizedRepartitioning.java index bd80cbeb30ef7..23bb14d91eb06 100644 --- a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithOptimizedRepartitioning.java +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithOptimizedRepartitioning.java @@ -49,5 +49,11 @@ public void testDelete() // Hive connector currently does not support row-by-row delete } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + // Hive specific tests should normally go in TestHiveIntegrationSmokeTest } diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithThriftRpc.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithThriftRpc.java index f484e37e0b76f..a7d87ff709a88 100644 --- a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithThriftRpc.java +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueriesWithThriftRpc.java @@ -49,5 +49,11 @@ public void testDelete() // Hive connector currently does not support row-by-row delete } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + // Hive specific tests should normally go in TestHiveIntegrationSmokeTest } diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHivePushdownDistributedQueries.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHivePushdownDistributedQueries.java index ae4ad303ec937..2ad72fa1f7883 100644 --- a/presto-hive/src/test/java/com/facebook/presto/hive/TestHivePushdownDistributedQueries.java +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHivePushdownDistributedQueries.java @@ -57,6 +57,12 @@ public void testDelete() // Hive connector currently does not support row-by-row delete } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + @Test public void testExplainOfCreateTableAs() { diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestParquetDistributedQueries.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestParquetDistributedQueries.java index 41370f865108c..2db096b8318df 100644 --- a/presto-hive/src/test/java/com/facebook/presto/hive/TestParquetDistributedQueries.java +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestParquetDistributedQueries.java @@ -118,6 +118,12 @@ public void testDelete() // Hive connector currently does not support row-by-row delete } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + @Override public void testRenameColumn() { diff --git a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergAbstractMetadata.java b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergAbstractMetadata.java index 5e67f26d9e68b..b6c983cd23ab4 100644 --- a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergAbstractMetadata.java +++ b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergAbstractMetadata.java @@ -414,7 +414,7 @@ public Optional finishInsert(ConnectorSession session, } @Override - public ColumnHandle getUpdateRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) + public ColumnHandle getDeleteRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) { return primitiveIcebergColumnHandle(0, "$row_id", BIGINT, Optional.empty()); } diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java index 076e2d0d36ab5..21843ab8955b4 100644 --- a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java +++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java @@ -148,6 +148,12 @@ public void testDelete() assertQuerySucceeds("DROP TABLE test_partitioned_drop"); } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + @Test public void testDeleteWithPartitionSpecEvolution() { diff --git a/presto-kudu/src/main/java/com/facebook/presto/kudu/KuduMetadata.java b/presto-kudu/src/main/java/com/facebook/presto/kudu/KuduMetadata.java index a90817169251f..ff6ced78b19e4 100755 --- a/presto-kudu/src/main/java/com/facebook/presto/kudu/KuduMetadata.java +++ b/presto-kudu/src/main/java/com/facebook/presto/kudu/KuduMetadata.java @@ -351,7 +351,7 @@ public Optional finishCreateTable(ConnectorSession sess } @Override - public ColumnHandle getUpdateRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) + public ColumnHandle getDeleteRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) { return KuduColumnHandle.ROW_ID_HANDLE; } diff --git a/presto-main/src/main/java/com/facebook/presto/execution/scheduler/ExecutionWriterTarget.java b/presto-main/src/main/java/com/facebook/presto/execution/scheduler/ExecutionWriterTarget.java index 0f17b45fd3c4d..248bfd2def3ef 100644 --- a/presto-main/src/main/java/com/facebook/presto/execution/scheduler/ExecutionWriterTarget.java +++ b/presto-main/src/main/java/com/facebook/presto/execution/scheduler/ExecutionWriterTarget.java @@ -30,7 +30,8 @@ @JsonSubTypes.Type(value = ExecutionWriterTarget.CreateHandle.class, name = "CreateHandle"), @JsonSubTypes.Type(value = ExecutionWriterTarget.InsertHandle.class, name = "InsertHandle"), @JsonSubTypes.Type(value = ExecutionWriterTarget.DeleteHandle.class, name = "DeleteHandle"), - @JsonSubTypes.Type(value = ExecutionWriterTarget.RefreshMaterializedViewHandle.class, name = "RefreshMaterializedViewHandle")}) + @JsonSubTypes.Type(value = ExecutionWriterTarget.RefreshMaterializedViewHandle.class, name = "RefreshMaterializedViewHandle"), + @JsonSubTypes.Type(value = ExecutionWriterTarget.UpdateHandle.class, name = "UpdateHandle")}) @SuppressWarnings({"EmptyClass", "ClassMayBeInterface"}) public abstract class ExecutionWriterTarget { @@ -169,4 +170,38 @@ public String toString() return handle.toString(); } } + + public static class UpdateHandle + extends ExecutionWriterTarget + { + private final TableHandle handle; + private final SchemaTableName schemaTableName; + + @JsonCreator + public UpdateHandle( + @JsonProperty("handle") TableHandle handle, + @JsonProperty("schemaTableName") SchemaTableName schemaTableName) + { + this.handle = requireNonNull(handle, "handle is null"); + this.schemaTableName = requireNonNull(schemaTableName, "schemaTableName is null"); + } + + @JsonProperty + public TableHandle getHandle() + { + return handle; + } + + @JsonProperty + public SchemaTableName getSchemaTableName() + { + return schemaTableName; + } + + @Override + public String toString() + { + return handle.toString(); + } + } } diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/DelegatingMetadataManager.java b/presto-main/src/main/java/com/facebook/presto/metadata/DelegatingMetadataManager.java index eabead9016168..74de592712fdc 100644 --- a/presto-main/src/main/java/com/facebook/presto/metadata/DelegatingMetadataManager.java +++ b/presto-main/src/main/java/com/facebook/presto/metadata/DelegatingMetadataManager.java @@ -360,9 +360,15 @@ public Optional finishInsert( } @Override - public ColumnHandle getUpdateRowIdColumnHandle(Session session, TableHandle tableHandle) + public ColumnHandle getDeleteRowIdColumnHandle(Session session, TableHandle tableHandle) { - return delegate.getUpdateRowIdColumnHandle(session, tableHandle); + return delegate.getDeleteRowIdColumnHandle(session, tableHandle); + } + + @Override + public ColumnHandle getUpdateRowIdColumnHandle(Session session, TableHandle tableHandle, List updatedColumns) + { + return delegate.getUpdateRowIdColumnHandle(session, tableHandle, updatedColumns); } @Override @@ -389,6 +395,18 @@ public void finishDelete(Session session, TableHandle tableHandle, Collection updatedColumns) + { + return delegate.beginUpdate(session, tableHandle, updatedColumns); + } + + @Override + public void finishUpdate(Session session, TableHandle tableHandle, Collection fragments) + { + delegate.finishUpdate(session, tableHandle, fragments); + } + @Override public Optional getCatalogHandle(Session session, String catalogName) { diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/Metadata.java b/presto-main/src/main/java/com/facebook/presto/metadata/Metadata.java index 5f8272b82ca26..3d29324f55c05 100644 --- a/presto-main/src/main/java/com/facebook/presto/metadata/Metadata.java +++ b/presto-main/src/main/java/com/facebook/presto/metadata/Metadata.java @@ -298,10 +298,15 @@ public interface Metadata */ Optional finishInsert(Session session, InsertTableHandle tableHandle, Collection fragments, Collection computedStatistics); + /** + * Get the row ID column handle used with UpdatablePageSource#deleteRows. + */ + ColumnHandle getDeleteRowIdColumnHandle(Session session, TableHandle tableHandle); + /** * Get the row ID column handle used with UpdatablePageSource. */ - ColumnHandle getUpdateRowIdColumnHandle(Session session, TableHandle tableHandle); + ColumnHandle getUpdateRowIdColumnHandle(Session session, TableHandle tableHandle, List updatedColumns); /** * @return whether delete without table scan is supported @@ -325,6 +330,16 @@ public interface Metadata */ void finishDelete(Session session, TableHandle tableHandle, Collection fragments); + /** + * Begin update query + */ + TableHandle beginUpdate(Session session, TableHandle tableHandle, List updatedColumns); + + /** + * Finish update query + */ + void finishUpdate(Session session, TableHandle tableHandle, Collection fragments); + /** * Returns a connector id for the specified catalog name. */ diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/MetadataManager.java b/presto-main/src/main/java/com/facebook/presto/metadata/MetadataManager.java index 57332fd832470..e9ceb161e7ff1 100644 --- a/presto-main/src/main/java/com/facebook/presto/metadata/MetadataManager.java +++ b/presto-main/src/main/java/com/facebook/presto/metadata/MetadataManager.java @@ -866,11 +866,19 @@ public Optional finishInsert(Session session, InsertTab } @Override - public ColumnHandle getUpdateRowIdColumnHandle(Session session, TableHandle tableHandle) + public ColumnHandle getDeleteRowIdColumnHandle(Session session, TableHandle tableHandle) { ConnectorId connectorId = tableHandle.getConnectorId(); ConnectorMetadata metadata = getMetadata(session, connectorId); - return metadata.getUpdateRowIdColumnHandle(session.toConnectorSession(connectorId), tableHandle.getConnectorHandle()); + return metadata.getDeleteRowIdColumnHandle(session.toConnectorSession(connectorId), tableHandle.getConnectorHandle()); + } + + @Override + public ColumnHandle getUpdateRowIdColumnHandle(Session session, TableHandle tableHandle, List updatedColumns) + { + ConnectorId connectorId = tableHandle.getConnectorId(); + ConnectorMetadata metadata = getMetadata(session, connectorId); + return metadata.getUpdateRowIdColumnHandle(session.toConnectorSession(connectorId), tableHandle.getConnectorHandle(), updatedColumns); } @Override @@ -913,6 +921,23 @@ public void finishDelete(Session session, TableHandle tableHandle, Collection updatedColumns) + { + ConnectorId connectorId = tableHandle.getConnectorId(); + ConnectorMetadata metadata = getMetadataForWrite(session, connectorId); + ConnectorTableHandle newHandle = metadata.beginUpdate(session.toConnectorSession(connectorId), tableHandle.getConnectorHandle(), updatedColumns); + return new TableHandle(tableHandle.getConnectorId(), newHandle, tableHandle.getTransaction(), tableHandle.getLayout()); + } + + @Override + public void finishUpdate(Session session, TableHandle tableHandle, Collection fragments) + { + ConnectorId connectorId = tableHandle.getConnectorId(); + ConnectorMetadata metadata = getMetadata(session, connectorId); + metadata.finishUpdate(session.toConnectorSession(connectorId), tableHandle.getConnectorHandle(), fragments); + } + @Override public Optional getCatalogHandle(Session session, String catalogName) { diff --git a/presto-main/src/main/java/com/facebook/presto/operator/AbstractRowChangeOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/AbstractRowChangeOperator.java new file mode 100644 index 0000000000000..bbf7431e3f531 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/AbstractRowChangeOperator.java @@ -0,0 +1,176 @@ +/* + * 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.operator; + +import com.facebook.airlift.json.JsonCodec; +import com.facebook.presto.common.Page; +import com.facebook.presto.common.block.BlockBuilder; +import com.facebook.presto.common.block.RunLengthEncodedBlock; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.spi.UpdatablePageSource; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.slice.Slice; + +import java.util.Collection; +import java.util.Optional; +import java.util.function.Supplier; + +import static com.facebook.airlift.concurrent.MoreFutures.getFutureValue; +import static com.facebook.airlift.concurrent.MoreFutures.toListenableFuture; +import static com.facebook.presto.common.type.BigintType.BIGINT; +import static com.facebook.presto.common.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.operator.PageSinkCommitStrategy.NO_COMMIT; +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.slice.Slices.wrappedBuffer; +import static java.util.Objects.requireNonNull; + +public abstract class AbstractRowChangeOperator + implements Operator +{ + protected enum State + { + RUNNING, FINISHING, FINISHED + } + + private final OperatorContext operatorContext; + + protected State state = State.RUNNING; + protected long rowCount; + private boolean closed; + private ListenableFuture> finishFuture; + private Supplier> pageSource = Optional::empty; + private final JsonCodec tableCommitContextCodec; + + public AbstractRowChangeOperator(OperatorContext operatorContext, JsonCodec tableCommitContextCodec) + { + this.operatorContext = requireNonNull(operatorContext, "operatorContext is null"); + this.tableCommitContextCodec = requireNonNull(tableCommitContextCodec, "tableCommitContextCodec is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public void finish() + { + if (state == State.RUNNING) { + state = State.FINISHING; + finishFuture = toListenableFuture(pageSource().finish()); + } + } + + @Override + public boolean isFinished() + { + return state == State.FINISHED; + } + + @Override + public boolean needsInput() + { + return state == State.RUNNING; + } + + @Override + public abstract void addInput(Page page); + + @Override + public ListenableFuture isBlocked() + { + if (finishFuture == null) { + return NOT_BLOCKED; + } + return finishFuture; + } + + @Override + public Page getOutput() + { + if ((state != State.FINISHING) || !finishFuture.isDone()) { + return null; + } + state = State.FINISHED; + + // There are three channels in the output page of DeleteOperator + // 1. Row count (BIGINT) + // 2. Delete fragments (VARBINARY) + // 3. Table commit context (VARBINARY) + // + // Page layout: + // + // row fragments context + // X null X + // null X X + // null X X + // null X X + // ... + + Collection fragments = getFutureValue(finishFuture); + int positionCount = fragments.size() + 1; + + // Output page will only be constructed once, and the table commit context channel will be constructed using RunLengthEncodedBlock. + // Thus individual BlockBuilder is used for each channel, instead of using PageBuilder. + BlockBuilder rowsBuilder = BIGINT.createBlockBuilder(null, positionCount); + BlockBuilder fragmentBuilder = VARBINARY.createBlockBuilder(null, positionCount); + + // write row count + rowsBuilder.writeLong(rowCount); + fragmentBuilder.appendNull(); + + // write fragments + for (Slice fragment : fragments) { + rowsBuilder.appendNull(); + VARBINARY.writeSlice(fragmentBuilder, fragment); + } + + // create table commit context + TaskId taskId = operatorContext.getDriverContext().getPipelineContext().getTaskId(); + Slice tableCommitContext = wrappedBuffer(tableCommitContextCodec.toJsonBytes( + new TableCommitContext( + operatorContext.getDriverContext().getLifespan(), + taskId, + NO_COMMIT, + true))); + return new Page(positionCount, rowsBuilder.build(), fragmentBuilder.build(), RunLengthEncodedBlock.create(VARBINARY, tableCommitContext, positionCount)); + } + + @Override + public void close() + { + if (!closed) { + closed = true; + if (finishFuture != null) { + finishFuture.cancel(true); + } + else { + pageSource.get().ifPresent(UpdatablePageSource::abort); + } + } + } + + public void setPageSource(Supplier> pageSource) + { + this.pageSource = requireNonNull(pageSource, "pageSource is null"); + } + + protected UpdatablePageSource pageSource() + { + Optional source = pageSource.get(); + checkState(source.isPresent(), "UpdatablePageSource not set"); + return source.get(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/DeleteOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/DeleteOperator.java index 9273d78ba2dff..6d785e28c1b31 100644 --- a/presto-main/src/main/java/com/facebook/presto/operator/DeleteOperator.java +++ b/presto-main/src/main/java/com/facebook/presto/operator/DeleteOperator.java @@ -16,29 +16,13 @@ import com.facebook.airlift.json.JsonCodec; import com.facebook.presto.common.Page; import com.facebook.presto.common.block.Block; -import com.facebook.presto.common.block.BlockBuilder; -import com.facebook.presto.common.block.RunLengthEncodedBlock; -import com.facebook.presto.execution.TaskId; -import com.facebook.presto.spi.UpdatablePageSource; import com.facebook.presto.spi.plan.PlanNodeId; -import com.google.common.util.concurrent.ListenableFuture; -import io.airlift.slice.Slice; -import java.util.Collection; -import java.util.Optional; -import java.util.function.Supplier; - -import static com.facebook.airlift.concurrent.MoreFutures.getFutureValue; -import static com.facebook.airlift.concurrent.MoreFutures.toListenableFuture; -import static com.facebook.presto.common.type.BigintType.BIGINT; -import static com.facebook.presto.common.type.VarbinaryType.VARBINARY; -import static com.facebook.presto.operator.PageSinkCommitStrategy.NO_COMMIT; import static com.google.common.base.Preconditions.checkState; -import static io.airlift.slice.Slices.wrappedBuffer; import static java.util.Objects.requireNonNull; public class DeleteOperator - implements Operator + extends AbstractRowChangeOperator { public static class DeleteOperatorFactory implements OperatorFactory @@ -78,53 +62,12 @@ public OperatorFactory duplicate() } } - private enum State - { - RUNNING, FINISHING, FINISHED - } - - private final OperatorContext operatorContext; private final int rowIdChannel; - private final JsonCodec tableCommitContextCodec; - - private State state = State.RUNNING; - private long rowCount; - private boolean closed; - private ListenableFuture> finishFuture; - private Supplier> pageSource = Optional::empty; public DeleteOperator(OperatorContext operatorContext, int rowIdChannel, JsonCodec tableCommitContextCodec) { - this.operatorContext = requireNonNull(operatorContext, "operatorContext is null"); + super(operatorContext, tableCommitContextCodec); this.rowIdChannel = rowIdChannel; - this.tableCommitContextCodec = requireNonNull(tableCommitContextCodec, "tableCommitContextCodec is null"); - } - - @Override - public OperatorContext getOperatorContext() - { - return operatorContext; - } - - @Override - public void finish() - { - if (state == State.RUNNING) { - state = State.FINISHING; - finishFuture = toListenableFuture(pageSource().finish()); - } - } - - @Override - public boolean isFinished() - { - return state == State.FINISHED; - } - - @Override - public boolean needsInput() - { - return state == State.RUNNING; } @Override @@ -137,90 +80,4 @@ public void addInput(Page page) pageSource().deleteRows(rowIds); rowCount += rowIds.getPositionCount(); } - - @Override - public ListenableFuture isBlocked() - { - if (finishFuture == null) { - return NOT_BLOCKED; - } - return finishFuture; - } - - @Override - public Page getOutput() - { - if ((state != State.FINISHING) || !finishFuture.isDone()) { - return null; - } - state = State.FINISHED; - - // There are three channels in the output page of DeleteOperator - // 1. Row count (BIGINT) - // 2. Delete fragments (VARBINARY) - // 3. Table commit context (VARBINARY) - // - // Page layout: - // - // row fragments context - // X null X - // null X X - // null X X - // null X X - // ... - Collection fragments = getFutureValue(finishFuture); - int positionCount = fragments.size() + 1; - - // Output page will only be constructed once, and the table commit context channel will be constructed using RunLengthEncodedBlock. - // Thus individual BlockBuilder is used for each channel, instead of using PageBuilder. - BlockBuilder rowsBuilder = BIGINT.createBlockBuilder(null, positionCount); - BlockBuilder fragmentBuilder = VARBINARY.createBlockBuilder(null, positionCount); - - // write row count - rowsBuilder.writeLong(rowCount); - fragmentBuilder.appendNull(); - - // write fragments - for (Slice fragment : fragments) { - rowsBuilder.appendNull(); - VARBINARY.writeSlice(fragmentBuilder, fragment); - } - - // create table commit context - TaskId taskId = operatorContext.getDriverContext().getPipelineContext().getTaskId(); - Slice tableCommitContext = wrappedBuffer(tableCommitContextCodec.toJsonBytes( - new TableCommitContext( - operatorContext.getDriverContext().getLifespan(), - taskId, - NO_COMMIT, - true))); - - return new Page(positionCount, rowsBuilder.build(), fragmentBuilder.build(), RunLengthEncodedBlock.create(VARBINARY, tableCommitContext, positionCount)); - } - - @Override - public void close() - { - if (!closed) { - closed = true; - if (finishFuture != null) { - finishFuture.cancel(true); - } - else { - pageSource.get().ifPresent(UpdatablePageSource::abort); - } - } - } - - public void setPageSource(Supplier> pageSource) - { - this.pageSource = requireNonNull(pageSource, "pageSource is null"); - } - - private UpdatablePageSource pageSource() - { - Optional source = pageSource.get(); - checkState(source.isPresent(), "UpdatablePageSource not set"); - return source.get(); - } } diff --git a/presto-main/src/main/java/com/facebook/presto/operator/Driver.java b/presto-main/src/main/java/com/facebook/presto/operator/Driver.java index cda250b3c8901..123a582409c87 100644 --- a/presto-main/src/main/java/com/facebook/presto/operator/Driver.java +++ b/presto-main/src/main/java/com/facebook/presto/operator/Driver.java @@ -81,6 +81,7 @@ public class Driver private final List allOperators; private final Optional sourceOperator; private final Optional deleteOperator; + private final Optional updateOperator; // This variable acts as a staging area. When new splits (encapsulated in TaskSource) are // provided to a Driver, the Driver will not process them right away. Instead, the splits are @@ -140,6 +141,7 @@ private Driver(DriverContext driverContext, List operators) Optional sourceOperator = Optional.empty(); Optional deleteOperator = Optional.empty(); + Optional updateOperator = Optional.empty(); for (Operator operator : operators) { if (operator instanceof SourceOperator) { checkArgument(!sourceOperator.isPresent(), "There must be at most one SourceOperator"); @@ -149,9 +151,14 @@ else if (operator instanceof DeleteOperator) { checkArgument(!deleteOperator.isPresent(), "There must be at most one DeleteOperator"); deleteOperator = Optional.of((DeleteOperator) operator); } + else if (operator instanceof UpdateOperator) { + checkArgument(!updateOperator.isPresent(), "There must be at most one UpdateOperator"); + updateOperator = Optional.of((UpdateOperator) operator); + } } this.sourceOperator = sourceOperator; this.deleteOperator = deleteOperator; + this.updateOperator = updateOperator; currentTaskSource = sourceOperator.map(operator -> new TaskSource(operator.getSourceId(), ImmutableSet.of(), false)).orElse(null); // initially the driverBlockedFuture is not blocked (it is completed) @@ -277,6 +284,7 @@ private void processNewSources() Supplier> pageSource = sourceOperator.addSplit(newSplit); deleteOperator.ifPresent(deleteOperator -> deleteOperator.setPageSource(pageSource)); + updateOperator.ifPresent(updateOperator -> updateOperator.setPageSource(pageSource)); } // set no more splits diff --git a/presto-main/src/main/java/com/facebook/presto/operator/UpdateOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/UpdateOperator.java new file mode 100644 index 0000000000000..48cb520649c86 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/UpdateOperator.java @@ -0,0 +1,86 @@ +/* + * 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.operator; + +import com.facebook.airlift.json.JsonCodec; +import com.facebook.presto.common.Page; +import com.facebook.presto.spi.plan.PlanNodeId; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class UpdateOperator + extends AbstractRowChangeOperator +{ + public static class UpdateOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final PlanNodeId planNodeId; + private final List columnValueAndRowIdChannels; + private boolean closed; + + private final JsonCodec tableCommitContextCodec; + + public UpdateOperatorFactory(int operatorId, PlanNodeId planNodeId, List columnValueAndRowIdChannels, JsonCodec tableCommitContextCodec) + { + this.operatorId = operatorId; + this.planNodeId = requireNonNull(planNodeId, "planNodeId is null"); + this.columnValueAndRowIdChannels = ImmutableList.copyOf(requireNonNull(columnValueAndRowIdChannels, "columnValueAndRowIdChannels is null")); + this.tableCommitContextCodec = requireNonNull(tableCommitContextCodec, "tableCommitContextCodec is null"); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext context = driverContext.addOperatorContext(operatorId, planNodeId, UpdateOperator.class.getSimpleName()); + return new UpdateOperator(context, columnValueAndRowIdChannels, tableCommitContextCodec); + } + + @Override + public void noMoreOperators() + { + closed = true; + } + + @Override + public OperatorFactory duplicate() + { + return new UpdateOperatorFactory(operatorId, planNodeId, columnValueAndRowIdChannels, tableCommitContextCodec); + } + } + + private final List columnValueAndRowIdChannels; + + public UpdateOperator(OperatorContext operatorContext, List columnValueAndRowIdChannels, JsonCodec tableCommitContextCodec) + { + super(operatorContext, tableCommitContextCodec); + this.columnValueAndRowIdChannels = columnValueAndRowIdChannels; + } + + @Override + public void addInput(Page page) + { + requireNonNull(page, "page is null"); + checkState(state == State.RUNNING, "Operator is %s", state); + + // Call the UpdatablePageSource to update rows in the page supplied. + pageSource().updateRows(page, columnValueAndRowIdChannels); + rowCount += page.getPositionCount(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/security/AccessControlManager.java b/presto-main/src/main/java/com/facebook/presto/security/AccessControlManager.java index 500b8832560d5..aed99a82ff809 100644 --- a/presto-main/src/main/java/com/facebook/presto/security/AccessControlManager.java +++ b/presto-main/src/main/java/com/facebook/presto/security/AccessControlManager.java @@ -469,6 +469,22 @@ public void checkCanTruncateTable(TransactionId transactionId, Identity identity } } + @Override + public void checkCanUpdateTableColumns(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName, Set updatedColumnNames) + { + requireNonNull(identity, "identity is null"); + requireNonNull(tableName, "tableName is null"); + + authenticationCheck(() -> checkCanAccessCatalog(identity, context, tableName.getCatalogName())); + + authorizationCheck(() -> systemAccessControl.get().checkCanUpdateTableColumns(identity, context, toCatalogSchemaTableName(tableName), updatedColumnNames)); + + CatalogAccessControlEntry entry = getConnectorAccessControl(transactionId, tableName.getCatalogName()); + if (entry != null) { + authorizationCheck(() -> entry.getAccessControl().checkCanUpdateTableColumns(entry.getTransactionHandle(transactionId), identity.toConnectorIdentity(tableName.getCatalogName()), context, toSchemaTableName(tableName), updatedColumnNames)); + } + } + @Override public void checkCanCreateView(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName viewName) { diff --git a/presto-main/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java b/presto-main/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java index 480dcd53fd115..d230904934472 100644 --- a/presto-main/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java +++ b/presto-main/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java @@ -178,6 +178,11 @@ public void checkCanTruncateTable(Identity identity, AccessControlContext contex { } + @Override + public void checkCanUpdateTableColumns(Identity identity, AccessControlContext context, CatalogSchemaTableName table, Set updatedColumnNames) + { + } + @Override public void checkCanCreateView(Identity identity, AccessControlContext context, CatalogSchemaTableName view) { diff --git a/presto-main/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java b/presto-main/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java index df816bba82c4f..03a51ce68126b 100644 --- a/presto-main/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java +++ b/presto-main/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java @@ -66,6 +66,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyRevokeTablePrivilege; import static com.facebook.presto.spi.security.AccessDeniedException.denySetUser; import static com.facebook.presto.spi.security.AccessDeniedException.denyTruncateTable; +import static com.facebook.presto.spi.security.AccessDeniedException.denyUpdateTableColumns; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Suppliers.memoizeWithExpiration; import static java.lang.String.format; @@ -361,6 +362,14 @@ public void checkCanDeleteFromTable(Identity identity, AccessControlContext cont } } + @Override + public void checkCanUpdateTableColumns(Identity identity, AccessControlContext context, CatalogSchemaTableName table, Set updatedColumnNames) + { + if (!canAccessCatalog(identity, table.getCatalogName(), ALL)) { + denyUpdateTableColumns(table.toString(), updatedColumnNames); + } + } + @Override public void checkCanCreateView(Identity identity, AccessControlContext context, CatalogSchemaTableName view) { diff --git a/presto-main/src/main/java/com/facebook/presto/split/EmptySplitPageSource.java b/presto-main/src/main/java/com/facebook/presto/split/EmptySplitPageSource.java index e4b8713463ea9..51d0c16adc42c 100644 --- a/presto-main/src/main/java/com/facebook/presto/split/EmptySplitPageSource.java +++ b/presto-main/src/main/java/com/facebook/presto/split/EmptySplitPageSource.java @@ -20,6 +20,7 @@ import io.airlift.slice.Slice; import java.util.Collection; +import java.util.List; import java.util.concurrent.CompletableFuture; import static java.util.concurrent.CompletableFuture.completedFuture; @@ -33,6 +34,12 @@ public void deleteRows(Block rowIds) throw new UnsupportedOperationException("deleteRows called on EmptySplitPageSource"); } + @Override + public void updateRows(Page page, List columnValueAndRowIdChannels) + { + throw new UnsupportedOperationException("updateRows called on EmptySplitPageSource"); + } + @Override public CompletableFuture> finish() { diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java index 9bcc16946df47..bf56caa10df19 100644 --- a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java @@ -153,6 +153,8 @@ import com.facebook.presto.sql.tree.TruncateTable; import com.facebook.presto.sql.tree.Union; import com.facebook.presto.sql.tree.Unnest; +import com.facebook.presto.sql.tree.Update; +import com.facebook.presto.sql.tree.UpdateAssignment; import com.facebook.presto.sql.tree.Use; import com.facebook.presto.sql.tree.Values; import com.facebook.presto.sql.tree.Window; @@ -284,6 +286,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Throwables.throwIfInstanceOf; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.Iterables.getLast; import static java.lang.Math.toIntExact; @@ -1974,6 +1977,87 @@ private String createWarningMessage(Node node, String description) return format("line %s:%s: %s", nodeLocation.getLineNumber(), nodeLocation.getColumnNumber(), description); } + @Override + protected Scope visitUpdate(Update update, Optional scope) + { + Table table = update.getTable(); + QualifiedObjectName tableName = createQualifiedObjectName(session, table, table.getName()); + MetadataHandle metadataHandle = analysis.getMetadataHandle(); + + if (getViewDefinition(session, metadataResolver, metadataHandle, tableName).isPresent()) { + throw new SemanticException(NOT_SUPPORTED, update, "Updating into views is not supported"); + } + + if (getMaterializedViewDefinition(session, metadataResolver, metadataHandle, tableName).isPresent()) { + throw new SemanticException(NOT_SUPPORTED, update, "Updating into materialized views is not supported"); + } + + TableColumnMetadata tableMetadata = getTableColumnsMetadata(session, metadataResolver, metadataHandle, tableName); + + List allColumns = tableMetadata.getColumnsMetadata(); + + Map columns = allColumns.stream() + .collect(toImmutableMap(ColumnMetadata::getName, Function.identity())); + + for (UpdateAssignment assignment : update.getAssignments()) { + String columnName = assignment.getName().getValue(); + if (!columns.containsKey(columnName)) { + throw new SemanticException(MISSING_COLUMN, assignment.getName(), "The UPDATE SET target column %s doesn't exist", columnName); + } + } + + Set assignmentTargets = update.getAssignments().stream() + .map(assignment -> assignment.getName().getValue()) + .collect(toImmutableSet()); + accessControl.checkCanUpdateTableColumns(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), tableName, assignmentTargets); + + List updatedColumns = allColumns.stream() + .filter(column -> assignmentTargets.contains(column.getName())) + .collect(toImmutableList()); + analysis.setUpdateType("UPDATE"); + analysis.setUpdatedColumns(updatedColumns); + + // Analyzer checks for select permissions but UPDATE has a separate permission, so disable access checks + StatementAnalyzer analyzer = new StatementAnalyzer( + analysis, + metadata, + sqlParser, + new AllowAllAccessControl(), + session, + warningCollector); + + Scope tableScope = analyzer.analyze(table, scope); + update.getWhere().ifPresent(where -> analyzeWhere(update, tableScope, where)); + + ImmutableList.Builder analysesBuilder = ImmutableList.builder(); + ImmutableList.Builder expressionTypesBuilder = ImmutableList.builder(); + for (UpdateAssignment assignment : update.getAssignments()) { + Expression expression = assignment.getValue(); + ExpressionAnalysis analysis = analyzeExpression(expression, tableScope); + analysesBuilder.add(analysis); + expressionTypesBuilder.add(analysis.getType(expression)); + } + List analyses = analysesBuilder.build(); + List expressionTypes = expressionTypesBuilder.build(); + + List tableTypes = update.getAssignments().stream() + .map(assignment -> requireNonNull(columns.get(assignment.getName().getValue()))) + .map(ColumnMetadata::getType) + .collect(toImmutableList()); + + for (int index = 0; index < expressionTypes.size(); index++) { + Expression expression = update.getAssignments().get(index).getValue(); + Type expressionType = expressionTypes.get(index); + Type targetType = tableTypes.get(index); + if (!targetType.equals(expressionType)) { + analysis.addCoercion(expression, targetType, functionAndTypeResolver.isTypeOnlyCoercion(expressionType, targetType)); + } + analysis.recordSubqueries(update, analyses.get(index)); + } + + return createAndAssignScope(update, scope, Field.newUnqualified(update.getLocation(), "rows", BIGINT)); + } + private Scope analyzeJoinUsing(Join node, List columns, Optional scope, Scope left, Scope right) { List joinFields = new ArrayList<>(); diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/LocalExecutionPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/LocalExecutionPlanner.java index b0501ce123c66..e727f9141de29 100644 --- a/presto-main/src/main/java/com/facebook/presto/sql/planner/LocalExecutionPlanner.java +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/LocalExecutionPlanner.java @@ -38,6 +38,7 @@ import com.facebook.presto.execution.scheduler.ExecutionWriterTarget.DeleteHandle; import com.facebook.presto.execution.scheduler.ExecutionWriterTarget.InsertHandle; import com.facebook.presto.execution.scheduler.ExecutionWriterTarget.RefreshMaterializedViewHandle; +import com.facebook.presto.execution.scheduler.ExecutionWriterTarget.UpdateHandle; import com.facebook.presto.execution.scheduler.TableWriteInfo; import com.facebook.presto.execution.scheduler.TableWriteInfo.DeleteScanInfo; import com.facebook.presto.expressions.DynamicFilters; @@ -105,6 +106,7 @@ import com.facebook.presto.operator.TaskOutputOperator.TaskOutputFactory; import com.facebook.presto.operator.TopNOperator.TopNOperatorFactory; import com.facebook.presto.operator.TopNRowNumberOperator; +import com.facebook.presto.operator.UpdateOperator.UpdateOperatorFactory; import com.facebook.presto.operator.ValuesOperator.ValuesOperatorFactory; import com.facebook.presto.operator.WindowFunctionDefinition; import com.facebook.presto.operator.WindowOperator.WindowOperatorFactory; @@ -203,6 +205,7 @@ import com.facebook.presto.sql.planner.plan.TableWriterNode; import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.UpdateNode; import com.facebook.presto.sql.planner.plan.WindowNode; import com.facebook.presto.sql.planner.plan.WindowNode.Frame; import com.facebook.presto.sql.relational.FunctionResolution; @@ -2938,6 +2941,38 @@ public PhysicalOperation visitMetadataDelete(MetadataDeleteNode node, LocalExecu return new PhysicalOperation(operatorFactory, makeLayout(node), context, UNGROUPED_EXECUTION); } + @Override + public PhysicalOperation visitUpdate(UpdateNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + List channelNumbers = createColumnValueAndRowIdChannels(node.getSource().getOutputVariables(), node.getColumnValueAndRowIdSymbols()); + OperatorFactory operatorFactory = new UpdateOperatorFactory(context.getNextOperatorId(), node.getId(), channelNumbers, tableCommitContextCodec); + + Map layout = ImmutableMap.builder() + .put(node.getOutputVariables().get(0), 0) + .put(node.getOutputVariables().get(1), 1) + .build(); + + return new PhysicalOperation(operatorFactory, layout, context, source); + } + + private List createColumnValueAndRowIdChannels(List variableReferenceExpressions, List columnValueAndRowIdSymbols) + { + Integer[] columnValueAndRowIdChannels = new Integer[columnValueAndRowIdSymbols.size()]; + int symbolCounter = 0; + // This depends on the outputSymbols being ordered as the blocks of the + // resulting page are ordered. + for (VariableReferenceExpression variableReferenceExpression : variableReferenceExpressions) { + int index = columnValueAndRowIdSymbols.indexOf(variableReferenceExpression); + if (index >= 0) { + columnValueAndRowIdChannels[index] = symbolCounter; + } + symbolCounter++; + } + checkArgument(symbolCounter == columnValueAndRowIdSymbols.size(), "symbolCounter %s should be columnValueAndRowIdChannels.size() %s", symbolCounter); + return Arrays.asList(columnValueAndRowIdChannels); + } + @Override public PhysicalOperation visitUnion(UnionNode node, LocalExecutionPlanContext context) { @@ -3413,6 +3448,10 @@ else if (target instanceof DeleteHandle) { else if (target instanceof RefreshMaterializedViewHandle) { return metadata.finishRefreshMaterializedView(session, ((RefreshMaterializedViewHandle) target).getHandle(), fragments, statistics); } + else if (target instanceof UpdateHandle) { + metadata.finishUpdate(session, ((UpdateHandle) target).getHandle(), fragments); + return Optional.empty(); + } else { throw new AssertionError("Unhandled target type: " + target.getClass().getName()); } diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/LogicalPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/LogicalPlanner.java index 642ba7f1165e0..f03278306fbdb 100644 --- a/presto-main/src/main/java/com/facebook/presto/sql/planner/LogicalPlanner.java +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/LogicalPlanner.java @@ -53,6 +53,7 @@ import com.facebook.presto.sql.planner.plan.TableFinishNode; import com.facebook.presto.sql.planner.plan.TableWriterNode; import com.facebook.presto.sql.planner.plan.TableWriterNode.DeleteHandle; +import com.facebook.presto.sql.planner.plan.UpdateNode; import com.facebook.presto.sql.tree.Analyze; import com.facebook.presto.sql.tree.Cast; import com.facebook.presto.sql.tree.CreateTableAsSelect; @@ -68,6 +69,7 @@ import com.facebook.presto.sql.tree.Query; import com.facebook.presto.sql.tree.RefreshMaterializedView; import com.facebook.presto.sql.tree.Statement; +import com.facebook.presto.sql.tree.Update; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -96,6 +98,7 @@ import static com.facebook.presto.sql.planner.plan.TableWriterNode.CreateName; import static com.facebook.presto.sql.planner.plan.TableWriterNode.InsertReference; import static com.facebook.presto.sql.planner.plan.TableWriterNode.RefreshMaterializedViewReference; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.UpdateTarget; import static com.facebook.presto.sql.planner.plan.TableWriterNode.WriterTarget; import static com.facebook.presto.sql.relational.Expressions.constant; import static com.google.common.base.Preconditions.checkState; @@ -169,6 +172,9 @@ else if (statement instanceof Insert) { else if (statement instanceof Delete) { return createDeletePlan(analysis, (Delete) statement); } + if (statement instanceof Update) { + return createUpdatePlan(analysis, (Update) statement); + } else if (statement instanceof Query) { return createRelationPlan(analysis, (Query) statement, new SqlPlannerContext(0)); } @@ -479,6 +485,47 @@ private RelationPlan createDeletePlan(Analysis analysis, Delete node) return new RelationPlan(commitNode, analysis.getScope(node), commitNode.getOutputVariables()); } + private RelationPlan createUpdatePlan(Analysis analysis, Update node) + { + SqlPlannerContext context = new SqlPlannerContext(0); + UpdateNode updateNode = new QueryPlanner(analysis, variableAllocator, idAllocator, buildLambdaDeclarationToVariableMap(analysis, variableAllocator), metadata, session, context, sqlParser) + .plan(node); + + TableHandle handle = analysis.getTableHandle(node.getTable()); + ImmutableList.Builder updatedColumnNamesBuilder = ImmutableList.builder(); + ImmutableList.Builder updatedColumnHandlesBuilder = ImmutableList.builder(); + + TableMetadata tableMetadata = metadata.getTableMetadata(session, handle); + Map columnMap = metadata.getColumnHandles(session, handle); + List dataColumns = tableMetadata.getMetadata().getColumns().stream() + .filter(column -> !column.isHidden()) + .collect(toImmutableList()); + List targetColumnNames = node.getAssignments().stream() + .map(assignment -> assignment.getName().getValue()) + .collect(toImmutableList()); + + for (ColumnMetadata columnMetadata : dataColumns) { + String name = columnMetadata.getName(); + int index = targetColumnNames.indexOf(name); + if (index >= 0) { + updatedColumnNamesBuilder.add(name); + updatedColumnHandlesBuilder.add(requireNonNull(columnMap.get(name), "columnMap didn't contain name")); + } + } + + UpdateTarget updateTarget = new UpdateTarget(handle, metadata.getTableMetadata(session, handle).getTable(), updatedColumnNamesBuilder.build(), updatedColumnHandlesBuilder.build()); + TableFinishNode commitNode = new TableFinishNode( + updateNode.getSourceLocation(), + idAllocator.getNextId(), + updateNode, + Optional.of(updateTarget), + variableAllocator.newVariable("rows", BIGINT), + Optional.empty(), + Optional.empty()); + + return new RelationPlan(commitNode, analysis.getScope(node), commitNode.getOutputVariables()); + } + private PlanNode createOutputPlan(RelationPlan plan, Analysis analysis) { ImmutableList.Builder outputs = ImmutableList.builder(); diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java index 68b7840da5f62..7510f5b91d0a1 100644 --- a/presto-main/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java @@ -20,6 +20,7 @@ import com.facebook.presto.common.type.Type; import com.facebook.presto.metadata.Metadata; import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; import com.facebook.presto.spi.TableHandle; import com.facebook.presto.spi.VariableAllocator; import com.facebook.presto.spi.function.FunctionHandle; @@ -32,6 +33,7 @@ import com.facebook.presto.spi.plan.Ordering; import com.facebook.presto.spi.plan.OrderingScheme; import com.facebook.presto.spi.plan.PlanNode; +import com.facebook.presto.spi.plan.PlanNodeId; import com.facebook.presto.spi.plan.PlanNodeIdAllocator; import com.facebook.presto.spi.plan.ProjectNode; import com.facebook.presto.spi.plan.TableScanNode; @@ -51,6 +53,7 @@ import com.facebook.presto.sql.planner.plan.GroupIdNode; import com.facebook.presto.sql.planner.plan.OffsetNode; import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.UpdateNode; import com.facebook.presto.sql.planner.plan.WindowNode; import com.facebook.presto.sql.tree.Cast; import com.facebook.presto.sql.tree.ComparisonExpression; @@ -76,8 +79,10 @@ import com.facebook.presto.sql.tree.SortItem; import com.facebook.presto.sql.tree.StringLiteral; import com.facebook.presto.sql.tree.SymbolReference; +import com.facebook.presto.sql.tree.Update; import com.facebook.presto.sql.tree.Window; import com.facebook.presto.sql.tree.WindowFrame; +import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -127,6 +132,7 @@ import static com.facebook.presto.type.IntervalDayTimeType.INTERVAL_DAY_TIME; import static com.facebook.presto.type.IntervalYearMonthType.INTERVAL_YEAR_MONTH; import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -244,7 +250,7 @@ public DeleteNode plan(Delete node) { RelationType descriptor = analysis.getOutputDescriptor(node.getTable()); TableHandle handle = analysis.getTableHandle(node.getTable()); - ColumnHandle rowIdHandle = metadata.getUpdateRowIdColumnHandle(session, handle); + ColumnHandle rowIdHandle = metadata.getDeleteRowIdColumnHandle(session, handle); Type rowIdType = metadata.getColumnMetadata(session, handle, rowIdHandle).getType(); // add table columns @@ -289,6 +295,104 @@ public DeleteNode plan(Delete node) return new DeleteNode(getSourceLocation(node), idAllocator.getNextId(), builder.getRoot(), rowId, deleteNodeOutputVariables); } + public UpdateNode plan(Update node) + { + RelationType descriptor = analysis.getOutputDescriptor(node.getTable()); + TableHandle handle = analysis.getTableHandle(node.getTable()); + + Map columnHandles = metadata.getColumnHandles(session, handle); + List updatedColumnMetadata = analysis.getUpdatedColumns() + .orElseThrow(() -> new VerifyException("updated columns not set")); + Set updatedColumnNames = updatedColumnMetadata.stream().map(ColumnMetadata::getName).collect(toImmutableSet()); + List updatedColumns = columnHandles.entrySet().stream() + .filter(entry -> updatedColumnNames.contains(entry.getKey())) + .map(Map.Entry::getValue) + .collect(toImmutableList()); + ColumnHandle rowIdHandle = metadata.getUpdateRowIdColumnHandle(session, handle, updatedColumns); + Type rowIdType = metadata.getColumnMetadata(session, handle, rowIdHandle).getType(); + + List targetColumnNames = node.getAssignments().stream() + .map(assignment -> assignment.getName().getValue()) + .collect(toImmutableList()); + + // Create lists of columnnames and SET expressions, in table column order + ImmutableList.Builder outputVariablesBuilder = ImmutableList.builder(); + ImmutableMap.Builder columns = ImmutableMap.builder(); + ImmutableList.Builder fields = ImmutableList.builder(); + ImmutableList.Builder orderedColumnValuesBuilder = ImmutableList.builder(); + for (Field field : descriptor.getAllFields()) { + String name = field.getName().get(); + int index = targetColumnNames.indexOf(name); + if (index >= 0) { + VariableReferenceExpression variable = variableAllocator.newVariable(getSourceLocation(field.getNodeLocation()), field.getName().get(), field.getType()); + outputVariablesBuilder.add(variable); + columns.put(variable, analysis.getColumn(field)); + fields.add(field); + orderedColumnValuesBuilder.add(node.getAssignments().get(index).getValue()); + } + } + List orderedColumnValues = orderedColumnValuesBuilder.build(); + + // add rowId column + Field rowIdField = Field.newUnqualified(node.getLocation(), Optional.empty(), rowIdType); + VariableReferenceExpression rowIdVariable = variableAllocator.newVariable(getSourceLocation(node), "$rowId", rowIdField.getType()); + outputVariablesBuilder.add(rowIdVariable); + columns.put(rowIdVariable, rowIdHandle); + fields.add(rowIdField); + + // create table scan + List outputVariables = outputVariablesBuilder.build(); + PlanNode tableScan = new TableScanNode(getSourceLocation(node), idAllocator.getNextId(), handle, outputVariables, columns.build(), TupleDomain.all(), TupleDomain.all()); + Scope scope = Scope.builder().withRelationType(RelationId.anonymous(), new RelationType(fields.build())).build(); + RelationPlan relationPlan = new RelationPlan(tableScan, scope, outputVariables); + + TranslationMap translations = new TranslationMap(relationPlan, analysis, lambdaDeclarationToVariableMap); + translations.setFieldMappings(relationPlan.getFieldMappings()); + PlanBuilder builder = new PlanBuilder(translations, relationPlan.getRoot()); + + if (node.getWhere().isPresent()) { + builder = filter(builder, node.getWhere().get(), node); + } + + builder = builder.appendProjections(orderedColumnValues, variableAllocator, idAllocator, session, metadata, sqlParser, analysis, sqlPlannerContext); + + PlanAndMappings planAndMappings = coerce(builder, orderedColumnValues, analysis, idAllocator, variableAllocator, metadata); + builder = planAndMappings.getSubPlan(); + + ImmutableList.Builder updatedColumnValuesBuilder = ImmutableList.builder(); + orderedColumnValues.forEach(columnValue -> updatedColumnValuesBuilder.add(planAndMappings.get(columnValue))); + VariableReferenceExpression rowId = new VariableReferenceExpression(Optional.empty(), builder.translate(new FieldReference(relationPlan.getDescriptor().indexOf(rowIdField))).getName(), rowIdField.getType()); + updatedColumnValuesBuilder.add(rowId); + + List outputs = ImmutableList.of( + variableAllocator.newVariable("partialrows", BIGINT), + variableAllocator.newVariable("fragment", VARBINARY)); + + Optional tableScanId = getIdForLeftTableScan(relationPlan.getRoot()); + checkArgument(tableScanId.isPresent(), "tableScanId not present"); + + // create update node + return new UpdateNode( + getSourceLocation(node), + idAllocator.getNextId(), + builder.getRoot(), + rowId, + updatedColumnValuesBuilder.build(), + outputs); + } + + private Optional getIdForLeftTableScan(PlanNode node) + { + if (node instanceof TableScanNode) { + return Optional.of(node.getId()); + } + List sources = node.getSources(); + if (sources.isEmpty()) { + return Optional.empty(); + } + return getIdForLeftTableScan(sources.get(0)); + } + private static List computeOutputs(PlanBuilder builder, List outputExpressions) { ImmutableList.Builder outputs = ImmutableList.builder(); diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/InternalPlanVisitor.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/InternalPlanVisitor.java index f2828854f6fbc..0a7f9e3b05844 100644 --- a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/InternalPlanVisitor.java +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/InternalPlanVisitor.java @@ -102,6 +102,11 @@ public R visitMetadataDelete(MetadataDeleteNode node, C context) return visitPlan(node, context); } + public R visitUpdate(UpdateNode node, C context) + { + return visitPlan(node, context); + } + public R visitTableFinish(TableFinishNode node, C context) { return visitPlan(node, context); diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableWriterNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableWriterNode.java index a5c5fee9c1163..e27e77037c050 100644 --- a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableWriterNode.java +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableWriterNode.java @@ -14,6 +14,7 @@ package com.facebook.presto.sql.planner.plan; import com.facebook.presto.metadata.NewTableLayout; +import com.facebook.presto.spi.ColumnHandle; import com.facebook.presto.spi.ConnectorId; import com.facebook.presto.spi.ConnectorTableMetadata; import com.facebook.presto.spi.SchemaTableName; @@ -423,4 +424,63 @@ public String toString() return handle.toString(); } } + + public static class UpdateTarget + extends WriterTarget + { + private final TableHandle handle; + private final SchemaTableName schemaTableName; + private final List updatedColumns; + private final List updatedColumnHandles; + + @JsonCreator + public UpdateTarget( + @JsonProperty("handle") TableHandle handle, + @JsonProperty("schemaTableName") SchemaTableName schemaTableName, + @JsonProperty("updatedColumns") List updatedColumns, + @JsonProperty("updatedColumnHandles") List updatedColumnHandles) + { + this.handle = requireNonNull(handle, "handle is null"); + this.schemaTableName = requireNonNull(schemaTableName, "schemaTableName is null"); + checkArgument(updatedColumns.size() == updatedColumnHandles.size(), "updatedColumns size %s must equal updatedColumnHandles size %s", updatedColumns.size(), updatedColumnHandles.size()); + this.updatedColumns = requireNonNull(updatedColumns, "updatedColumns is null"); + this.updatedColumnHandles = requireNonNull(updatedColumnHandles, "updatedColumnHandles is null"); + } + + @JsonProperty + public TableHandle getHandle() + { + return handle; + } + + @Override + public ConnectorId getConnectorId() + { + return handle.getConnectorId(); + } + + @JsonProperty + public SchemaTableName getSchemaTableName() + { + return schemaTableName; + } + + @JsonProperty + public List getUpdatedColumns() + { + return updatedColumns; + } + + @JsonProperty + public List getUpdatedColumnHandles() + { + return updatedColumnHandles; + } + + @Override + public String toString() + { + return handle.toString(); + } + } } diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UpdateNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UpdateNode.java new file mode 100644 index 0000000000000..1b2e8c89e21d4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UpdateNode.java @@ -0,0 +1,117 @@ +/* + * 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.planner.plan; + +import com.facebook.presto.spi.SourceLocation; +import com.facebook.presto.spi.plan.PlanNode; +import com.facebook.presto.spi.plan.PlanNodeId; +import com.facebook.presto.spi.relation.VariableReferenceExpression; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +@Immutable +public class UpdateNode + extends InternalPlanNode +{ + private final PlanNode source; + private final VariableReferenceExpression rowId; + private final List columnValueAndRowIdSymbols; + private final List outputVariables; + + @JsonCreator + public UpdateNode( + Optional sourceLocation, + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("rowId") VariableReferenceExpression rowId, + @JsonProperty("columnValueAndRowIdSymbols") List columnValueAndRowIdSymbols, + @JsonProperty("outputVariables") List outputVariables) + { + this(source.getSourceLocation(), id, Optional.empty(), source, rowId, columnValueAndRowIdSymbols, outputVariables); + } + + public UpdateNode( + Optional sourceLocation, + PlanNodeId id, + Optional statsEquivalentPlanNode, + PlanNode source, + VariableReferenceExpression rowId, + List columnValueAndRowIdSymbols, + List outputVariables) + { + super(sourceLocation, id, statsEquivalentPlanNode); + + this.source = requireNonNull(source, "source is null"); + this.rowId = requireNonNull(rowId, "rowId is null"); + this.columnValueAndRowIdSymbols = ImmutableList.copyOf(requireNonNull(columnValueAndRowIdSymbols, "columnValueAndRowIdSymbols is null")); + this.outputVariables = ImmutableList.copyOf(requireNonNull(outputVariables, "outputVariables is null")); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public VariableReferenceExpression getRowId() + { + return rowId; + } + + @JsonProperty + public List getColumnValueAndRowIdSymbols() + { + return columnValueAndRowIdSymbols; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public List getOutputVariables() + { + return outputVariables; + } + + @Override + public R accept(InternalPlanVisitor visitor, C context) + { + return visitor.visitUpdate(this, context); + } + + @Override + public PlanNode assignStatsEquivalentPlanNode(Optional statsEquivalentPlanNode) + { + return new UpdateNode(getSourceLocation(), getId(), statsEquivalentPlanNode, source, rowId, columnValueAndRowIdSymbols, outputVariables); + } + + @Override + public PlanNode replaceChildren(List newChildren) + { + return new UpdateNode(getSourceLocation(), getId(), Optional.of(Iterables.getOnlyElement(newChildren)), source, rowId, columnValueAndRowIdSymbols, outputVariables); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/TestingAccessControlManager.java b/presto-main/src/main/java/com/facebook/presto/testing/TestingAccessControlManager.java index 1f685a051fa8f..2500a2be42351 100644 --- a/presto-main/src/main/java/com/facebook/presto/testing/TestingAccessControlManager.java +++ b/presto-main/src/main/java/com/facebook/presto/testing/TestingAccessControlManager.java @@ -52,6 +52,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denySetSystemSessionProperty; import static com.facebook.presto.spi.security.AccessDeniedException.denySetUser; import static com.facebook.presto.spi.security.AccessDeniedException.denyTruncateTable; +import static com.facebook.presto.spi.security.AccessDeniedException.denyUpdateTableColumns; import static com.facebook.presto.testing.TestingAccessControlManager.TestingPrivilegeType.ADD_COLUMN; import static com.facebook.presto.testing.TestingAccessControlManager.TestingPrivilegeType.CREATE_SCHEMA; import static com.facebook.presto.testing.TestingAccessControlManager.TestingPrivilegeType.CREATE_TABLE; @@ -70,6 +71,7 @@ import static com.facebook.presto.testing.TestingAccessControlManager.TestingPrivilegeType.SET_SESSION; import static com.facebook.presto.testing.TestingAccessControlManager.TestingPrivilegeType.SET_USER; import static com.facebook.presto.testing.TestingAccessControlManager.TestingPrivilegeType.TRUNCATE_TABLE; +import static com.facebook.presto.testing.TestingAccessControlManager.TestingPrivilegeType.UPDATE_TABLE; import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static java.util.Objects.requireNonNull; @@ -243,6 +245,17 @@ public void checkCanTruncateTable(TransactionId transactionId, Identity identity } } + @Override + public void checkCanUpdateTableColumns(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName, Set updatedColumnNames) + { + if (shouldDenyPrivilege(identity.getUser(), tableName.getObjectName(), UPDATE_TABLE)) { + denyUpdateTableColumns(tableName.toString(), updatedColumnNames); + } + if (denyPrivileges.isEmpty()) { + super.checkCanUpdateTableColumns(transactionId, identity, context, tableName, updatedColumnNames); + } + } + @Override public void checkCanCreateView(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName viewName) { @@ -330,7 +343,7 @@ public enum TestingPrivilegeType { SET_USER, CREATE_SCHEMA, DROP_SCHEMA, RENAME_SCHEMA, - CREATE_TABLE, DROP_TABLE, RENAME_TABLE, INSERT_TABLE, DELETE_TABLE, TRUNCATE_TABLE, + CREATE_TABLE, DROP_TABLE, RENAME_TABLE, INSERT_TABLE, DELETE_TABLE, TRUNCATE_TABLE, UPDATE_TABLE, ADD_COLUMN, DROP_COLUMN, RENAME_COLUMN, SELECT_COLUMN, CREATE_VIEW, DROP_VIEW, CREATE_VIEW_WITH_SELECT_COLUMNS, SET_SESSION diff --git a/presto-main/src/test/java/com/facebook/presto/metadata/AbstractMockMetadata.java b/presto-main/src/test/java/com/facebook/presto/metadata/AbstractMockMetadata.java index ec0acf28af077..e63e15bd70141 100644 --- a/presto-main/src/test/java/com/facebook/presto/metadata/AbstractMockMetadata.java +++ b/presto-main/src/test/java/com/facebook/presto/metadata/AbstractMockMetadata.java @@ -391,7 +391,13 @@ public Optional finishInsert(Session session, InsertTab } @Override - public ColumnHandle getUpdateRowIdColumnHandle(Session session, TableHandle tableHandle) + public ColumnHandle getDeleteRowIdColumnHandle(Session session, TableHandle tableHandle) + { + throw new UnsupportedOperationException(); + } + + @Override + public ColumnHandle getUpdateRowIdColumnHandle(Session session, TableHandle tableHandle, List updatedColumns) { throw new UnsupportedOperationException(); } @@ -420,6 +426,18 @@ public void finishDelete(Session session, TableHandle tableHandle, Collection updatedColumns) + { + throw new UnsupportedOperationException(); + } + + @Override + public void finishUpdate(Session session, TableHandle tableHandle, Collection fragments) + { + throw new UnsupportedOperationException(); + } + @Override public Optional getCatalogHandle(Session session, String catalogName) { diff --git a/presto-main/src/test/java/com/facebook/presto/security/TestAccessControlManager.java b/presto-main/src/test/java/com/facebook/presto/security/TestAccessControlManager.java index e01255b149ddf..87914996430d5 100644 --- a/presto-main/src/test/java/com/facebook/presto/security/TestAccessControlManager.java +++ b/presto-main/src/test/java/com/facebook/presto/security/TestAccessControlManager.java @@ -448,6 +448,12 @@ public void checkCanDeleteFromTable(ConnectorTransactionHandle transactionHandle throw new UnsupportedOperationException(); } + @Override + public void checkCanUpdateTableColumns(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + throw new UnsupportedOperationException(); + } + @Override public void checkCanCreateView(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName viewName) { diff --git a/presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/TestMySqlDistributedQueries.java b/presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/TestMySqlDistributedQueries.java index 6bce223401a7a..ff53ae95e8e2e 100644 --- a/presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/TestMySqlDistributedQueries.java +++ b/presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/TestMySqlDistributedQueries.java @@ -114,5 +114,11 @@ public void testDelete() // Delete is currently unsupported } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + // MySQL specific tests should normally go in TestMySqlIntegrationSmokeTest } diff --git a/presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4 b/presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4 index 6c899ecffdc2d..42885cf9903df 100644 --- a/presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4 +++ b/presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4 @@ -136,6 +136,9 @@ statement | EXECUTE identifier (USING expression (',' expression)*)? #execute | DESCRIBE INPUT identifier #describeInput | DESCRIBE OUTPUT identifier #describeOutput + | UPDATE qualifiedName + SET updateAssignment (',' updateAssignment)* + (WHERE where=booleanExpression)? #update ; query @@ -500,6 +503,9 @@ frameBound | expression boundType=(PRECEDING | FOLLOWING) #boundedFrame // expression should be unsignedLiteral ; +updateAssignment + : identifier '=' expression + ; explainOption : FORMAT value=(TEXT | GRAPHVIZ | JSON) #explainFormat @@ -586,7 +592,7 @@ nonReserved | SCHEMA | SCHEMAS | SECOND | SECURITY | SERIALIZABLE | SESSION | SET | SETS | SQL | SHOW | SOME | START | STATS | SUBSTRING | SYSTEM | SYSTEM_TIME | SYSTEM_VERSION | TABLES | TABLESAMPLE | TEMPORARY | TEXT | TIME | TIMESTAMP | TO | TRANSACTION | TRUNCATE | TRY_CAST | TYPE - | UNBOUNDED | UNCOMMITTED | USE | USER + | UNBOUNDED | UNCOMMITTED | UPDATE | USE | USER | VALIDATE | VERBOSE | VERSION | VIEW | WORK | WRITE | YEAR @@ -788,6 +794,7 @@ UNBOUNDED: 'UNBOUNDED'; UNCOMMITTED: 'UNCOMMITTED'; UNION: 'UNION'; UNNEST: 'UNNEST'; +UPDATE: 'UPDATE'; USE: 'USE'; USER: 'USER'; USING: 'USING'; diff --git a/presto-parser/src/main/java/com/facebook/presto/sql/SqlFormatter.java b/presto-parser/src/main/java/com/facebook/presto/sql/SqlFormatter.java index bbcecb9cd4c9a..f7ecbbfe7a268 100644 --- a/presto-parser/src/main/java/com/facebook/presto/sql/SqlFormatter.java +++ b/presto-parser/src/main/java/com/facebook/presto/sql/SqlFormatter.java @@ -112,6 +112,8 @@ import com.facebook.presto.sql.tree.TruncateTable; import com.facebook.presto.sql.tree.Union; import com.facebook.presto.sql.tree.Unnest; +import com.facebook.presto.sql.tree.Update; +import com.facebook.presto.sql.tree.UpdateAssignment; import com.facebook.presto.sql.tree.Use; import com.facebook.presto.sql.tree.Values; import com.facebook.presto.sql.tree.With; @@ -1269,6 +1271,32 @@ protected Void visitInsert(Insert node, Integer indent) return null; } + @Override + protected Void visitUpdate(Update node, Integer indent) + { + builder.append("UPDATE ") + .append(node.getTable().getName()) + .append(" SET"); + int setCounter = node.getAssignments().size() - 1; + for (UpdateAssignment assignment : node.getAssignments()) { + builder.append("\n") + .append(indentString(indent + 1)) + .append(assignment.getName().getValue()) + .append(" = ") + .append(formatExpression(assignment.getValue(), parameters)); + if (setCounter > 0) { + builder.append(","); + } + setCounter--; + } + if (node.getWhere().isPresent()) { + builder.append("\n") + .append(indentString(indent)) + .append("WHERE ").append(formatExpression(node.getWhere().get(), parameters)); + } + return null; + } + @Override public Void visitSetSession(SetSession node, Integer context) { diff --git a/presto-parser/src/main/java/com/facebook/presto/sql/parser/AstBuilder.java b/presto-parser/src/main/java/com/facebook/presto/sql/parser/AstBuilder.java index bf7ffe97deb8f..a66e7d2024c64 100644 --- a/presto-parser/src/main/java/com/facebook/presto/sql/parser/AstBuilder.java +++ b/presto-parser/src/main/java/com/facebook/presto/sql/parser/AstBuilder.java @@ -173,6 +173,8 @@ import com.facebook.presto.sql.tree.TryExpression; import com.facebook.presto.sql.tree.Union; import com.facebook.presto.sql.tree.Unnest; +import com.facebook.presto.sql.tree.Update; +import com.facebook.presto.sql.tree.UpdateAssignment; import com.facebook.presto.sql.tree.Use; import com.facebook.presto.sql.tree.Values; import com.facebook.presto.sql.tree.WhenClause; @@ -417,6 +419,22 @@ public Node visitDelete(SqlBaseParser.DeleteContext context) visitIfPresent(context.booleanExpression(), Expression.class)); } + @Override + public Node visitUpdate(SqlBaseParser.UpdateContext context) + { + return new Update( + getLocation(context), + new Table(getLocation(context), getQualifiedName(context.qualifiedName())), + visit(context.updateAssignment(), UpdateAssignment.class), + visitIfPresent(context.booleanExpression(), Expression.class)); + } + + @Override + public Node visitUpdateAssignment(SqlBaseParser.UpdateAssignmentContext context) + { + return new UpdateAssignment((Identifier) visit(context.identifier()), (Expression) visit(context.expression())); + } + @Override public Node visitTruncateTable(SqlBaseParser.TruncateTableContext context) { diff --git a/presto-parser/src/main/java/com/facebook/presto/sql/tree/AstVisitor.java b/presto-parser/src/main/java/com/facebook/presto/sql/tree/AstVisitor.java index dec1bfd08d3b2..17d8552d1464d 100644 --- a/presto-parser/src/main/java/com/facebook/presto/sql/tree/AstVisitor.java +++ b/presto-parser/src/main/java/com/facebook/presto/sql/tree/AstVisitor.java @@ -657,6 +657,16 @@ protected R visitDelete(Delete node, C context) return visitStatement(node, context); } + protected R visitUpdate(Update node, C context) + { + return visitStatement(node, context); + } + + protected R visitUpdateAssignment(UpdateAssignment node, C context) + { + return visitNode(node, context); + } + protected R visitTruncateTable(TruncateTable node, C context) { return visitStatement(node, context); diff --git a/presto-parser/src/main/java/com/facebook/presto/sql/tree/DefaultTraversalVisitor.java b/presto-parser/src/main/java/com/facebook/presto/sql/tree/DefaultTraversalVisitor.java index 86a412cf6935e..7c90775c78d3a 100644 --- a/presto-parser/src/main/java/com/facebook/presto/sql/tree/DefaultTraversalVisitor.java +++ b/presto-parser/src/main/java/com/facebook/presto/sql/tree/DefaultTraversalVisitor.java @@ -459,6 +459,22 @@ protected R visitDelete(Delete node, C context) return null; } + protected R visitUpdate(Update node, C context) + { + process(node.getTable(), context); + node.getAssignments().forEach(value -> process(value, context)); + node.getWhere().ifPresent(where -> process(where, context)); + + return null; + } + + protected R visitUpdateAssignment(UpdateAssignment node, C context) + { + process(node.getName(), context); + process(node.getValue(), context); + return null; + } + @Override protected R visitCreateTableAsSelect(CreateTableAsSelect node, C context) { diff --git a/presto-parser/src/main/java/com/facebook/presto/sql/tree/Update.java b/presto-parser/src/main/java/com/facebook/presto/sql/tree/Update.java new file mode 100644 index 0000000000000..36e5204d4dd7e --- /dev/null +++ b/presto-parser/src/main/java/com/facebook/presto/sql/tree/Update.java @@ -0,0 +1,111 @@ +/* + * 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.Objects; +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public class Update + extends Statement +{ + private final Table table; + private final List assignments; + private final Optional where; + + public Update(Table table, List assignments, Optional where) + { + this(Optional.empty(), table, assignments, where); + } + + public Update(NodeLocation location, Table table, List assignments, Optional where) + { + this(Optional.of(location), table, assignments, where); + } + + private Update(Optional location, Table table, List assignments, Optional where) + { + super(location); + this.table = requireNonNull(table, "table is null"); + this.assignments = requireNonNull(assignments, "targets is null"); + this.where = requireNonNull(where, "where is null"); + } + + public Table getTable() + { + return table; + } + + public List getAssignments() + { + return assignments; + } + + public Optional getWhere() + { + return where; + } + + @Override + public List getChildren() + { + ImmutableList.Builder nodes = ImmutableList.builder(); + nodes.addAll(assignments); + where.ifPresent(nodes::add); + return nodes.build(); + } + + @Override + public R accept(AstVisitor visitor, C context) + { + return visitor.visitUpdate(this, context); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Update update = (Update) o; + return table.equals(update.table) && + assignments.equals(update.assignments) && + where.equals(update.where); + } + + @Override + public int hashCode() + { + return Objects.hash(table, assignments, where); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("table", table) + .add("assignments", assignments) + .add("where", where.orElse(null)) + .omitNullValues() + .toString(); + } +} diff --git a/presto-parser/src/main/java/com/facebook/presto/sql/tree/UpdateAssignment.java b/presto-parser/src/main/java/com/facebook/presto/sql/tree/UpdateAssignment.java new file mode 100644 index 0000000000000..1e1ecdca4a637 --- /dev/null +++ b/presto-parser/src/main/java/com/facebook/presto/sql/tree/UpdateAssignment.java @@ -0,0 +1,95 @@ +/* + * 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.Objects; +import java.util.Optional; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class UpdateAssignment + extends Node +{ + private final Identifier name; + private final Expression value; + + public UpdateAssignment(Identifier name, Expression value) + { + this(Optional.empty(), name, value); + } + + public UpdateAssignment(NodeLocation location, Identifier name, Expression value) + { + this(Optional.of(location), name, value); + } + + private UpdateAssignment(Optional location, Identifier name, Expression value) + { + super(location); + this.name = requireNonNull(name, "name is null"); + this.value = requireNonNull(value, "value is null"); + } + + public Identifier getName() + { + return name; + } + + public Expression getValue() + { + return value; + } + + @Override + public R accept(AstVisitor visitor, C context) + { + return visitor.visitUpdateAssignment(this, context); + } + + @Override + public List getChildren() + { + return ImmutableList.of(name, value); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + UpdateAssignment other = (UpdateAssignment) obj; + return Objects.equals(name, other.name) && + Objects.equals(value, other.value); + } + + @Override + public int hashCode() + { + return Objects.hash(name, value); + } + + @Override + public String toString() + { + return format("%s = %s", name, value); + } +} diff --git a/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParser.java b/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParser.java index 3a28c5fa3437d..dc26c8cb7c3ac 100644 --- a/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParser.java +++ b/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParser.java @@ -147,6 +147,8 @@ import com.facebook.presto.sql.tree.TruncateTable; import com.facebook.presto.sql.tree.Union; import com.facebook.presto.sql.tree.Unnest; +import com.facebook.presto.sql.tree.Update; +import com.facebook.presto.sql.tree.UpdateAssignment; import com.facebook.presto.sql.tree.Use; import com.facebook.presto.sql.tree.Values; import com.facebook.presto.sql.tree.Window; @@ -1572,6 +1574,37 @@ public void testDelete() new Identifier("b"))))); } + @Test + public void testUpdate() + { + assertStatement("" + + "UPDATE foo_table\n" + + " SET bar = 23, baz = 3.1415E0, bletch = 'barf'\n" + + "WHERE (nothing = 'fun')", + new Update( + new NodeLocation(1, 1), + table(QualifiedName.of("foo_table")), + ImmutableList.of( + new UpdateAssignment(new Identifier("bar"), new LongLiteral("23")), + new UpdateAssignment(new Identifier("baz"), new DoubleLiteral("3.1415")), + new UpdateAssignment(new Identifier("bletch"), new StringLiteral("barf"))), + Optional.of(new ComparisonExpression(ComparisonExpression.Operator.EQUAL, new Identifier("nothing"), new StringLiteral("fun"))))); + } + + @Test + public void testWherelessUpdate() + { + assertStatement("" + + "UPDATE foo_table\n" + + " SET bar = 23", + new Update( + new NodeLocation(1, 1), + table(QualifiedName.of("foo_table")), + ImmutableList.of( + new UpdateAssignment(new Identifier("bar"), new LongLiteral("23"))), + Optional.empty())); + } + @Test public void testRenameTable() { diff --git a/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParserErrorHandling.java b/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParserErrorHandling.java index 7245156fbe149..dacb7065c991a 100644 --- a/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParserErrorHandling.java +++ b/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParserErrorHandling.java @@ -40,10 +40,10 @@ public Object[][] getStatements() return new Object[][] { {"", "line 1:1: mismatched input ''. Expecting: 'ALTER', 'ANALYZE', 'CALL', 'COMMIT', 'CREATE', 'DEALLOCATE', 'DELETE', 'DESC', 'DESCRIBE', 'DROP', 'EXECUTE', 'EXPLAIN', 'GRANT', " + - "'INSERT', 'PREPARE', 'REFRESH', 'RESET', 'REVOKE', 'ROLLBACK', 'SET', 'SHOW', 'START', 'TRUNCATE', 'USE', "}, + "'INSERT', 'PREPARE', 'REFRESH', 'RESET', 'REVOKE', 'ROLLBACK', 'SET', 'SHOW', 'START', 'TRUNCATE', 'UPDATE', 'USE', "}, {"@select", "line 1:1: mismatched input '@'. Expecting: 'ALTER', 'ANALYZE', 'CALL', 'COMMIT', 'CREATE', 'DEALLOCATE', 'DELETE', 'DESC', 'DESCRIBE', 'DROP', 'EXECUTE', 'EXPLAIN', 'GRANT', " + - "'INSERT', 'PREPARE', 'REFRESH', 'RESET', 'REVOKE', 'ROLLBACK', 'SET', 'SHOW', 'START', 'TRUNCATE', 'USE', "}, + "'INSERT', 'PREPARE', 'REFRESH', 'RESET', 'REVOKE', 'ROLLBACK', 'SET', 'SHOW', 'START', 'TRUNCATE', 'UPDATE', 'USE', "}, {"select * from foo where @what", "line 1:25: mismatched input '@'. Expecting: "}, {"select * from 'oops", diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/AllowAllAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/AllowAllAccessControl.java index ea682b17af422..48e2f02937b9f 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/AllowAllAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/AllowAllAccessControl.java @@ -115,6 +115,11 @@ public void checkCanTruncateTable(ConnectorTransactionHandle transaction, Connec { } + @Override + public void checkCanUpdateTableColumns(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + } + @Override public void checkCanCreateView(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName viewName) { diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/FileBasedAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/FileBasedAccessControl.java index b2775cbc3afe7..11a1a38f22861 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/FileBasedAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/FileBasedAccessControl.java @@ -38,6 +38,7 @@ import static com.facebook.presto.plugin.base.security.TableAccessControlRule.TablePrivilege.INSERT; import static com.facebook.presto.plugin.base.security.TableAccessControlRule.TablePrivilege.OWNERSHIP; import static com.facebook.presto.plugin.base.security.TableAccessControlRule.TablePrivilege.SELECT; +import static com.facebook.presto.plugin.base.security.TableAccessControlRule.TablePrivilege.UPDATE; import static com.facebook.presto.spi.security.AccessDeniedException.denyAddColumn; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateSchema; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateTable; @@ -56,6 +57,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyRevokeTablePrivilege; import static com.facebook.presto.spi.security.AccessDeniedException.denySelectTable; import static com.facebook.presto.spi.security.AccessDeniedException.denyTruncateTable; +import static com.facebook.presto.spi.security.AccessDeniedException.denyUpdateTableColumns; public class FileBasedAccessControl implements ConnectorAccessControl @@ -197,6 +199,14 @@ public void checkCanTruncateTable(ConnectorTransactionHandle transaction, Connec } } + @Override + public void checkCanUpdateTableColumns(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + if (!checkTablePermission(identity, tableName, UPDATE)) { + denyUpdateTableColumns(tableName.toString(), updatedColumns); + } + } + @Override public void checkCanCreateView(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName viewName) { diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingConnectorAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingConnectorAccessControl.java index 736c7d328cbcc..6610cb92bc273 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingConnectorAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingConnectorAccessControl.java @@ -148,6 +148,12 @@ public void checkCanTruncateTable(ConnectorTransactionHandle transactionHandle, delegate().checkCanTruncateTable(transactionHandle, identity, context, tableName); } + @Override + public void checkCanUpdateTableColumns(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + delegate().checkCanUpdateTableColumns(transactionHandle, identity, context, tableName, updatedColumns); + } + @Override public void checkCanCreateView(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName viewName) { diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingSystemAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingSystemAccessControl.java index f1f437271197d..e04b8efe93b37 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingSystemAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingSystemAccessControl.java @@ -188,6 +188,12 @@ public void checkCanTruncateTable(Identity identity, AccessControlContext contex delegate().checkCanTruncateTable(identity, context, table); } + @Override + public void checkCanUpdateTableColumns(Identity identity, AccessControlContext context, CatalogSchemaTableName table, Set updatedColumnNames) + { + delegate().checkCanUpdateTableColumns(identity, context, table, updatedColumnNames); + } + @Override public void checkCanCreateView(Identity identity, AccessControlContext context, CatalogSchemaTableName view) { diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ReadOnlyAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ReadOnlyAccessControl.java index ce64b1df71235..36fe955604718 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ReadOnlyAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ReadOnlyAccessControl.java @@ -37,6 +37,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyRenameTable; import static com.facebook.presto.spi.security.AccessDeniedException.denyRevokeTablePrivilege; import static com.facebook.presto.spi.security.AccessDeniedException.denyTruncateTable; +import static com.facebook.presto.spi.security.AccessDeniedException.denyUpdateTableColumns; public class ReadOnlyAccessControl implements ConnectorAccessControl @@ -117,6 +118,12 @@ public void checkCanDeleteFromTable(ConnectorTransactionHandle transaction, Conn denyDeleteTable(tableName.toString()); } + @Override + public void checkCanUpdateTableColumns(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + denyUpdateTableColumns(tableName.toString(), updatedColumns); + } + @Override public void checkCanTruncateTable(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/TableAccessControlRule.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/TableAccessControlRule.java index 3511ce6dbaefe..8da288c0da894 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/TableAccessControlRule.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/TableAccessControlRule.java @@ -56,6 +56,6 @@ public Optional> match(String user, SchemaTableName table) public enum TablePrivilege { - SELECT, INSERT, DELETE, OWNERSHIP, GRANT_SELECT + SELECT, INSERT, DELETE, OWNERSHIP, GRANT_SELECT, UPDATE } } diff --git a/presto-postgresql/src/test/java/com/facebook/presto/plugin/postgresql/TestPostgreSqlDistributedQueries.java b/presto-postgresql/src/test/java/com/facebook/presto/plugin/postgresql/TestPostgreSqlDistributedQueries.java index 65751c19ae000..060e20b983364 100644 --- a/presto-postgresql/src/test/java/com/facebook/presto/plugin/postgresql/TestPostgreSqlDistributedQueries.java +++ b/presto-postgresql/src/test/java/com/facebook/presto/plugin/postgresql/TestPostgreSqlDistributedQueries.java @@ -70,5 +70,11 @@ public void testDelete() // Delete is currently unsupported } + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } + // PostgreSQL specific tests should normally go in TestPostgreSqlIntegrationSmokeTest } diff --git a/presto-singlestore/src/test/java/com/facebook/presto/plugin/singlestore/TestSingleStoreDistributedQueries.java b/presto-singlestore/src/test/java/com/facebook/presto/plugin/singlestore/TestSingleStoreDistributedQueries.java index 9e89ef7a09476..6576625c70d0b 100644 --- a/presto-singlestore/src/test/java/com/facebook/presto/plugin/singlestore/TestSingleStoreDistributedQueries.java +++ b/presto-singlestore/src/test/java/com/facebook/presto/plugin/singlestore/TestSingleStoreDistributedQueries.java @@ -217,4 +217,10 @@ public void testDelete() { // Delete is currently unsupported } + + @Override + public void testUpdate() + { + // Updates are not supported by the connector + } } diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/UpdatablePageSource.java b/presto-spi/src/main/java/com/facebook/presto/spi/UpdatablePageSource.java index 97133574555da..0cc0693e13016 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/UpdatablePageSource.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/UpdatablePageSource.java @@ -13,16 +13,26 @@ */ package com.facebook.presto.spi; +import com.facebook.presto.common.Page; import com.facebook.presto.common.block.Block; import io.airlift.slice.Slice; import java.util.Collection; +import java.util.List; import java.util.concurrent.CompletableFuture; public interface UpdatablePageSource extends ConnectorPageSource { - void deleteRows(Block rowIds); + default void deleteRows(Block rowIds) + { + throw new UnsupportedOperationException("This connector does not support row-level delete"); + } + + default void updateRows(Page page, List columnValueAndRowIdChannels) + { + throw new UnsupportedOperationException("This connector does not support row update"); + } CompletableFuture> finish(); diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorAccessControl.java b/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorAccessControl.java index 497768a58e538..b3777704a6a2a 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorAccessControl.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorAccessControl.java @@ -51,6 +51,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyShowRoles; import static com.facebook.presto.spi.security.AccessDeniedException.denyShowSchemas; import static com.facebook.presto.spi.security.AccessDeniedException.denyShowTablesMetadata; +import static com.facebook.presto.spi.security.AccessDeniedException.denyUpdateTableColumns; import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toList; @@ -235,6 +236,16 @@ default void checkCanTruncateTable(ConnectorTransactionHandle transactionHandle, denyDeleteTable(tableName.toString()); } + /** + * Check if identity is allowed to update the supplied columns in the specified table in this catalog. + * + * @throws com.facebook.presto.spi.security.AccessDeniedException if not allowed + */ + default void checkCanUpdateTableColumns(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName, Set updatedColumns) + { + denyUpdateTableColumns(tableName.toString(), updatedColumns); + } + /** * Check if identity is allowed to create the specified view in this catalog. * diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorMetadata.java b/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorMetadata.java index b24df29eadae0..7cc33b02a5803 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorMetadata.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorMetadata.java @@ -517,9 +517,14 @@ default Optional finishInsert(ConnectorSession session, * These IDs will be passed to the {@code deleteRows()} method of the * {@link com.facebook.presto.spi.UpdatablePageSource} that created them. */ - default ColumnHandle getUpdateRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) + default ColumnHandle getDeleteRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) { - throw new PrestoException(NOT_SUPPORTED, "This connector does not support updates or deletes"); + throw new PrestoException(NOT_SUPPORTED, "This connector does not support deletes"); + } + + default ColumnHandle getUpdateRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle, List updatedColumns) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support updates"); } /** @@ -540,6 +545,16 @@ default void finishDelete(ConnectorSession session, ConnectorTableHandle tableHa throw new PrestoException(NOT_SUPPORTED, "This connector does not support deletes"); } + default ConnectorTableHandle beginUpdate(ConnectorSession session, ConnectorTableHandle tableHandle, List updatedColumns) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support update"); + } + + default void finishUpdate(ConnectorSession session, ConnectorTableHandle tableHandle, Collection fragments) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support update"); + } + /** * Create the specified view. The data for the view is opaque to the connector. */ diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/connector/classloader/ClassLoaderSafeConnectorMetadata.java b/presto-spi/src/main/java/com/facebook/presto/spi/connector/classloader/ClassLoaderSafeConnectorMetadata.java index 0054bfeb1235e..26e695b74581d 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/connector/classloader/ClassLoaderSafeConnectorMetadata.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/connector/classloader/ClassLoaderSafeConnectorMetadata.java @@ -470,6 +470,14 @@ public void createView(ConnectorSession session, ConnectorTableMetadata viewMeta } } + @Override + public ColumnHandle getDeleteRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) + { + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + return delegate.getDeleteRowIdColumnHandle(session, tableHandle); + } + } + @Override public void dropView(ConnectorSession session, SchemaTableName viewName) { @@ -557,10 +565,10 @@ public Optional finishRefreshMaterializedView(Connector } @Override - public ColumnHandle getUpdateRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle) + public ColumnHandle getUpdateRowIdColumnHandle(ConnectorSession session, ConnectorTableHandle tableHandle, List updatedColumns) { try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { - return delegate.getUpdateRowIdColumnHandle(session, tableHandle); + return delegate.getUpdateRowIdColumnHandle(session, tableHandle, updatedColumns); } } @@ -580,6 +588,22 @@ public void finishDelete(ConnectorSession session, ConnectorTableHandle tableHan } } + @Override + public ConnectorTableHandle beginUpdate(ConnectorSession session, ConnectorTableHandle tableHandle, List updatedColumns) + { + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + return delegate.beginUpdate(session, tableHandle, updatedColumns); + } + } + + @Override + public void finishUpdate(ConnectorSession session, ConnectorTableHandle tableHandle, Collection fragments) + { + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + delegate.finishUpdate(session, tableHandle, fragments); + } + } + @Override public boolean supportsMetadataDelete(ConnectorSession session, ConnectorTableHandle tableHandle, Optional tableLayoutHandle) { diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessControl.java b/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessControl.java index e927e99e0a6d8..3907615005bb7 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessControl.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessControl.java @@ -171,6 +171,13 @@ default AuthorizedIdentity selectAuthorizedIdentity(Identity identity, AccessCon */ void checkCanTruncateTable(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName); + /** + * Check if identity is allowed to update the specified table. + * + * @throws AccessDeniedException if not allowed + */ + void checkCanUpdateTableColumns(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName, Set updatedColumnNames); + /** * Check if identity is allowed to create the specified view. * diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessDeniedException.java b/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessDeniedException.java index d1bbf60fdacac..e0818932c9f6a 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessDeniedException.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessDeniedException.java @@ -206,6 +206,16 @@ public static void denyTruncateTable(String tableName, String extraInfo) throw new AccessDeniedException(format("Cannot truncate table %s%s", tableName, formatExtraInfo(extraInfo))); } + public static void denyUpdateTableColumns(String tableName, Set updatedColumnNames) + { + denyUpdateTableColumns(tableName, updatedColumnNames, null); + } + + public static void denyUpdateTableColumns(String tableName, Set updatedColumnNames, String extraInfo) + { + throw new AccessDeniedException(format("Cannot update columns [%s] in table %s%s", updatedColumnNames, tableName, formatExtraInfo(extraInfo))); + } + public static void denyCreateView(String viewName) { denyCreateView(viewName, null); diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/security/AllowAllAccessControl.java b/presto-spi/src/main/java/com/facebook/presto/spi/security/AllowAllAccessControl.java index c32c960b4fa67..e06259d449e03 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/security/AllowAllAccessControl.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/security/AllowAllAccessControl.java @@ -129,6 +129,11 @@ public void checkCanTruncateTable(TransactionId transactionId, Identity identity { } + @Override + public void checkCanUpdateTableColumns(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName, Set updatedColumnNames) + { + } + @Override public void checkCanCreateView(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName viewName) { diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/security/DenyAllAccessControl.java b/presto-spi/src/main/java/com/facebook/presto/spi/security/DenyAllAccessControl.java index 7a2ebc96ef6dc..0c44d88562b16 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/security/DenyAllAccessControl.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/security/DenyAllAccessControl.java @@ -57,6 +57,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyShowSchemas; import static com.facebook.presto.spi.security.AccessDeniedException.denyShowTablesMetadata; import static com.facebook.presto.spi.security.AccessDeniedException.denyTruncateTable; +import static com.facebook.presto.spi.security.AccessDeniedException.denyUpdateTableColumns; import static java.util.Collections.emptySet; import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toSet; @@ -184,6 +185,12 @@ public void checkCanTruncateTable(TransactionId transactionId, Identity identity denyTruncateTable(tableName.toString()); } + @Override + public void checkCanUpdateTableColumns(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName, Set updatedColumnNames) + { + denyUpdateTableColumns(tableName.toString(), updatedColumnNames); + } + @Override public void checkCanCreateView(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName viewName) { diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/security/SystemAccessControl.java b/presto-spi/src/main/java/com/facebook/presto/spi/security/SystemAccessControl.java index db513405fd3a8..418ac8bc47139 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/security/SystemAccessControl.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/security/SystemAccessControl.java @@ -45,6 +45,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denySetCatalogSessionProperty; import static com.facebook.presto.spi.security.AccessDeniedException.denyShowSchemas; import static com.facebook.presto.spi.security.AccessDeniedException.denyShowTablesMetadata; +import static com.facebook.presto.spi.security.AccessDeniedException.denyUpdateTableColumns; public interface SystemAccessControl { @@ -266,6 +267,16 @@ default void checkCanTruncateTable(Identity identity, AccessControlContext conte denyDeleteTable(table.toString()); } + /** + * Check if identity is allowed to update the supplied columns in the specified table in a catalog. + * + * @throws AccessDeniedException if not allowed + */ + default void checkCanUpdateTableColumns(Identity identity, AccessControlContext context, CatalogSchemaTableName table, Set updatedColumnNames) + { + denyUpdateTableColumns(table.toString(), updatedColumnNames); + } + /** * Check if identity is allowed to create the specified view in a catalog. * diff --git a/presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestDistributedQueries.java b/presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestDistributedQueries.java index 6905895c852b3..34357c4b51ab5 100644 --- a/presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestDistributedQueries.java +++ b/presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestDistributedQueries.java @@ -815,6 +815,17 @@ public void testDelete() assertAccessAllowed("DELETE FROM test_delete", privilege("orders", SELECT_COLUMN)); } + @Test + public void testUpdate() + { + assertUpdate("CREATE TABLE test_update AS SELECT * FROM orders", "SELECT count(*) FROM orders"); + + assertUpdate("UPDATE test_update SET orderstatus = 'O_UPDATED' WHERE orderstatus = 'O'", "SELECT count(*) FROM orders WHERE orderstatus = 'O'"); + assertQuery("SELECT * FROM test_update", "SELECT * FROM orders WHERE orderstatus <> 'O'"); + + assertUpdate("DROP TABLE test_update"); + } + @Test public void testDropTableIfExists() {