diff --git a/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java b/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java index 15600c67239d..8274cec20188 100644 --- a/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java +++ b/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java @@ -21,6 +21,7 @@ import io.trino.connector.MockConnectorFactory; import io.trino.execution.warnings.WarningCollector; import io.trino.metadata.AbstractMockMetadata; +import io.trino.metadata.ColumnPropertyManager; import io.trino.metadata.MaterializedViewDefinition; import io.trino.metadata.MaterializedViewPropertyManager; import io.trino.metadata.MetadataManager; @@ -74,6 +75,7 @@ import static com.google.common.base.Verify.verifyNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.collect.MoreCollectors.onlyElement; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static io.trino.metadata.MetadataManager.createTestMetadataManager; import static io.trino.spi.StandardErrorCode.ALREADY_EXISTS; @@ -92,6 +94,9 @@ public abstract class BaseDataDefinitionTaskTest { public static final String SCHEMA = "schema"; + protected static final String COLUMN_PROPERTY_NAME = "column_property"; + protected static final Long COLUMN_PROPERTY_DEFAULT_VALUE = null; + protected static final String MATERIALIZED_VIEW_PROPERTY_1_NAME = "property1"; protected static final Long MATERIALIZED_VIEW_PROPERTY_1_DEFAULT_VALUE = null; @@ -102,6 +107,7 @@ public abstract class BaseDataDefinitionTaskTest protected Session testSession; protected MockMetadata metadata; protected PlannerContext plannerContext; + protected ColumnPropertyManager columnPropertyManager; protected MaterializedViewPropertyManager materializedViewPropertyManager; protected TransactionManager transactionManager; protected QueryStateMachine queryStateMachine; @@ -118,6 +124,9 @@ public void setUp() metadata = new MockMetadata(TEST_CATALOG_NAME); plannerContext = plannerContextBuilder().withMetadata(metadata).build(); + Map> columnProperties = ImmutableMap.of( + COLUMN_PROPERTY_NAME, longProperty(COLUMN_PROPERTY_NAME, "column_property 1", COLUMN_PROPERTY_DEFAULT_VALUE, false)); + columnPropertyManager = new ColumnPropertyManager(CatalogServiceProvider.singleton(TEST_CATALOG_HANDLE, columnProperties)); Map> properties = ImmutableMap.of( MATERIALIZED_VIEW_PROPERTY_1_NAME, longProperty(MATERIALIZED_VIEW_PROPERTY_1_NAME, "property 1", MATERIALIZED_VIEW_PROPERTY_1_DEFAULT_VALUE, false), MATERIALIZED_VIEW_PROPERTY_2_NAME, stringProperty(MATERIALIZED_VIEW_PROPERTY_2_NAME, "property 2", MATERIALIZED_VIEW_PROPERTY_2_DEFAULT_VALUE, false)); @@ -316,6 +325,31 @@ public void renameTable(Session session, TableHandle tableHandle, CatalogSchemaT tables.remove(oldTableName); } + @Override + public void addColumn(Session session, TableHandle tableHandle, ColumnMetadata column) + { + SchemaTableName tableName = getTableName(tableHandle); + ConnectorTableMetadata metadata = tables.get(tableName); + + ImmutableList.Builder columns = ImmutableList.builderWithExpectedSize(metadata.getColumns().size() + 1); + columns.addAll(metadata.getColumns()); + columns.add(column); + tables.put(tableName, new ConnectorTableMetadata(tableName, columns.build())); + } + + @Override + public void dropColumn(Session session, TableHandle tableHandle, ColumnHandle columnHandle) + { + SchemaTableName tableName = getTableName(tableHandle); + ConnectorTableMetadata metadata = tables.get(tableName); + String columnName = ((TestingColumnHandle) columnHandle).getName(); + + List columns = metadata.getColumns().stream() + .filter(column -> !column.getName().equals(columnName)) + .collect(toImmutableList()); + tables.put(tableName, new ConnectorTableMetadata(tableName, columns)); + } + @Override public void setColumnType(Session session, TableHandle tableHandle, ColumnHandle columnHandle, Type type) { @@ -354,6 +388,15 @@ public Map getColumnHandles(Session session, TableHandle t column -> new TestingColumnHandle(column.getName()))); } + @Override + public ColumnMetadata getColumnMetadata(Session session, TableHandle tableHandle, ColumnHandle columnHandle) + { + String columnName = ((TestingColumnHandle) columnHandle).getName(); + return getTableMetadata(tableHandle).getColumns().stream() + .filter(column -> column.getName().equals(columnName)) + .collect(onlyElement()); + } + @Override public Optional getMaterializedView(Session session, QualifiedObjectName viewName) { diff --git a/core/trino-main/src/test/java/io/trino/execution/TestAddColumnTask.java b/core/trino-main/src/test/java/io/trino/execution/TestAddColumnTask.java new file mode 100644 index 000000000000..3bae379398bb --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/execution/TestAddColumnTask.java @@ -0,0 +1,181 @@ +/* + * 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 io.trino.execution; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import io.trino.execution.warnings.WarningCollector; +import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.TableHandle; +import io.trino.security.AllowAllAccessControl; +import io.trino.spi.connector.ColumnMetadata; +import io.trino.spi.type.Type; +import io.trino.sql.tree.AddColumn; +import io.trino.sql.tree.ColumnDefinition; +import io.trino.sql.tree.Identifier; +import io.trino.sql.tree.LongLiteral; +import io.trino.sql.tree.Property; +import io.trino.sql.tree.QualifiedName; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.collect.MoreCollectors.onlyElement; +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static io.trino.spi.StandardErrorCode.COLUMN_ALREADY_EXISTS; +import static io.trino.spi.StandardErrorCode.TABLE_NOT_FOUND; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.sql.analyzer.TypeSignatureTranslator.toSqlType; +import static io.trino.testing.TestingHandles.TEST_CATALOG_NAME; +import static io.trino.testing.assertions.TrinoExceptionAssert.assertTrinoExceptionThrownBy; +import static org.assertj.core.api.Assertions.assertThat; + +@Test(singleThreaded = true) +public class TestAddColumnTask + extends BaseDataDefinitionTaskTest +{ + @Test + public void testAddColumn() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("test", BIGINT)); + + getFutureValue(executeAddColumn(asQualifiedName(tableName), new Identifier("new_col"), INTEGER, Optional.empty(), false, false)); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("test", BIGINT), new ColumnMetadata("new_col", INTEGER)); + } + + @Test + public void testAddColumnWithComment() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + + getFutureValue(executeAddColumn(asQualifiedName(tableName), new Identifier("new_col"), INTEGER, Optional.of("test comment"), false, false)); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly( + new ColumnMetadata("test", BIGINT), + ColumnMetadata.builder() + .setName("new_col") + .setType(INTEGER) + .setComment(Optional.of("test comment")) + .build()); + } + + @Test + public void testAddColumnWithColumnProperty() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + Property columnProperty = new Property(new Identifier("column_property"), new LongLiteral("111")); + + getFutureValue(executeAddColumn(asQualifiedName(tableName), new Identifier("new_col"), INTEGER, ImmutableList.of(columnProperty), false, false)); + ColumnMetadata columnMetadata = metadata.getTableMetadata(testSession, table).getColumns().stream() + .filter(column -> column.getName().equals("new_col")) + .collect(onlyElement()); + assertThat(columnMetadata.getProperties()).containsExactly(Map.entry("column_property", 111L)); + } + + @Test + public void testAddColumnNotExistingTable() + { + QualifiedObjectName tableName = qualifiedObjectName("not_existing_table"); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeAddColumn(asQualifiedName(tableName), new Identifier("test"), INTEGER, Optional.empty(), false, false))) + .hasErrorCode(TABLE_NOT_FOUND) + .hasMessageContaining("Table '%s' does not exist", tableName); + } + + @Test + public void testAddColumnNotExistingTableIfExists() + { + QualifiedName tableName = qualifiedName("not_existing_table"); + + getFutureValue(executeAddColumn(tableName, new Identifier("test"), INTEGER, Optional.empty(), true, false)); + // no exception + } + + @Test + public void testAddColumnNotExists() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("test", BIGINT)); + + getFutureValue(executeAddColumn(asQualifiedName(tableName), new Identifier("test"), INTEGER, Optional.empty(), false, true)); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("test", BIGINT)); + } + + @Test + public void testAddColumnAlreadyExist() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), false); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeAddColumn(asQualifiedName(tableName), new Identifier("test"), INTEGER, Optional.empty(), false, false))) + .hasErrorCode(COLUMN_ALREADY_EXISTS) + .hasMessage("Column 'test' already exists"); + } + + @Test + public void testAddColumnOnView() + { + QualifiedObjectName viewName = qualifiedObjectName("existing_view"); + metadata.createView(testSession, viewName, someView(), false); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeAddColumn(asQualifiedName(viewName), new Identifier("test"), INTEGER, Optional.empty(), false, false))) + .hasErrorCode(TABLE_NOT_FOUND) + .hasMessageContaining("Table '%s' does not exist", viewName); + } + + @Test + public void testAddColumnOnMaterializedView() + { + QualifiedObjectName materializedViewName = qualifiedObjectName("existing_materialized_view"); + metadata.createMaterializedView(testSession, QualifiedObjectName.valueOf(materializedViewName.toString()), someMaterializedView(), false, false); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeAddColumn(asQualifiedName(materializedViewName), new Identifier("test"), INTEGER, Optional.empty(), false, false))) + .hasErrorCode(TABLE_NOT_FOUND) + .hasMessageContaining("Table '%s' does not exist", materializedViewName); + } + + private ListenableFuture executeAddColumn(QualifiedName table, Identifier column, Type type, Optional comment, boolean tableExists, boolean columnNotExists) + { + ColumnDefinition columnDefinition = new ColumnDefinition(column, toSqlType(type), true, ImmutableList.of(), comment); + return executeAddColumn(table, columnDefinition, tableExists, columnNotExists); + } + + private ListenableFuture executeAddColumn(QualifiedName table, Identifier column, Type type, List properties, boolean tableExists, boolean columnNotExists) + { + ColumnDefinition columnDefinition = new ColumnDefinition(column, toSqlType(type), true, properties, Optional.empty()); + return executeAddColumn(table, columnDefinition, tableExists, columnNotExists); + } + + private ListenableFuture executeAddColumn(QualifiedName table, ColumnDefinition columnDefinition, boolean tableExists, boolean columnNotExists) + { + return new AddColumnTask(plannerContext, new AllowAllAccessControl(), columnPropertyManager) + .execute(new AddColumn(table, columnDefinition, tableExists, columnNotExists), queryStateMachine, ImmutableList.of(), WarningCollector.NOOP); + } +} diff --git a/core/trino-main/src/test/java/io/trino/execution/TestDropColumnTask.java b/core/trino-main/src/test/java/io/trino/execution/TestDropColumnTask.java new file mode 100644 index 000000000000..b1492f1a93eb --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/execution/TestDropColumnTask.java @@ -0,0 +1,182 @@ +/* + * 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 io.trino.execution; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import io.trino.execution.warnings.WarningCollector; +import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.TableHandle; +import io.trino.security.AllowAllAccessControl; +import io.trino.spi.connector.ColumnMetadata; +import io.trino.spi.connector.ConnectorTableMetadata; +import io.trino.spi.type.RowType; +import io.trino.spi.type.RowType.Field; +import io.trino.sql.tree.DropColumn; +import io.trino.sql.tree.QualifiedName; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static io.trino.spi.StandardErrorCode.COLUMN_NOT_FOUND; +import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; +import static io.trino.spi.StandardErrorCode.TABLE_NOT_FOUND; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.testing.TestingHandles.TEST_CATALOG_NAME; +import static io.trino.testing.assertions.TrinoExceptionAssert.assertTrinoExceptionThrownBy; +import static org.assertj.core.api.Assertions.assertThat; + +@Test(singleThreaded = true) +public class TestDropColumnTask + extends BaseDataDefinitionTaskTest +{ + @Test + public void testDropColumn() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, simpleTable(tableName), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("a", BIGINT), new ColumnMetadata("b", BIGINT)); + + getFutureValue(executeDropColumn(asQualifiedName(tableName), QualifiedName.of("b"), false, false)); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("a", BIGINT)); + } + + @Test + public void testDropOnlyColumn() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("test", BIGINT)); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeDropColumn(asQualifiedName(tableName), QualifiedName.of("test"), false, false))) + .hasErrorCode(NOT_SUPPORTED) + .hasMessageContaining("Cannot drop the only column in a table"); + } + + @Test + public void testDropColumnNotExistingTable() + { + QualifiedObjectName tableName = qualifiedObjectName("not_existing_table"); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeDropColumn(asQualifiedName(tableName), QualifiedName.of("test"), false, false))) + .hasErrorCode(TABLE_NOT_FOUND) + .hasMessageContaining("Table '%s' does not exist", tableName); + } + + @Test + public void testDropColumnNotExistingTableIfExists() + { + QualifiedName tableName = qualifiedName("not_existing_table"); + + getFutureValue(executeDropColumn(tableName, QualifiedName.of("test"), true, false)); + // no exception + } + + @Test + public void testDropMissingColumn() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, simpleTable(tableName), false); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeDropColumn(asQualifiedName(tableName), QualifiedName.of("missing_column"), false, false))) + .hasErrorCode(COLUMN_NOT_FOUND) + .hasMessageContaining("Column 'missing_column' does not exist"); + } + + @Test + public void testDropColumnIfExists() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, simpleTable(tableName), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + + getFutureValue(executeDropColumn(asQualifiedName(tableName), QualifiedName.of("c"), false, true)); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("a", BIGINT), new ColumnMetadata("b", BIGINT)); + } + + @Test + public void testUnsupportedDropDuplicatedField() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, rowTable(tableName, new Field(Optional.of("a"), BIGINT), new Field(Optional.of("a"), BIGINT)), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .isEqualTo(ImmutableList.of(new ColumnMetadata("col", RowType.rowType( + new Field(Optional.of("a"), BIGINT), new Field(Optional.of("a"), BIGINT))))); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeDropColumn(asQualifiedName(tableName), QualifiedName.of("col", "a"), false, false))) + .hasErrorCode(COLUMN_NOT_FOUND) + .hasMessageContaining("Field path [a] within row(a bigint, a bigint) is ambiguous"); + } + + @Test + public void testUnsupportedDropOnlyField() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, rowTable(tableName, new Field(Optional.of("a"), BIGINT)), false); + TableHandle table = metadata.getTableHandle(testSession, tableName).get(); + assertThat(metadata.getTableMetadata(testSession, table).getColumns()) + .containsExactly(new ColumnMetadata("col", RowType.rowType(new Field(Optional.of("a"), BIGINT)))); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeDropColumn(asQualifiedName(tableName), QualifiedName.of("col", "a"), false, false))) + .hasErrorCode(NOT_SUPPORTED) + .hasMessageContaining("Cannot drop the only field in a row type"); + } + + @Test + public void testDropColumnOnView() + { + QualifiedObjectName viewName = qualifiedObjectName("existing_view"); + metadata.createView(testSession, viewName, someView(), false); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeDropColumn(asQualifiedName(viewName), QualifiedName.of("test"), false, false))) + .hasErrorCode(TABLE_NOT_FOUND) + .hasMessageContaining("Table '%s' does not exist", viewName); + } + + @Test + public void testDropColumnOnMaterializedView() + { + QualifiedObjectName materializedViewName = qualifiedObjectName("existing_materialized_view"); + metadata.createMaterializedView(testSession, QualifiedObjectName.valueOf(materializedViewName.toString()), someMaterializedView(), false, false); + + assertTrinoExceptionThrownBy(() -> getFutureValue(executeDropColumn(asQualifiedName(materializedViewName), QualifiedName.of("test"), false, false))) + .hasErrorCode(TABLE_NOT_FOUND) + .hasMessageContaining("Table '%s' does not exist", materializedViewName); + } + + private ListenableFuture executeDropColumn(QualifiedName table, QualifiedName column, boolean tableExists, boolean columnExists) + { + return new DropColumnTask(plannerContext.getMetadata(), new AllowAllAccessControl()) + .execute(new DropColumn(table, column, tableExists, columnExists), queryStateMachine, ImmutableList.of(), WarningCollector.NOOP); + } + + private static ConnectorTableMetadata simpleTable(QualifiedObjectName tableName) + { + return new ConnectorTableMetadata(tableName.asSchemaTableName(), ImmutableList.of(new ColumnMetadata("a", BIGINT), new ColumnMetadata("b", BIGINT))); + } + + private static ConnectorTableMetadata rowTable(QualifiedObjectName tableName, Field... fields) + { + return new ConnectorTableMetadata(tableName.asSchemaTableName(), ImmutableList.of( + new ColumnMetadata("col", RowType.rowType(fields)))); + } +}