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 e777e219eae6e..abbdad5152454 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 @@ -21,6 +21,7 @@ import com.facebook.presto.sql.tree.Analyze; import com.facebook.presto.sql.tree.Call; import com.facebook.presto.sql.tree.Commit; +import com.facebook.presto.sql.tree.CreateBranch; import com.facebook.presto.sql.tree.CreateFunction; import com.facebook.presto.sql.tree.CreateMaterializedView; import com.facebook.presto.sql.tree.CreateRole; @@ -131,6 +132,7 @@ private StatementUtils() {} builder.put(CreateType.class, QueryType.DATA_DEFINITION); builder.put(AddColumn.class, QueryType.DATA_DEFINITION); builder.put(CreateTable.class, QueryType.DATA_DEFINITION); + builder.put(CreateBranch.class, QueryType.DATA_DEFINITION); builder.put(RenameTable.class, QueryType.DATA_DEFINITION); builder.put(RenameColumn.class, QueryType.DATA_DEFINITION); builder.put(DropColumn.class, QueryType.DATA_DEFINITION); diff --git a/presto-docs/src/main/sphinx/connector/iceberg.rst b/presto-docs/src/main/sphinx/connector/iceberg.rst index ae27a94249689..bccb0193595c5 100644 --- a/presto-docs/src/main/sphinx/connector/iceberg.rst +++ b/presto-docs/src/main/sphinx/connector/iceberg.rst @@ -1580,6 +1580,22 @@ Alter table operations are supported in the Iceberg connector:: ALTER TABLE iceberg.web.page_views DROP TAG 'tag1'; + ALTER TABLE iceberg.default.mytable CREATE BRANCH 'audit-branch'; + + ALTER TABLE iceberg.default.mytable CREATE BRANCH IF NOT EXISTS 'audit-branch'; + + ALTER TABLE iceberg.default.mytable CREATE OR REPLACE BRANCH 'audit-branch'; + + ALTER TABLE iceberg.default.mytable CREATE BRANCH 'audit-branch-system' FOR SYSTEM_VERSION AS OF 4176642711908913940; + + ALTER TABLE iceberg.default.mytable CREATE BRANCH IF NOT EXISTS 'audit-branch-system' FOR SYSTEM_VERSION AS OF 4176642711908913940; + + ALTER TABLE iceberg.default.mytable CREATE BRANCH 'audit-branch-retain' FOR SYSTEM_VERSION AS OF 4176642711908913940 RETAIN 7 DAYS; + + ALTER TABLE iceberg.default.mytable CREATE BRANCH 'audit-branch-snap-retain' FOR SYSTEM_VERSION AS OF 4176642711908913940 RETAIN 7 DAYS WITH SNAPSHOT RETENTION 2 SNAPSHOTS 2 DAYS; + + ALTER TABLE iceberg.default.mytable CREATE OR REPLACE BRANCH 'audit-branch-time' FOR SYSTEM_TIME AS OF TIMESTAMP '2026-01-02 17:30:35.247 Asia/Kolkata'; + To add a new column as a partition column, identify the transform functions for the column. The table is partitioned by the transformed value of the column:: diff --git a/presto-docs/src/main/sphinx/sql/alter-table.rst b/presto-docs/src/main/sphinx/sql/alter-table.rst index 93778714a24bb..ff3e244a5022b 100644 --- a/presto-docs/src/main/sphinx/sql/alter-table.rst +++ b/presto-docs/src/main/sphinx/sql/alter-table.rst @@ -17,6 +17,11 @@ Synopsis ALTER TABLE [ IF EXISTS ] name SET PROPERTIES (property_name=value, [, ...]) ALTER TABLE [ IF EXISTS ] name DROP BRANCH [ IF EXISTS ] branch_name ALTER TABLE [ IF EXISTS ] name DROP TAG [ IF EXISTS ] tag_name + ALTER TABLE [ IF EXISTS ] name CREATE [ OR REPLACE ] BRANCH [ IF NOT EXISTS ] branch_name + ALTER TABLE [ IF EXISTS ] name CREATE [ OR REPLACE ] BRANCH [ IF NOT EXISTS ] branch_name FOR SYSTEM_VERSION AS OF version + ALTER TABLE [ IF EXISTS ] name CREATE [ OR REPLACE ] BRANCH [ IF NOT EXISTS ] branch_name FOR SYSTEM_TIME AS OF timestamp + ALTER TABLE [ IF EXISTS ] name CREATE [ OR REPLACE ] BRANCH [ IF NOT EXISTS ] branch_name FOR SYSTEM_VERSION AS OF version RETAIN retention_period + ALTER TABLE [ IF EXISTS ] name CREATE [ OR REPLACE ] BRANCH [ IF NOT EXISTS ] branch_name FOR SYSTEM_VERSION AS OF version RETAIN retention_period WITH SNAPSHOT RETENTION min_snapshots SNAPSHOTS min_retention_period Description ----------- @@ -29,6 +34,12 @@ The optional ``IF EXISTS`` (when used before the column name) clause causes the The optional ``IF NOT EXISTS`` clause causes the error to be suppressed if the column already exists. +For ``CREATE BRANCH`` statements: + +* The optional ``OR REPLACE`` clause causes the branch to be replaced if it already exists. +* The optional ``IF NOT EXISTS`` clause causes the error to be suppressed if the branch already exists. +* ``OR REPLACE`` and ``IF NOT EXISTS`` cannot be specified together. + Examples -------- @@ -104,6 +115,38 @@ Drop tag ``tag1`` from the ``users`` table:: ALTER TABLE users DROP TAG 'tag1'; +Create branch ``branch1`` from the ``users`` table:: + + ALTER TABLE users CREATE BRANCH 'branch1'; + +Create branch ``branch1`` from the ``users`` table only if it doesn't already exist:: + + ALTER TABLE users CREATE BRANCH IF NOT EXISTS 'branch1'; + +Create or replace branch ``branch1`` from the ``users`` table:: + + ALTER TABLE users CREATE OR REPLACE BRANCH 'branch1'; + +Create branch ``branch1`` from the ``users`` table for system version as of version 5:: + + ALTER TABLE users CREATE BRANCH 'branch1' FOR SYSTEM_VERSION AS OF 5; + +Create branch ``branch1`` from the ``users`` table for system version as of version 5, only if it doesn't exist:: + + ALTER TABLE users CREATE BRANCH IF NOT EXISTS 'branch1' FOR SYSTEM_VERSION AS OF 5; + +Create or replace branch ``branch1`` from the ``users`` table for system time as of timestamp '2026-01-02 17:30:35.247 Asia/Kolkata':: + + ALTER TABLE users CREATE OR REPLACE BRANCH 'branch1' FOR SYSTEM_TIME AS OF TIMESTAMP '2026-01-02 17:30:35.247 Asia/Kolkata'; + +Create branch ``branch1`` from the ``users`` table for system version as of version 5 with retention period of 30 days:: + + ALTER TABLE users CREATE BRANCH 'branch1' FOR SYSTEM_VERSION AS OF 5 RETAIN INTERVAL 30 DAY; + +Create branch ``branch1`` from the ``users`` table for system version as of version 5 with snapshot retention of minimum 3 snapshots and minimum retention period of 7 days:: + + ALTER TABLE users CREATE BRANCH 'branch1' FOR SYSTEM_VERSION AS OF 5 RETAIN INTERVAL 7 DAY WITH SNAPSHOT RETENTION 3 SNAPSHOTS INTERVAL 7 DAYS; + See Also -------- 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 3ccb4db413a69..db645b7009b90 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 @@ -301,6 +301,11 @@ public void checkCanDropBranch(ConnectorTransactionHandle transactionHandle, Con { } + @Override + public void checkCanCreateBranch(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + } + @Override public void checkCanDropTag(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { 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 b230ffc0d8249..ed827233204c2 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 @@ -57,6 +57,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyAddColumn; import static com.facebook.presto.spi.security.AccessDeniedException.denyAddConstraint; import static com.facebook.presto.spi.security.AccessDeniedException.denyCallProcedure; +import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateBranch; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateRole; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateSchema; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateTable; @@ -269,6 +270,24 @@ public void checkCanDropBranch(ConnectorTransactionHandle transaction, Connector } } + @Override + public void checkCanCreateBranch(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + MetastoreContext metastoreContext = new MetastoreContext( + identity, context.getQueryId().getId(), + context.getClientInfo(), + context.getClientTags(), + context.getSource(), + Optional.empty(), + false, + HiveColumnConverterProvider.DEFAULT_COLUMN_CONVERTER_PROVIDER, + context.getWarningCollector(), + context.getRuntimeStats()); + if (!isTableOwner(transaction, identity, metastoreContext, tableName)) { + denyCreateBranch(tableName.toString()); + } + } + @Override public void checkCanDropTag(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { 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 f22bb65c01c30..3c1b6bf074e3b 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 @@ -300,6 +300,12 @@ public void checkCanDropBranch(ConnectorTransactionHandle transactionHandle, Con delegate.checkCanDropBranch(transactionHandle, identity, context, tableName); } + @Override + public void checkCanCreateBranch(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + delegate.checkCanCreateBranch(transactionHandle, identity, context, tableName); + } + @Override public void checkCanDropTag(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { 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 913c501b5795d..ab2d1583ce35a 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 @@ -100,6 +100,7 @@ import org.apache.iceberg.FileFormat; import org.apache.iceberg.FileMetadata; import org.apache.iceberg.IsolationLevel; +import org.apache.iceberg.ManageSnapshots; import org.apache.iceberg.MetadataColumns; import org.apache.iceberg.MetricsConfig; import org.apache.iceberg.MetricsModes.None; @@ -243,6 +244,7 @@ import static com.google.common.collect.Maps.transformValues; import static java.lang.Long.parseLong; import static java.lang.String.format; +import static java.time.Duration.ofDays; import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static org.apache.iceberg.MetadataColumns.ROW_POSITION; @@ -1046,6 +1048,50 @@ public void dropBranch(ConnectorSession session, ConnectorTableHandle tableHandl } } + @Override + public void createBranch( + ConnectorSession session, + ConnectorTableHandle tableHandle, + String branchName, + boolean replace, + boolean ifNotExists, + Optional tableVersion, + Optional retainDays, + Optional minSnapshotsToKeep, + Optional maxSnapshotAgeDays) + { + IcebergTableHandle icebergTableHandle = (IcebergTableHandle) tableHandle; + verify(icebergTableHandle.getIcebergTableName().getTableType() == DATA, "only the data table can have branch created"); + Table icebergTable = getIcebergTable(session, icebergTableHandle.getSchemaTableName()); + + boolean branchExists = icebergTable.refs().containsKey(branchName); + if (ifNotExists && branchExists) { + return; + } + long targetSnapshotId = tableVersion.map(version -> getSnapshotIdForTableVersion(icebergTable, version)) + .orElseGet(() -> { + if (icebergTable.currentSnapshot() == null) { + throw new PrestoException(NOT_FOUND, format("Table %s has no current snapshot", icebergTableHandle.getSchemaTableName().getTableName())); + } + return icebergTable.currentSnapshot().snapshotId(); + }); + ManageSnapshots manageSnapshots = icebergTable.manageSnapshots(); + if (replace && branchExists) { + manageSnapshots.replaceBranch(branchName, targetSnapshotId); + } + else if (!branchExists) { + manageSnapshots.createBranch(branchName, targetSnapshotId); + } + else { + throw new PrestoException(ALREADY_EXISTS, format("Branch %s already exists in table %s", branchName, icebergTableHandle.getSchemaTableName().getTableName())); + } + // Apply retention policies if specified + retainDays.ifPresent(retainDs -> manageSnapshots.setMaxRefAgeMs(branchName, ofDays(retainDs).toMillis())); + minSnapshotsToKeep.ifPresent(minSnapshots -> manageSnapshots.setMinSnapshotsToKeep(branchName, minSnapshots)); + maxSnapshotAgeDays.ifPresent(maxAgeDays -> manageSnapshots.setMaxSnapshotAgeMs(branchName, ofDays(maxAgeDays).toMillis())); + manageSnapshots.commit(); + } + @Override public void dropTag(ConnectorSession session, ConnectorTableHandle tableHandle, String tagName, boolean tagExists) { diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergCreateBranch.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergCreateBranch.java new file mode 100644 index 0000000000000..7f1650b6f322c --- /dev/null +++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergCreateBranch.java @@ -0,0 +1,337 @@ +/* + * 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.iceberg; + +import com.facebook.presto.Session; +import com.facebook.presto.testing.QueryRunner; +import com.facebook.presto.tests.AbstractTestQueryFramework; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import static com.facebook.presto.iceberg.CatalogType.HIVE; +import static com.facebook.presto.testing.TestingSession.testSessionBuilder; +import static java.lang.String.format; + +@Test(singleThreaded = true) +public class TestIcebergCreateBranch + extends AbstractTestQueryFramework +{ + public static final String ICEBERG_CATALOG = "iceberg"; + public static final String TEST_SCHEMA = "test_schema_branch"; + private Session session; + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + session = testSessionBuilder() + .setCatalog(ICEBERG_CATALOG) + .setSchema(TEST_SCHEMA) + .build(); + + return IcebergQueryRunner.builder() + .setCatalogType(HIVE) + .setSchemaName(TEST_SCHEMA) + .setCreateTpchTables(false) + .build().getQueryRunner(); + } + + @BeforeClass + public void setUp() + { + assertUpdate(session, format("CREATE SCHEMA IF NOT EXISTS %s", TEST_SCHEMA)); + } + + @AfterClass(alwaysRun = true) + public void tearDown() + { + assertUpdate(session, format("DROP SCHEMA IF EXISTS %s", TEST_SCHEMA)); + } + + private void createTable(String tableName) + { + assertUpdate(session, "CREATE TABLE IF NOT EXISTS " + tableName + " (id BIGINT, name VARCHAR) WITH (format = 'PARQUET')"); + assertUpdate(session, "INSERT INTO " + tableName + " VALUES (1, 'Alice'), (2, 'Bob')", 2); + } + + private void dropTable(String tableName) + { + assertQuerySucceeds(session, "DROP TABLE IF EXISTS " + TEST_SCHEMA + "." + tableName); + } + + @Test + public void testCreateBranchBasic() + { + String tableName = "create_branch_basic_table_test"; + createTable(tableName); + + try { + assertUpdate(session, "ALTER TABLE " + tableName + " CREATE BRANCH 'test_branch'"); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'test_branch' and type = 'BRANCH'", "VALUES 1"); + assertQuery(session, "SELECT count(*) FROM " + tableName + " FOR SYSTEM_VERSION AS OF 'test_branch'", "VALUES 2"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'test_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchFromVersion() + { + String tableName = "create_branch_version_table_test"; + createTable(tableName); + + try { + assertUpdate(session, "INSERT INTO " + tableName + " VALUES (3, 'Charlie')", 1); + long snapshotId = (Long) computeScalar(session, "SELECT snapshot_id FROM \"" + tableName + "$snapshots\" ORDER BY committed_at DESC LIMIT 1"); + assertUpdate(session, format("ALTER TABLE %s CREATE BRANCH 'version_branch' FOR SYSTEM_VERSION AS OF %d", tableName, snapshotId)); + assertQuery(session, "SELECT count(*) FROM " + tableName + " FOR SYSTEM_VERSION AS OF 'version_branch'", "VALUES 3"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'version_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchFromTimestamp() + { + String tableName = "create_branch_ts_table_test"; + createTable(tableName); + + try { + ZonedDateTime committedAt = (ZonedDateTime) computeScalar(session, "SELECT committed_at FROM \"" + tableName + "$snapshots\" ORDER BY committed_at DESC LIMIT 1"); + DateTimeFormatter prestoTimestamp = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS XXX"); + String timestampLiteral = committedAt.format(prestoTimestamp); + assertUpdate(session, format("ALTER TABLE %s CREATE BRANCH 'time_branch' FOR SYSTEM_TIME AS OF TIMESTAMP '%s'", tableName, timestampLiteral)); + assertQuery(session, "SELECT count(*) FROM " + tableName + " FOR SYSTEM_VERSION AS OF 'time_branch'", "VALUES 2"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'time_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchWithRetention() + { + String tableName = "create_branch_retention_table_test"; + createTable(tableName); + + try { + long snapshotId = (Long) computeScalar(session, "SELECT snapshot_id FROM \"" + tableName + "$snapshots\" ORDER BY committed_at DESC LIMIT 1"); + assertUpdate(session, format("ALTER TABLE %s CREATE BRANCH 'retention_branch' FOR SYSTEM_VERSION AS OF %d RETAIN 7 DAYS", tableName, snapshotId)); + assertQuery(session, "SELECT count(*) FROM " + tableName + " FOR SYSTEM_VERSION AS OF 'retention_branch'", "VALUES 2"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'retention_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchWithSnapshotRetention() + { + String tableName = "create_branch_snapshot_retention"; + createTable(tableName); + + try { + long snapshotId = (Long) computeScalar(session, "SELECT snapshot_id FROM \"" + tableName + "$snapshots\" ORDER BY committed_at DESC LIMIT 1"); + assertUpdate(session, format("ALTER TABLE %s CREATE BRANCH 'full_retention_branch' " + + "FOR SYSTEM_VERSION AS OF %d RETAIN 7 DAYS WITH SNAPSHOT RETENTION 2 SNAPSHOTS 2 DAYS", tableName, snapshotId)); + assertQuery(session, "SELECT count(*) FROM " + tableName + " FOR SYSTEM_VERSION AS OF 'full_retention_branch'", "VALUES 2"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'full_retention_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchDuplicate() + { + String tableName = "create_branch_duplicate_table_test"; + createTable(tableName); + + try { + assertUpdate(session, "ALTER TABLE " + tableName + " CREATE BRANCH 'duplicate_branch'"); + assertQueryFails(session, "ALTER TABLE " + tableName + " CREATE BRANCH 'duplicate_branch'", ".*Branch.*already exists.*"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'duplicate_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchWithBothVersionAndTime() + { + String tableName = "create_branch_both_table_test"; + createTable(tableName); + + try { + long snapshotId = (Long) computeScalar(session, "SELECT snapshot_id FROM \"" + tableName + "$snapshots\" ORDER BY committed_at DESC LIMIT 1"); + ZonedDateTime committedAt = (ZonedDateTime) computeScalar(session, "SELECT committed_at FROM \"" + tableName + "$snapshots\" ORDER BY committed_at DESC LIMIT 1"); + DateTimeFormatter prestoTimestamp = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS XXX"); + String timestampLiteral = committedAt.format(prestoTimestamp); + assertQueryFails(session, format("ALTER TABLE " + tableName + " CREATE BRANCH 'both_branch' FOR SYSTEM_VERSION AS OF %d FOR SYSTEM_TIME AS OF TIMESTAMP '%s'", + snapshotId, timestampLiteral), ".*mismatched input.*"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchIfNotExists() + { + String tableName = "create_branch_ne_table_test"; + createTable(tableName); + + try { + // Create branch first time - should succeed + assertUpdate(session, "ALTER TABLE " + tableName + " CREATE BRANCH IF NOT EXISTS 'if_not_exists_branch'"); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'if_not_exists_branch' and type = 'BRANCH'", "VALUES 1"); + + // Create same branch again with IF NOT EXISTS - should succeed (no-op) + assertUpdate(session, "ALTER TABLE " + tableName + " CREATE BRANCH IF NOT EXISTS 'if_not_exists_branch'"); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'if_not_exists_branch' and type = 'BRANCH'", "VALUES 1"); + + assertQuery(session, "SELECT count(*) FROM " + tableName + " FOR SYSTEM_VERSION AS OF 'if_not_exists_branch'", "VALUES 2"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'if_not_exists_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateOrReplaceBranch() + { + String tableName = "create_branch_replace_table_test"; + createTable(tableName); + + try { + // Create branch first time + assertUpdate(session, "ALTER TABLE " + tableName + " CREATE BRANCH 'or_replace_branch'"); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'or_replace_branch' and type = 'BRANCH'", "VALUES 1"); + long firstSnapshotId = (Long) computeScalar(session, "SELECT snapshot_id FROM \"" + tableName + "$refs\" where name = 'or_replace_branch'"); + // Insert more data + assertUpdate(session, "INSERT INTO " + tableName + " VALUES (4, 'David')", 1); + // Replace branch - should point to new snapshot + assertUpdate(session, "ALTER TABLE " + tableName + " CREATE OR REPLACE BRANCH 'or_replace_branch'"); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'or_replace_branch' and type = 'BRANCH'", "VALUES 1"); + long secondSnapshotId = (Long) computeScalar(session, "SELECT snapshot_id FROM \"" + tableName + "$refs\" where name = 'or_replace_branch'"); + // Verify snapshot IDs are different + if (firstSnapshotId == secondSnapshotId) { + throw new AssertionError("Expected different snapshot IDs after OR REPLACE"); + } + // Verify branch now has updated data + assertQuery(session, "SELECT count(*) FROM " + tableName + " FOR SYSTEM_VERSION AS OF 'or_replace_branch'", "VALUES 3"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'or_replace_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateOrReplaceBranchNonExistent() + { + String tableName = "create_branch_cr_ne_table_test"; + createTable(tableName); + + try { + // OR REPLACE should work even if branch doesn't exist + assertUpdate(session, "ALTER TABLE " + tableName + " CREATE OR REPLACE BRANCH 'new_or_replace_branch'"); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'new_or_replace_branch' and type = 'BRANCH'", "VALUES 1"); + assertQuery(session, "SELECT count(*) FROM " + tableName + " FOR SYSTEM_VERSION AS OF 'new_or_replace_branch'", "VALUES 2"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'new_or_replace_branch'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchWithBothReplaceAndIfNotExists() + { + // Cannot specify both OR REPLACE and IF NOT EXISTS + assertQueryFails(session, "ALTER TABLE test_table_for_branch CREATE OR REPLACE BRANCH IF NOT EXISTS 'invalid_branch'", ".*Cannot specify both OR REPLACE and IF NOT EXISTS.*"); + } + + @Test + public void testCreateBranchIfNotExistsWithRetention() + { + String tableName = "create_branch_ne_retention"; + createTable(tableName); + + try { + long snapshotId = (Long) computeScalar(session, "SELECT snapshot_id FROM \"" + tableName + "$snapshots\" ORDER BY committed_at DESC LIMIT 1"); + // Create with retention + assertUpdate(session, format("ALTER TABLE %s CREATE BRANCH IF NOT EXISTS 'retention_if_not_exists' FOR SYSTEM_VERSION AS OF %d RETAIN 7 DAYS", tableName, snapshotId)); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'retention_if_not_exists' and type = 'BRANCH'", "VALUES 1"); + // Try to create again - should be no-op + assertUpdate(session, format("ALTER TABLE %s CREATE BRANCH IF NOT EXISTS 'retention_if_not_exists' FOR SYSTEM_VERSION AS OF %d RETAIN 14 DAYS", tableName, snapshotId)); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'retention_if_not_exists' and type = 'BRANCH'", "VALUES 1"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'retention_if_not_exists'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateOrReplaceBranchWithRetention() + { + String tableName = "create_branch_cr_with_retention"; + createTable(tableName); + + try { + long snapshotId = (Long) computeScalar(session, "SELECT snapshot_id FROM \"" + tableName + "$snapshots\" ORDER BY committed_at DESC LIMIT 1"); + // Create with retention + assertUpdate(session, format("ALTER TABLE %s CREATE BRANCH 'retention_or_replace' FOR SYSTEM_VERSION AS OF %d RETAIN 7 DAYS", tableName, snapshotId)); + // Replace with different retention + assertUpdate(session, format("ALTER TABLE %s CREATE OR REPLACE BRANCH 'retention_or_replace' FOR SYSTEM_VERSION AS OF %d RETAIN 14 DAYS", tableName, snapshotId)); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'retention_or_replace' and type = 'BRANCH'", "VALUES 1"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'retention_or_replace'"); + } + finally { + dropTable(tableName); + } + } + + @Test + public void testCreateBranchIfTableExists() + { + String tableName = "create_branch_table_not_exist"; + createTable(tableName); + + try { + assertUpdate(session, "ALTER TABLE IF EXISTS " + tableName + " CREATE BRANCH 'if_exists_branch'"); + assertQuery(session, "SELECT count(*) FROM \"" + tableName + "$refs\" where name = 'if_exists_branch' and type = 'BRANCH'", "VALUES 1"); + assertUpdate(session, "ALTER TABLE " + tableName + " DROP BRANCH 'if_exists_branch'"); + + assertUpdate(session, "ALTER TABLE IF EXISTS " + tableName + " CREATE BRANCH 'should_not_fail'"); + assertQueryFails(session, "ALTER TABLE non_existent_table CREATE BRANCH 'should_fail'", "No value present"); + } + finally { + dropTable(tableName); + } + } +} diff --git a/presto-main-base/src/main/java/com/facebook/presto/execution/CreateBranchTask.java b/presto-main-base/src/main/java/com/facebook/presto/execution/CreateBranchTask.java new file mode 100644 index 0000000000000..e15ae892a922e --- /dev/null +++ b/presto-main-base/src/main/java/com/facebook/presto/execution/CreateBranchTask.java @@ -0,0 +1,140 @@ +/* + * 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.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.common.QualifiedObjectName; +import com.facebook.presto.common.type.BigintType; +import com.facebook.presto.common.type.TimestampType; +import com.facebook.presto.common.type.TimestampWithTimeZoneType; +import com.facebook.presto.common.type.Type; +import com.facebook.presto.common.type.VarcharType; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.spi.MaterializedViewDefinition; +import com.facebook.presto.spi.TableHandle; +import com.facebook.presto.spi.WarningCollector; +import com.facebook.presto.spi.connector.ConnectorTableVersion; +import com.facebook.presto.spi.security.AccessControl; +import com.facebook.presto.sql.analyzer.ExpressionAnalyzer; +import com.facebook.presto.sql.analyzer.Scope; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.CreateBranch; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.NodeRef; +import com.facebook.presto.sql.tree.Parameter; +import com.facebook.presto.sql.tree.TableVersionExpression; +import com.facebook.presto.transaction.TransactionManager; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.facebook.presto.metadata.MetadataUtil.createQualifiedObjectName; +import static com.facebook.presto.metadata.MetadataUtil.getConnectorIdOrThrow; +import static com.facebook.presto.spi.connector.ConnectorTableVersion.VersionOperator; +import static com.facebook.presto.spi.connector.ConnectorTableVersion.VersionType; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.TYPE_MISMATCH; +import static com.facebook.presto.sql.analyzer.utils.ParameterUtils.parameterExtractor; +import static com.facebook.presto.sql.planner.ExpressionInterpreter.evaluateConstantExpression; +import static com.facebook.presto.sql.tree.TableVersionExpression.TableVersionType.TIMESTAMP; +import static com.facebook.presto.sql.tree.TableVersionExpression.TableVersionType.VERSION; +import static com.google.common.util.concurrent.Futures.immediateFuture; + +public class CreateBranchTask + implements DDLDefinitionTask +{ + @Override + public String getName() + { + return "CREATE BRANCH"; + } + + @Override + public ListenableFuture execute(CreateBranch statement, TransactionManager transactionManager, Metadata metadata, AccessControl accessControl, Session session, List parameters, WarningCollector warningCollector, String query) + { + QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getTableName(), metadata); + Optional tableHandleOptional = metadata.getMetadataResolver(session).getTableHandle(tableName); + + if (statement.isTableExists() && !tableHandleOptional.isPresent()) { + return immediateFuture(null); + } + + Optional optionalMaterializedView = metadata.getMetadataResolver(session).getMaterializedView(tableName); + if (optionalMaterializedView.isPresent()) { + throw new SemanticException(NOT_SUPPORTED, statement, "'%s' is a materialized view, and create branch is not supported", tableName); + } + + getConnectorIdOrThrow(session, metadata, tableName.getCatalogName()); + accessControl.checkCanCreateBranch(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), tableName); + + if (statement.isReplace() && statement.isIfNotExists()) { + throw new SemanticException(NOT_SUPPORTED, statement, + "Cannot specify both OR REPLACE and IF NOT EXISTS in CREATE BRANCH statement"); + } + + Optional tableVersion = Optional.empty(); + + if (statement.getTableVersion().isPresent()) { + TableVersionExpression tableVersionExpr = statement.getTableVersion().get(); + Expression stateExpr = tableVersionExpr.getStateExpression(); + TableVersionExpression.TableVersionType tableVersionType = tableVersionExpr.getTableVersionType(); + TableVersionExpression.TableVersionOperator tableVersionOperator = tableVersionExpr.getTableVersionOperator(); + + Map, Expression> parameterLookup = parameterExtractor(statement, parameters); + + ExpressionAnalyzer analyzer = ExpressionAnalyzer.createConstantAnalyzer( + metadata.getFunctionAndTypeManager().getFunctionAndTypeResolver(), + session, + parameterLookup, + WarningCollector.NOOP); + analyzer.analyze(stateExpr, Scope.create()); + Type stateExprType = analyzer.getExpressionTypes().get(NodeRef.of(stateExpr)); + + if (tableVersionType == TIMESTAMP) { + if (!(stateExprType instanceof TimestampWithTimeZoneType || stateExprType instanceof TimestampType)) { + throw new SemanticException(TYPE_MISMATCH, stateExpr, + "Type %s is invalid. Supported table version AS OF/BEFORE expression type is Timestamp or Timestamp with Time Zone.", + stateExprType.getDisplayName()); + } + } + else if (tableVersionType == VERSION) { + if (!(stateExprType instanceof BigintType || stateExprType instanceof VarcharType)) { + throw new SemanticException(TYPE_MISMATCH, stateExpr, "Type %s is invalid. Supported table version AS OF/BEFORE expression type is BIGINT or VARCHAR", stateExprType.getDisplayName()); + } + } + + Object evalStateExpr = evaluateConstantExpression(stateExpr, stateExprType, metadata, session, parameterLookup); + VersionType versionType = tableVersionType == TIMESTAMP ? VersionType.TIMESTAMP : VersionType.VERSION; + VersionOperator versionOperator = tableVersionOperator == TableVersionExpression.TableVersionOperator.EQUAL + ? VersionOperator.EQUAL : VersionOperator.LESS_THAN; + + tableVersion = Optional.of(new ConnectorTableVersion(versionType, versionOperator, stateExprType, evalStateExpr)); + } + + metadata.createBranch( + session, + tableHandleOptional.get(), + statement.getBranchName(), + statement.isReplace(), + statement.isIfNotExists(), + tableVersion, + statement.getRetainDays(), + statement.getMinSnapshotsToKeep(), + statement.getMaxSnapshotAgeDays()); + + return immediateFuture(null); + } +} diff --git a/presto-main-base/src/main/java/com/facebook/presto/metadata/DelegatingMetadataManager.java b/presto-main-base/src/main/java/com/facebook/presto/metadata/DelegatingMetadataManager.java index efab521446259..93f1adecbde96 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/metadata/DelegatingMetadataManager.java +++ b/presto-main-base/src/main/java/com/facebook/presto/metadata/DelegatingMetadataManager.java @@ -714,6 +714,21 @@ public void dropBranch(Session session, TableHandle tableHandle, String branchNa delegate.dropBranch(session, tableHandle, branchName, branchExists); } + @Override + public void createBranch( + Session session, + TableHandle tableHandle, + String branchName, + boolean replace, + boolean ifNotExists, + Optional tableVersion, + Optional retainDays, + Optional minSnapshotsToKeep, + Optional maxSnapshotAgeDays) + { + delegate.createBranch(session, tableHandle, branchName, replace, ifNotExists, tableVersion, retainDays, minSnapshotsToKeep, maxSnapshotAgeDays); + } + @Override public void dropTag(Session session, TableHandle tableHandle, String tagName, boolean tagExists) { diff --git a/presto-main-base/src/main/java/com/facebook/presto/metadata/Metadata.java b/presto-main-base/src/main/java/com/facebook/presto/metadata/Metadata.java index 41c53f31d775f..e945966a80f27 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/metadata/Metadata.java +++ b/presto-main-base/src/main/java/com/facebook/presto/metadata/Metadata.java @@ -593,6 +593,16 @@ default TableLayoutFilterCoverage getTableLayoutFilterCoverage(Session session, void dropBranch(Session session, TableHandle tableHandle, String branchName, boolean branchExists); + void createBranch(Session session, + TableHandle tableHandle, + String branchName, + boolean replace, + boolean ifNotExists, + Optional tableVersion, + Optional retainDays, + Optional minSnapshotsToKeep, + Optional maxSnapshotAgeDays); + void dropTag(Session session, TableHandle tableHandle, String tagName, boolean tagExists); void dropConstraint(Session session, TableHandle tableHandle, Optional constraintName, Optional columnName); diff --git a/presto-main-base/src/main/java/com/facebook/presto/metadata/MetadataManager.java b/presto-main-base/src/main/java/com/facebook/presto/metadata/MetadataManager.java index 6c8ef22ebb878..9b10e2d0ed20f 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/metadata/MetadataManager.java +++ b/presto-main-base/src/main/java/com/facebook/presto/metadata/MetadataManager.java @@ -1746,6 +1746,14 @@ public void dropBranch(Session session, TableHandle tableHandle, String branchNa metadata.dropBranch(session.toConnectorSession(connectorId), tableHandle.getConnectorHandle(), branchName, branchExists); } + @Override + public void createBranch(Session session, TableHandle tableHandle, String branchName, boolean replace, boolean ifNotExists, Optional tableVersion, Optional retainDays, Optional minSnapshotsToKeep, Optional maxSnapshotAgeDays) + { + ConnectorId connectorId = tableHandle.getConnectorId(); + ConnectorMetadata metadata = getMetadataForWrite(session, connectorId); + metadata.createBranch(session.toConnectorSession(connectorId), tableHandle.getConnectorHandle(), branchName, replace, ifNotExists, tableVersion, retainDays, minSnapshotsToKeep, maxSnapshotAgeDays); + } + @Override public void dropTag(Session session, TableHandle tableHandle, String tagName, boolean tagExists) { diff --git a/presto-main-base/src/main/java/com/facebook/presto/security/AccessControlManager.java b/presto-main-base/src/main/java/com/facebook/presto/security/AccessControlManager.java index 6bc0a9b1a197d..32774e2f5d647 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/security/AccessControlManager.java +++ b/presto-main-base/src/main/java/com/facebook/presto/security/AccessControlManager.java @@ -863,6 +863,22 @@ public void checkCanDropBranch(TransactionId transactionId, Identity identity, A } } + @Override + public void checkCanCreateBranch(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) + { + requireNonNull(identity, "identity is null"); + requireNonNull(tableName, "tableName is null"); + + authenticationCheck(() -> checkCanAccessCatalog(identity, context, tableName.getCatalogName())); + + authorizationCheck(() -> systemAccessControl.checkCanCreateBranch(identity, context, toCatalogSchemaTableName(tableName))); + + CatalogAccessControlEntry entry = getConnectorAccessControl(transactionId, tableName.getCatalogName()); + if (entry != null) { + authorizationCheck(() -> entry.getAccessControl().checkCanCreateBranch(entry.getTransactionHandle(transactionId), identity.toConnectorIdentity(tableName.getCatalogName()), context, toSchemaTableName(tableName))); + } + } + @Override public void checkCanDropTag(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) { diff --git a/presto-main-base/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java b/presto-main-base/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java index 2b7459ef6b1c6..0cf85a59e3ec0 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java +++ b/presto-main-base/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java @@ -250,6 +250,11 @@ public void checkCanRevokeTablePrivilege(Identity identity, AccessControlContext { } + @Override + public void checkCanCreateBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) + { + } + @Override public void checkCanDropBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) { diff --git a/presto-main-base/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java b/presto-main-base/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java index d248e838ff8ad..52b7973e7d17d 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java +++ b/presto-main-base/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java @@ -57,6 +57,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyAddConstraint; import static com.facebook.presto.spi.security.AccessDeniedException.denyCallProcedure; import static com.facebook.presto.spi.security.AccessDeniedException.denyCatalogAccess; +import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateBranch; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateSchema; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateTable; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateView; @@ -479,6 +480,14 @@ public void checkCanRevokeTablePrivilege(Identity identity, AccessControlContext } } + @Override + public void checkCanCreateBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) + { + if (!canAccessCatalog(identity, table.getCatalogName(), ALL)) { + denyCreateBranch(table.toString()); + } + } + @Override public void checkCanDropBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) { diff --git a/presto-main-base/src/main/java/com/facebook/presto/security/StatsRecordingSystemAccessControl.java b/presto-main-base/src/main/java/com/facebook/presto/security/StatsRecordingSystemAccessControl.java index 6c57e4204f063..667f00c91c976 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/security/StatsRecordingSystemAccessControl.java +++ b/presto-main-base/src/main/java/com/facebook/presto/security/StatsRecordingSystemAccessControl.java @@ -710,6 +710,24 @@ public void checkCanRevokeTablePrivilege(Identity identity, AccessControlContext } } + @Override + public void checkCanCreateBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) + { + long start = System.nanoTime(); + try { + delegate.get().checkCanCreateBranch(identity, context, table); + } + catch (RuntimeException e) { + stats.checkCanCreateBranch.recordFailure(); + throw e; + } + finally { + long duration = System.nanoTime() - start; + context.getRuntimeStats().addMetricValue("systemAccessControl.checkCanCreateBranch", RuntimeUnit.NANO, duration); + stats.checkCanCreateBranch.record(duration); + } + } + @Override public void checkCanDropBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) { @@ -860,6 +878,7 @@ public static class Stats final SystemAccessControlStats checkCanDropTag = new SystemAccessControlStats(); final SystemAccessControlStats checkCanDropConstraint = new SystemAccessControlStats(); final SystemAccessControlStats checkCanAddConstraint = new SystemAccessControlStats(); + final SystemAccessControlStats checkCanCreateBranch = new SystemAccessControlStats(); final SystemAccessControlStats getRowFilters = new SystemAccessControlStats(); final SystemAccessControlStats getColumnMasks = new SystemAccessControlStats(); @@ -954,6 +973,13 @@ public SystemAccessControlStats getCheckCanCreateTable() return checkCanCreateTable; } + @Managed + @Nested + public SystemAccessControlStats getCheckCanCreateBranch() + { + return checkCanCreateBranch; + } + @Managed @Nested public SystemAccessControlStats getCheckCanSetTableProperties() diff --git a/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java b/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java index d72a1ad680bbb..a2156615b6af1 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java +++ b/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java @@ -103,6 +103,7 @@ import com.facebook.presto.sql.tree.Analyze; import com.facebook.presto.sql.tree.Call; import com.facebook.presto.sql.tree.Commit; +import com.facebook.presto.sql.tree.CreateBranch; import com.facebook.presto.sql.tree.CreateFunction; import com.facebook.presto.sql.tree.CreateMaterializedView; import com.facebook.presto.sql.tree.CreateSchema; @@ -1197,6 +1198,12 @@ protected Scope visitDropBranch(DropBranch node, Optional scope) return createAndAssignScope(node, scope); } + @Override + protected Scope visitCreateBranch(CreateBranch node, Optional scope) + { + return createAndAssignScope(node, scope); + } + @Override protected Scope visitDropTag(DropTag node, Optional scope) { diff --git a/presto-main-base/src/main/java/com/facebook/presto/util/PrestoDataDefBindingHelper.java b/presto-main-base/src/main/java/com/facebook/presto/util/PrestoDataDefBindingHelper.java index 5091a7e3b285a..6a1e5db619d15 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/util/PrestoDataDefBindingHelper.java +++ b/presto-main-base/src/main/java/com/facebook/presto/util/PrestoDataDefBindingHelper.java @@ -19,6 +19,7 @@ import com.facebook.presto.execution.AlterFunctionTask; import com.facebook.presto.execution.CallTask; import com.facebook.presto.execution.CommitTask; +import com.facebook.presto.execution.CreateBranchTask; import com.facebook.presto.execution.CreateFunctionTask; import com.facebook.presto.execution.CreateMaterializedViewTask; import com.facebook.presto.execution.CreateRoleTask; @@ -61,6 +62,7 @@ import com.facebook.presto.sql.tree.AlterFunction; import com.facebook.presto.sql.tree.Call; import com.facebook.presto.sql.tree.Commit; +import com.facebook.presto.sql.tree.CreateBranch; import com.facebook.presto.sql.tree.CreateFunction; import com.facebook.presto.sql.tree.CreateMaterializedView; import com.facebook.presto.sql.tree.CreateRole; @@ -128,6 +130,7 @@ private PrestoDataDefBindingHelper() {} dataDefBuilder.put(CreateTable.class, CreateTableTask.class); dataDefBuilder.put(RenameTable.class, RenameTableTask.class); dataDefBuilder.put(RenameColumn.class, RenameColumnTask.class); + dataDefBuilder.put(CreateBranch.class, CreateBranchTask.class); dataDefBuilder.put(DropBranch.class, DropBranchTask.class); dataDefBuilder.put(DropTag.class, DropTagTask.class); dataDefBuilder.put(DropColumn.class, DropColumnTask.class); diff --git a/presto-main-base/src/test/java/com/facebook/presto/metadata/AbstractMockMetadata.java b/presto-main-base/src/test/java/com/facebook/presto/metadata/AbstractMockMetadata.java index 7ee833b6c9529..646ba09f38345 100644 --- a/presto-main-base/src/test/java/com/facebook/presto/metadata/AbstractMockMetadata.java +++ b/presto-main-base/src/test/java/com/facebook/presto/metadata/AbstractMockMetadata.java @@ -753,6 +753,12 @@ public Set getConnectorCapabilities(Session session, Conn throw new UnsupportedOperationException(); } + @Override + public void createBranch(Session session, TableHandle tableHandle, String branchName, boolean replace, boolean ifNotExists, Optional tableVersion, Optional retainDays, Optional minSnapshotsToKeep, Optional maxSnapshotAgeDays) + { + throw new UnsupportedOperationException(); + } + @Override public void dropBranch(Session session, TableHandle tableHandle, String branchName, boolean branchExists) { diff --git a/presto-main-base/src/test/java/com/facebook/presto/security/TestAccessControlManager.java b/presto-main-base/src/test/java/com/facebook/presto/security/TestAccessControlManager.java index 480c7fb2f44f0..2815c39d9c78c 100644 --- a/presto-main-base/src/test/java/com/facebook/presto/security/TestAccessControlManager.java +++ b/presto-main-base/src/test/java/com/facebook/presto/security/TestAccessControlManager.java @@ -604,6 +604,12 @@ public void checkCanDropBranch(ConnectorTransactionHandle transactionHandle, Con throw new UnsupportedOperationException(); } + @Override + public void checkCanCreateBranch(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + throw new UnsupportedOperationException(); + } + @Override public void checkCanDropTag(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { 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 92eabfe99cb04..a89304df6d750 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 @@ -68,6 +68,11 @@ statement ALTER (COLUMN)? column=identifier DROP NOT NULL #alterColumnDropNotNull | ALTER TABLE (IF EXISTS)? tableName=qualifiedName SET PROPERTIES properties #setTableProperties + | ALTER TABLE (IF EXISTS)? tableName=qualifiedName + CREATE (OR REPLACE)? BRANCH (IF NOT EXISTS)? name=string + tableVersionExpression? + (RETAIN retainDays=INTEGER_VALUE DAYS)? + (WITH SNAPSHOT RETENTION (minSnapshots=INTEGER_VALUE SNAPSHOTS)? (maxSnapshotAge=INTEGER_VALUE DAYS)?)? #createBranch | ALTER TABLE (IF EXISTS)? tableName=qualifiedName DROP BRANCH (IF EXISTS)? name=string #dropBranch | ALTER TABLE (IF EXISTS)? tableName=qualifiedName @@ -690,7 +695,7 @@ nonReserved : ADD | ADMIN | ALL | ANALYZE | ANY | ARRAY | ASC | AT | BEFORE | BERNOULLI | BRANCH | CALL | CALLED | CASCADE | CATALOGS | COLUMN | COLUMNS | COMMENT | COMMIT | COMMITTED | COPARTITION | CURRENT | CURRENT_ROLE - | DATA | DATE | DAY | DEFINER | DESC | DESCRIPTOR | DETERMINISTIC | DISABLED | DISTRIBUTED + | DATA | DATE | DAY | DEFINER | DESC | DESCRIPTOR | DETERMINISTIC | DISABLED | DISTRIBUTED | DAYS | EMPTY | ENABLED | ENFORCED | EXCLUDING | EXPLAIN | EXTERNAL | FETCH | FILTER | FIRST | FOLLOWING | FORMAT | FUNCTION | FUNCTIONS | GRANT | GRANTED | GRANTS | GRAPHVIZ | GROUPS @@ -703,8 +708,8 @@ nonReserved | NAME | NFC | NFD | NFKC | NFKD | NO | NONE | NULLIF | NULLS | OF | OFFSET | ONLY | OPTION | ORDINALITY | OUTPUT | OVER | PARTITION | PARTITIONS | POSITION | PRECEDING | PRIMARY | PRIVILEGES | PROPERTIES | PRUNE - | RANGE | READ | REFRESH | RELY | RENAME | REPEATABLE | REPLACE | RESET | RESPECT | RESTRICT | RETURN | RETURNS | REVOKE | ROLE | ROLES | ROLLBACK | ROW | ROWS - | SCHEMA | SCHEMAS | SECOND | SECURITY | SERIALIZABLE | SESSION | SET | SETS | SQL + | RANGE | READ | REFRESH | RELY | RENAME | REPEATABLE | REPLACE | RESET | RESPECT | RESTRICT | RETAIN | RETENTION | RETURN | RETURNS | REVOKE | ROLE | ROLES | ROLLBACK | ROW | ROWS + | SCHEMA | SCHEMAS | SECOND | SECURITY | SERIALIZABLE | SESSION | SET | SETS | SNAPSHOT | SNAPSHOTS | SQL | SHOW | SOME | START | STATS | SUBSTRING | SYSTEM | SYSTEM_TIME | SYSTEM_VERSION | TABLES | TABLESAMPLE | TAG | TEMPORARY | TEXT | TIME | TIMESTAMP | TO | TRANSACTION | TRUNCATE | TRY_CAST | TYPE | UNBOUNDED | UNCOMMITTED | UNIQUE | UPDATE | USE | USER @@ -754,6 +759,7 @@ CURRENT_TIMESTAMP: 'CURRENT_TIMESTAMP'; CURRENT_USER: 'CURRENT_USER'; DATA: 'DATA'; DATE: 'DATE'; +DAYS: 'DAYS'; DAY: 'DAY'; DEALLOCATE: 'DEALLOCATE'; DEFINER: 'DEFINER'; @@ -877,6 +883,8 @@ REPLACE: 'REPLACE'; RESET: 'RESET'; RESPECT: 'RESPECT'; RESTRICT: 'RESTRICT'; +RETAIN: 'RETAIN'; +RETENTION: 'RETENTION'; RETURN: 'RETURN'; RETURNS: 'RETURNS'; REVOKE: 'REVOKE'; @@ -897,6 +905,8 @@ SESSION: 'SESSION'; SET: 'SET'; SETS: 'SETS'; SHOW: 'SHOW'; +SNAPSHOT: 'SNAPSHOT'; +SNAPSHOTS: 'SNAPSHOTS'; SOME: 'SOME'; SQL: 'SQL'; START: 'START'; 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 44a0f0fdd3e4e..6c0c291b607e5 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 @@ -27,6 +27,7 @@ import com.facebook.presto.sql.tree.ColumnDefinition; import com.facebook.presto.sql.tree.Commit; import com.facebook.presto.sql.tree.ConstraintSpecification; +import com.facebook.presto.sql.tree.CreateBranch; import com.facebook.presto.sql.tree.CreateFunction; import com.facebook.presto.sql.tree.CreateMaterializedView; import com.facebook.presto.sql.tree.CreateRole; @@ -123,6 +124,7 @@ import com.facebook.presto.sql.tree.TableFunctionInvocation; import com.facebook.presto.sql.tree.TableFunctionTableArgument; import com.facebook.presto.sql.tree.TableSubquery; +import com.facebook.presto.sql.tree.TableVersionExpression; import com.facebook.presto.sql.tree.TransactionAccessMode; import com.facebook.presto.sql.tree.TransactionMode; import com.facebook.presto.sql.tree.TruncateTable; @@ -1830,6 +1832,54 @@ protected Void visitShowRoleGrants(ShowRoleGrants node, Integer context) return null; } + @Override + protected Void visitCreateBranch(CreateBranch node, Integer indent) + { + builder.append("ALTER TABLE "); + if (node.isTableExists()) { + builder.append("IF EXISTS "); + } + builder.append(formatName(node.getTableName())) + .append(" CREATE "); + if (node.isReplace()) { + builder.append("OR REPLACE "); + } + builder.append("BRANCH "); + if (node.isIfNotExists()) { + builder.append("IF NOT EXISTS "); + } + builder.append(formatStringLiteral(node.getBranchName())); + if (node.getTableVersion().isPresent()) { + TableVersionExpression tableVersion = node.getTableVersion().get(); + builder.append(" FOR ") + .append(tableVersion.getTableVersionType().name()) + .append(tableVersion.getTableVersionOperator() == TableVersionExpression.TableVersionOperator.EQUAL ? " AS OF " : " BEFORE ") + .append(formatExpression(tableVersion.getStateExpression(), parameters)); + } + + if (node.getRetainDays().isPresent()) { + builder.append(" RETAIN ") + .append(node.getRetainDays().get()) + .append(" DAYS"); + } + + if (node.getMinSnapshotsToKeep().isPresent() || node.getMaxSnapshotAgeDays().isPresent()) { + builder.append(" WITH SNAPSHOT RETENTION"); + if (node.getMinSnapshotsToKeep().isPresent()) { + builder.append(" ") + .append(node.getMinSnapshotsToKeep().get()) + .append(" SNAPSHOTS"); + } + if (node.getMaxSnapshotAgeDays().isPresent()) { + builder.append(" ") + .append(node.getMaxSnapshotAgeDays().get()) + .append(" DAYS"); + } + } + + return null; + } + @Override protected Void visitDropBranch(DropBranch node, Integer indent) { 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 4a4e91eb576c6..21767f867ad69 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 @@ -39,6 +39,7 @@ import com.facebook.presto.sql.tree.Commit; import com.facebook.presto.sql.tree.ComparisonExpression; import com.facebook.presto.sql.tree.ConstraintSpecification; +import com.facebook.presto.sql.tree.CreateBranch; import com.facebook.presto.sql.tree.CreateFunction; import com.facebook.presto.sql.tree.CreateMaterializedView; import com.facebook.presto.sql.tree.CreateRole; @@ -605,6 +606,45 @@ public Node visitDropBranch(SqlBaseParser.DropBranchContext context) context.EXISTS().stream().anyMatch(node -> node.getSymbol().getTokenIndex() > context.BRANCH().getSymbol().getTokenIndex())); } + @Override + public Node visitCreateBranch(SqlBaseParser.CreateBranchContext context) + { + boolean tableExists = context.EXISTS().stream() + .anyMatch(node -> node.getSymbol().getTokenIndex() > context.TABLE().getSymbol().getTokenIndex() && + node.getSymbol().getTokenIndex() < context.CREATE().getSymbol().getTokenIndex()); + boolean replace = context.REPLACE() != null; + boolean ifNotExists = context.EXISTS().stream() + .anyMatch(node -> node.getSymbol().getTokenIndex() > context.BRANCH().getSymbol().getTokenIndex()); + + Optional tableVersion = context.tableVersionExpression() != null + ? Optional.of((TableVersionExpression) visit(context.tableVersionExpression())) + : Optional.empty(); + + Optional retainDays = context.retainDays != null + ? Optional.of(Long.parseLong(context.retainDays.getText())) + : Optional.empty(); + + Optional minSnapshotsToKeep = context.minSnapshots != null + ? Optional.of(Integer.parseInt(context.minSnapshots.getText())) + : Optional.empty(); + + Optional maxSnapshotAgeDays = context.maxSnapshotAge != null + ? Optional.of(Long.parseLong(context.maxSnapshotAge.getText())) + : Optional.empty(); + + return new CreateBranch( + getLocation(context), + getQualifiedName(context.tableName), + tableExists, + replace, + ifNotExists, + ((StringLiteral) visit(context.name)).getValue(), + tableVersion, + retainDays, + minSnapshotsToKeep, + maxSnapshotAgeDays); + } + @Override public Node visitDropTag(SqlBaseParser.DropTagContext 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 6d250a6e22867..28399b520c14b 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 @@ -627,6 +627,11 @@ protected R visitDropBranch(DropBranch node, C context) return visitStatement(node, context); } + protected R visitCreateBranch(CreateBranch node, C context) + { + return visitStatement(node, context); + } + protected R visitDropTag(DropTag node, C context) { return visitStatement(node, context); diff --git a/presto-parser/src/main/java/com/facebook/presto/sql/tree/CreateBranch.java b/presto-parser/src/main/java/com/facebook/presto/sql/tree/CreateBranch.java new file mode 100644 index 0000000000000..d148c3b43fcc5 --- /dev/null +++ b/presto-parser/src/main/java/com/facebook/presto/sql/tree/CreateBranch.java @@ -0,0 +1,192 @@ +/* + * 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 CreateBranch + extends Statement +{ + private final QualifiedName tableName; + private final boolean tableExists; + private final boolean replace; + private final boolean ifNotExists; + private final String branchName; + private final Optional tableVersion; + private final Optional retainDays; + private final Optional minSnapshotsToKeep; + private final Optional maxSnapshotAgeDays; + + public CreateBranch( + QualifiedName tableName, + boolean tableExists, + boolean replace, + boolean ifNotExists, + String branchName, + Optional tableVersion, + Optional retainDays, + Optional minSnapshotsToKeep, + Optional maxSnapshotAgeDays) + { + this(Optional.empty(), tableName, tableExists, replace, ifNotExists, branchName, tableVersion, retainDays, minSnapshotsToKeep, maxSnapshotAgeDays); + } + + public CreateBranch( + NodeLocation location, + QualifiedName tableName, + boolean tableExists, + boolean replace, + boolean ifNotExists, + String branchName, + Optional tableVersion, + Optional retainDays, + Optional minSnapshotsToKeep, + Optional maxSnapshotAgeDays) + { + this(Optional.of(location), tableName, tableExists, replace, ifNotExists, branchName, tableVersion, retainDays, minSnapshotsToKeep, maxSnapshotAgeDays); + } + + private CreateBranch( + Optional location, + QualifiedName tableName, + boolean tableExists, + boolean replace, + boolean ifNotExists, + String branchName, + Optional tableVersion, + Optional retainDays, + Optional minSnapshotsToKeep, + Optional maxSnapshotAgeDays) + { + super(location); + this.tableName = requireNonNull(tableName, "table is null"); + this.tableExists = tableExists; + this.replace = replace; + this.ifNotExists = ifNotExists; + this.branchName = requireNonNull(branchName, "branchName is null"); + this.tableVersion = requireNonNull(tableVersion, "tableVersion is null"); + this.retainDays = requireNonNull(retainDays, "retainDays is null"); + this.minSnapshotsToKeep = requireNonNull(minSnapshotsToKeep, "minSnapshotsToKeep is null"); + this.maxSnapshotAgeDays = requireNonNull(maxSnapshotAgeDays, "maxSnapshotAgeDays is null"); + } + + public QualifiedName getTableName() + { + return tableName; + } + + public boolean isTableExists() + { + return tableExists; + } + + public boolean isReplace() + { + return replace; + } + + public boolean isIfNotExists() + { + return ifNotExists; + } + + public String getBranchName() + { + return branchName; + } + + public Optional getTableVersion() + { + return tableVersion; + } + + public Optional getRetainDays() + { + return retainDays; + } + + public Optional getMinSnapshotsToKeep() + { + return minSnapshotsToKeep; + } + + public Optional getMaxSnapshotAgeDays() + { + return maxSnapshotAgeDays; + } + + @Override + public R accept(AstVisitor visitor, C context) + { + return visitor.visitCreateBranch(this, context); + } + + @Override + public List getChildren() + { + ImmutableList.Builder children = ImmutableList.builder(); + tableVersion.ifPresent(children::add); + return children.build(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CreateBranch that = (CreateBranch) o; + return tableExists == that.tableExists && + replace == that.replace && + ifNotExists == that.ifNotExists && + Objects.equals(tableName, that.tableName) && + Objects.equals(branchName, that.branchName) && + Objects.equals(tableVersion, that.tableVersion) && + Objects.equals(retainDays, that.retainDays) && + Objects.equals(minSnapshotsToKeep, that.minSnapshotsToKeep) && + Objects.equals(maxSnapshotAgeDays, that.maxSnapshotAgeDays); + } + + @Override + public int hashCode() + { + return Objects.hash(tableName, tableExists, replace, ifNotExists, branchName, tableVersion, retainDays, minSnapshotsToKeep, maxSnapshotAgeDays); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("table", tableName) + .add("tableExists", tableExists) + .add("replace", replace) + .add("ifNotExists", ifNotExists) + .add("branchName", branchName) + .add("tableVersion", tableVersion) + .add("retainDays", retainDays) + .add("minSnapshotsToKeep", minSnapshotsToKeep) + .add("maxSnapshotAgeDays", maxSnapshotAgeDays) + .toString(); + } +} 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 8586b26022072..a2bef9372ef2e 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 @@ -36,6 +36,7 @@ import com.facebook.presto.sql.tree.Commit; import com.facebook.presto.sql.tree.ComparisonExpression; import com.facebook.presto.sql.tree.ConstraintSpecification; +import com.facebook.presto.sql.tree.CreateBranch; import com.facebook.presto.sql.tree.CreateFunction; import com.facebook.presto.sql.tree.CreateMaterializedView; import com.facebook.presto.sql.tree.CreateRole; @@ -2877,6 +2878,45 @@ public void testDropBranch() assertStatement("ALTER TABLE IF EXISTS foo.t DROP BRANCH IF EXISTS 'cons'", new DropBranch(QualifiedName.of("foo", "t"), "cons", true, true)); } + @Test + public void testCreateBranch() + { + assertStatement("ALTER TABLE foo.t CREATE BRANCH 'test_branch'", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, false, "test_branch", Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE IF EXISTS foo.t CREATE BRANCH 'test_branch'", + new CreateBranch(QualifiedName.of("foo", "t"), true, false, false, "test_branch", Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE BRANCH IF NOT EXISTS 'test_branch'", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, true, "test_branch", Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE OR REPLACE BRANCH 'test_branch'", + new CreateBranch(QualifiedName.of("foo", "t"), false, true, false, "test_branch", Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE IF EXISTS foo.t CREATE OR REPLACE BRANCH 'test_branch'", + new CreateBranch(QualifiedName.of("foo", "t"), true, true, false, "test_branch", Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE BRANCH 'test_branch' FOR SYSTEM_VERSION AS OF 123", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, false, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE BRANCH IF NOT EXISTS 'test_branch' FOR SYSTEM_VERSION AS OF 123", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, true, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE OR REPLACE BRANCH 'test_branch' FOR SYSTEM_VERSION AS OF 123", + new CreateBranch(QualifiedName.of("foo", "t"), false, true, false, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE BRANCH 'test_branch' FOR SYSTEM_TIME AS OF TIMESTAMP '2024-01-01 00:00:00'", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, false, "test_branch", Optional.of(new TableVersionExpression(TIMESTAMP, TableVersionExpression.TableVersionOperator.EQUAL, new TimestampLiteral("2024-01-01 00:00:00"))), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE BRANCH IF NOT EXISTS 'test_branch' FOR SYSTEM_TIME AS OF TIMESTAMP '2024-01-01 00:00:00'", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, true, "test_branch", Optional.of(new TableVersionExpression(TIMESTAMP, TableVersionExpression.TableVersionOperator.EQUAL, new TimestampLiteral("2024-01-01 00:00:00"))), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE OR REPLACE BRANCH 'test_branch' FOR SYSTEM_TIME AS OF TIMESTAMP '2024-01-01 00:00:00'", + new CreateBranch(QualifiedName.of("foo", "t"), false, true, false, "test_branch", Optional.of(new TableVersionExpression(TIMESTAMP, TableVersionExpression.TableVersionOperator.EQUAL, new TimestampLiteral("2024-01-01 00:00:00"))), Optional.empty(), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE BRANCH 'test_branch' FOR SYSTEM_VERSION AS OF 123 RETAIN 7 DAYS", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, false, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.of(7L), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE BRANCH IF NOT EXISTS 'test_branch' FOR SYSTEM_VERSION AS OF 123 RETAIN 7 DAYS", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, true, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.of(7L), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE OR REPLACE BRANCH 'test_branch' FOR SYSTEM_VERSION AS OF 123 RETAIN 7 DAYS", + new CreateBranch(QualifiedName.of("foo", "t"), false, true, false, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.of(7L), Optional.empty(), Optional.empty())); + assertStatement("ALTER TABLE foo.t CREATE BRANCH 'test_branch' FOR SYSTEM_VERSION AS OF 123 RETAIN 7 DAYS WITH SNAPSHOT RETENTION 2 SNAPSHOTS 3 DAYS", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, false, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.of(7L), Optional.of(2), Optional.of(3L))); + assertStatement("ALTER TABLE foo.t CREATE BRANCH IF NOT EXISTS 'test_branch' FOR SYSTEM_VERSION AS OF 123 RETAIN 7 DAYS WITH SNAPSHOT RETENTION 2 SNAPSHOTS 3 DAYS", + new CreateBranch(QualifiedName.of("foo", "t"), false, false, true, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.of(7L), Optional.of(2), Optional.of(3L))); + assertStatement("ALTER TABLE foo.t CREATE OR REPLACE BRANCH 'test_branch' FOR SYSTEM_VERSION AS OF 123 RETAIN 7 DAYS WITH SNAPSHOT RETENTION 2 SNAPSHOTS 3 DAYS", + new CreateBranch(QualifiedName.of("foo", "t"), false, true, false, "test_branch", Optional.of(new TableVersionExpression(VERSION, TableVersionExpression.TableVersionOperator.EQUAL, new LongLiteral("123"))), Optional.of(7L), Optional.of(2), Optional.of(3L))); + } + @Test public void testDropTag() { diff --git a/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestStatementBuilder.java b/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestStatementBuilder.java index e684a3d9395c4..ba65d7a4430ac 100644 --- a/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestStatementBuilder.java +++ b/presto-parser/src/test/java/com/facebook/presto/sql/parser/TestStatementBuilder.java @@ -209,6 +209,21 @@ public void testStatementBuilder() printStatement("alter table a.b.c drop column x"); printStatement("alter table a.b.c drop branch 'x'"); + printStatement("alter table a.b.c create branch 'test_branch'"); + printStatement("alter table a.b.c create branch if not exists 'test_branch'"); + printStatement("alter table a.b.c create or replace branch 'test_branch'"); + printStatement("alter table a.b.c create branch 'test_branch' for system_version as of 123"); + printStatement("alter table a.b.c create branch if not exists 'test_branch' for system_version as of 123"); + printStatement("alter table a.b.c create or replace branch 'test_branch' for system_version as of 123"); + printStatement("alter table a.b.c create branch 'test_branch' for system_time as of timestamp '2024-01-01 00:00:00'"); + printStatement("alter table a.b.c create branch if not exists 'test_branch' for system_time as of timestamp '2024-01-01 00:00:00'"); + printStatement("alter table a.b.c create or replace branch 'test_branch' for system_time as of timestamp '2024-01-01 00:00:00'"); + printStatement("alter table a.b.c create branch 'test_branch' for system_version as of 123 retain 7 days"); + printStatement("alter table a.b.c create branch if not exists 'test_branch' for system_version as of 123 retain 7 days"); + printStatement("alter table a.b.c create or replace branch 'test_branch' for system_version as of 123 retain 7 days"); + printStatement("alter table a.b.c create branch 'test_branch' for system_version as of 123 retain 7 days with snapshot retention 2 snapshots 3 days"); + printStatement("alter table a.b.c create branch if not exists 'test_branch' for system_version as of 123 retain 7 days with snapshot retention 2 snapshots 3 days"); + printStatement("alter table a.b.c create or replace branch 'test_branch' for system_version as of 123 retain 7 days with snapshot retention 2 snapshots 3 days"); printStatement("alter table a.b.c drop tag 'testTag'"); printStatement("create schema test"); 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 c1581f12ba5ee..949dc07a5880a 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 @@ -232,6 +232,11 @@ public void checkCanDropBranch(ConnectorTransactionHandle transactionHandle, Con { } + @Override + public void checkCanCreateBranch(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + } + @Override public void checkCanDropTag(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { 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 bb6db20397a61..5f32a10244386 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 @@ -46,6 +46,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyAddColumn; import static com.facebook.presto.spi.security.AccessDeniedException.denyAddConstraint; import static com.facebook.presto.spi.security.AccessDeniedException.denyCallProcedure; +import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateBranch; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateSchema; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateTable; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateView; @@ -378,6 +379,14 @@ public void checkCanDropBranch(ConnectorTransactionHandle transactionHandle, Con } } + @Override + public void checkCanCreateBranch(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + if (!checkTablePermission(identity, tableName, OWNERSHIP)) { + denyCreateBranch(tableName.toString()); + } + } + @Override public void checkCanDropTag(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { 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 df55228520809..1e5b6d44e5549 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 @@ -284,6 +284,12 @@ public void checkCanDropBranch(ConnectorTransactionHandle transactionHandle, Con delegate().checkCanDropBranch(transactionHandle, identity, context, tableName); } + @Override + public void checkCanCreateBranch(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + delegate().checkCanCreateBranch(transactionHandle, identity, context, tableName); + } + @Override public void checkCanDropTag(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { 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 5df3cc4947b76..fb89baf79faac 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 @@ -272,6 +272,12 @@ public void checkCanRevokeTablePrivilege(Identity identity, AccessControlContext delegate().checkCanRevokeTablePrivilege(identity, context, privilege, table, revokee, grantOptionFor); } + @Override + public void checkCanCreateBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) + { + delegate().checkCanCreateBranch(identity, context, table); + } + @Override public void checkCanDropBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) { 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 f3fccb0840d64..4154682d8f512 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 @@ -31,6 +31,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyAddColumn; import static com.facebook.presto.spi.security.AccessDeniedException.denyAddConstraint; import static com.facebook.presto.spi.security.AccessDeniedException.denyCallProcedure; +import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateBranch; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateRole; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateSchema; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateTable; @@ -447,6 +448,16 @@ default void checkCanDropBranch(ConnectorTransactionHandle transactionHandle, Co denyDropBranch(tableName.toString()); } + /** + * Check if identity is allowed to create branch from the specified table in this catalog. + * + * @throws com.facebook.presto.spi.security.AccessDeniedException if not allowed + */ + default void checkCanCreateBranch(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + denyCreateBranch(tableName.toString()); + } + /** * Check if identity is allowed to drop tag from the specified table 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 65ce146a9fa3f..f2f262529f2da 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 @@ -968,6 +968,23 @@ default void dropBranch(ConnectorSession session, ConnectorTableHandle tableHand throw new PrestoException(NOT_SUPPORTED, "This connector does not support dropping table branches"); } + /** + * Create a branch for the specified table + */ + default void createBranch( + ConnectorSession session, + ConnectorTableHandle tableHandle, + String branchName, + boolean replace, + boolean ifNotExists, + Optional tableVersion, + Optional retainDays, + Optional minSnapshotsToKeep, + Optional maxSnapshotAgeDays) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support creating table branches"); + } + /** * Drop the specified tag */ 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 a1e2333806d3b..c064eae378178 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 @@ -876,6 +876,23 @@ public void dropBranch(ConnectorSession session, ConnectorTableHandle tableHandl } } + @Override + public void createBranch( + ConnectorSession session, + ConnectorTableHandle tableHandle, + String branchName, + boolean replace, + boolean ifNotExists, + Optional tableVersion, + Optional retainDays, + Optional minSnapshotsToKeep, + Optional maxSnapshotAgeDays) + { + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + delegate.createBranch(session, tableHandle, branchName, replace, ifNotExists, tableVersion, retainDays, minSnapshotsToKeep, maxSnapshotAgeDays); + } + } + @Override public void dropTag(ConnectorSession session, ConnectorTableHandle tableHandle, String tagName, boolean tagExists) { 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 ae63db812f477..769bb24484ed2 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 @@ -350,6 +350,13 @@ default AuthorizedIdentity selectAuthorizedIdentity(Identity identity, AccessCon */ void checkCanDropBranch(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName); + /** + * Check if identity is allowed to create branch for the specified table. + * + * @throws com.facebook.presto.spi.security.AccessDeniedException if not allowed + */ + void checkCanCreateBranch(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName); + /** * Check if identity is allowed to drop tag from the specified table. * 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 f55b1b8e9e70b..af8b69e4e8615 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 @@ -407,6 +407,16 @@ public static void denySetRole(String role) throw new AccessDeniedException(format("Cannot set role %s", role)); } + public static void denyCreateBranch(String tableName) + { + denyCreateBranch(tableName, null); + } + + public static void denyCreateBranch(String tableName, String extraInfo) + { + throw new AccessDeniedException(format("Cannot create branch on table %s%s", tableName, formatExtraInfo(extraInfo))); + } + public static void denyDropBranch(String tableName) { denyDropBranch(tableName, 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 987ab6bf1720a..d5266c56a67ec 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 @@ -255,6 +255,11 @@ public void checkCanDropBranch(TransactionId transactionId, Identity identity, A { } + @Override + public void checkCanCreateBranch(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) + { + } + @Override public void checkCanDropTag(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) { 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 3d6030d91532d..0f1c5af494af1 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 @@ -33,6 +33,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyAddConstraint; import static com.facebook.presto.spi.security.AccessDeniedException.denyCallProcedure; import static com.facebook.presto.spi.security.AccessDeniedException.denyCatalogAccess; +import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateBranch; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateRole; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateSchema; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateTable; @@ -344,6 +345,12 @@ public void checkCanDropBranch(TransactionId transactionId, Identity identity, A denyDropBranch(tableName.toString()); } + @Override + public void checkCanCreateBranch(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) + { + denyCreateBranch(tableName.toString()); + } + @Override public void checkCanDropTag(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) { 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 fdc937e996ba4..fa05eacd72300 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 @@ -33,6 +33,7 @@ import static com.facebook.presto.spi.security.AccessDeniedException.denyAddConstraint; import static com.facebook.presto.spi.security.AccessDeniedException.denyCallProcedure; import static com.facebook.presto.spi.security.AccessDeniedException.denyCatalogAccess; +import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateBranch; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateSchema; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateTable; import static com.facebook.presto.spi.security.AccessDeniedException.denyCreateView; @@ -413,6 +414,16 @@ default void checkCanRevokeTablePrivilege(Identity identity, AccessControlContex denyRevokeTablePrivilege(privilege.toString(), table.toString()); } + /** + * Check if identity is allowed to create branch on the specified table in a catalog. + * + * @throws com.facebook.presto.spi.security.AccessDeniedException if not allowed + */ + default void checkCanCreateBranch(Identity identity, AccessControlContext context, CatalogSchemaTableName table) + { + denyCreateBranch(table.toString()); + } + /** * Check if identity is allowed to drop branch from the specified table in a catalog. *