diff --git a/plugin/trino-delta-lake/src/test/java/io/trino/plugin/deltalake/TestDeltaLakeConnectorTest.java b/plugin/trino-delta-lake/src/test/java/io/trino/plugin/deltalake/TestDeltaLakeConnectorTest.java index 7e5b37c9e1bc..2d45b07600c5 100644 --- a/plugin/trino-delta-lake/src/test/java/io/trino/plugin/deltalake/TestDeltaLakeConnectorTest.java +++ b/plugin/trino-delta-lake/src/test/java/io/trino/plugin/deltalake/TestDeltaLakeConnectorTest.java @@ -152,6 +152,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) case SUPPORTS_ADD_FIELD: case SUPPORTS_DROP_FIELD: + case SUPPORTS_RENAME_FIELD: case SUPPORTS_SET_COLUMN_TYPE: return false; diff --git a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/BaseHiveConnectorTest.java b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/BaseHiveConnectorTest.java index a0c3ec8e0ec9..bcc092a7d8a5 100644 --- a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/BaseHiveConnectorTest.java +++ b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/BaseHiveConnectorTest.java @@ -261,6 +261,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) case SUPPORTS_ADD_FIELD: case SUPPORTS_DROP_FIELD: + case SUPPORTS_RENAME_FIELD: case SUPPORTS_SET_COLUMN_TYPE: return false; diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMetadata.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMetadata.java index 3bc3cb118cdf..f2e167b4d059 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMetadata.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMetadata.java @@ -181,6 +181,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Maps.transformValues; import static com.google.common.collect.Sets.difference; import static io.trino.plugin.base.projection.ApplyProjectionUtil.extractSupportedProjectedColumns; @@ -1742,6 +1743,27 @@ public void renameColumn(ConnectorSession session, ConnectorTableHandle tableHan } } + @Override + public void renameField(ConnectorSession session, ConnectorTableHandle tableHandle, List fieldPath, String target) + { + Table icebergTable = catalog.loadTable(session, ((IcebergTableHandle) tableHandle).getSchemaTableName()); + String parentPath = String.join(".", fieldPath.subList(0, fieldPath.size() - 1)); + NestedField parent = icebergTable.schema().caseInsensitiveFindField(parentPath); + + String caseSensitiveParentName = icebergTable.schema().findColumnName(parent.fieldId()); + NestedField source = parent.type().asStructType().caseInsensitiveField(getLast(fieldPath)); + + String sourcePath = caseSensitiveParentName + "." + source.name(); + try { + icebergTable.updateSchema() + .renameColumn(sourcePath, target) + .commit(); + } + catch (RuntimeException e) { + throw new TrinoException(ICEBERG_COMMIT_ERROR, "Failed to rename field: " + firstNonNull(e.getMessage(), e), e); + } + } + @Override public void setColumnType(ConnectorSession session, ConnectorTableHandle tableHandle, ColumnHandle columnHandle, io.trino.spi.type.Type type) { diff --git a/plugin/trino-mongodb/src/test/java/io/trino/plugin/mongodb/TestMongoConnectorTest.java b/plugin/trino-mongodb/src/test/java/io/trino/plugin/mongodb/TestMongoConnectorTest.java index 83116449685a..533cde454bdc 100644 --- a/plugin/trino-mongodb/src/test/java/io/trino/plugin/mongodb/TestMongoConnectorTest.java +++ b/plugin/trino-mongodb/src/test/java/io/trino/plugin/mongodb/TestMongoConnectorTest.java @@ -111,6 +111,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) return false; case SUPPORTS_ADD_FIELD: + case SUPPORTS_RENAME_FIELD: case SUPPORTS_DROP_FIELD: return false; diff --git a/testing/trino-product-tests/src/main/java/io/trino/tests/product/iceberg/TestIcebergSparkCompatibility.java b/testing/trino-product-tests/src/main/java/io/trino/tests/product/iceberg/TestIcebergSparkCompatibility.java index 5dac320f2802..7e442175b91c 100644 --- a/testing/trino-product-tests/src/main/java/io/trino/tests/product/iceberg/TestIcebergSparkCompatibility.java +++ b/testing/trino-product-tests/src/main/java/io/trino/tests/product/iceberg/TestIcebergSparkCompatibility.java @@ -2269,6 +2269,26 @@ public void testAddNestedField() onTrino().executeQuery("DROP TABLE " + trinoTableName); } + @Test(groups = {ICEBERG, PROFILE_SPECIFIC_TESTS, ICEBERG_REST, ICEBERG_JDBC}) + public void testRenameNestedField() + { + String baseTableName = "test_rename_nested_field_" + randomNameSuffix(); + String trinoTableName = trinoTableName(baseTableName); + String sparkTableName = sparkTableName(baseTableName); + + onTrino().executeQuery("CREATE TABLE " + trinoTableName + " AS SELECT CAST(row(1, row(10)) AS row(a integer, b row(x integer))) AS col"); + + onTrino().executeQuery("ALTER TABLE " + trinoTableName + " ADD COLUMN col.c integer"); + assertThat(onTrino().executeQuery("SELECT col.a, col.b.x, col.c FROM " + trinoTableName)).containsOnly(row(1, 10, null)); + assertThat(onSpark().executeQuery("SELECT col.a, col.b.x, col.c FROM " + sparkTableName)).containsOnly(row(1, 10, null)); + + onTrino().executeQuery("ALTER TABLE " + trinoTableName + " ADD COLUMN col.b.y integer"); + assertThat(onTrino().executeQuery("SELECT col.a, col.b.x, col.b.y, col.c FROM " + trinoTableName)).containsOnly(row(1, 10, null, null)); + assertThat(onSpark().executeQuery("SELECT col.a, col.b.x, col.b.y, col.c FROM " + sparkTableName)).containsOnly(row(1, 10, null, null)); + + onTrino().executeQuery("DROP TABLE " + trinoTableName); + } + @Test(groups = {ICEBERG, PROFILE_SPECIFIC_TESTS, ICEBERG_REST, ICEBERG_JDBC}) public void testDropNestedField() { diff --git a/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java b/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java index 9a2df1600aa4..a99389e84943 100644 --- a/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java +++ b/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java @@ -138,6 +138,7 @@ import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_NEGATIVE_DATE; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_NOT_NULL_CONSTRAINT; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_RENAME_COLUMN; +import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_RENAME_FIELD; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_RENAME_MATERIALIZED_VIEW; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_RENAME_MATERIALIZED_VIEW_ACROSS_SCHEMAS; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_RENAME_SCHEMA; @@ -2779,6 +2780,68 @@ public void testRenameColumn() assertFalse(getQueryRunner().tableExists(getSession(), tableName)); } + @Test + public void testRenameRowField() + { + skipTestUnless(hasBehavior(SUPPORTS_CREATE_TABLE_WITH_DATA) && hasBehavior(SUPPORTS_ROW_TYPE)); + + if (!hasBehavior(SUPPORTS_RENAME_FIELD)) { + try (TestTable table = new TestTable(getQueryRunner()::execute, "test_rename_field_", "AS SELECT CAST(row(1) AS row(x integer)) AS col")) { + assertQueryFails( + "ALTER TABLE " + table.getName() + " RENAME COLUMN col.x TO x_renamed", + "This connector does not support renaming fields"); + } + return; + } + + try (TestTable table = new TestTable(getQueryRunner()::execute, + "test_add_field_", + "AS SELECT CAST(row(1, row(10)) AS row(a integer, b row(x integer))) AS col")) { + assertEquals(getColumnType(table.getName(), "col"), "row(a integer, b row(x integer))"); + + assertUpdate("ALTER TABLE " + table.getName() + " RENAME COLUMN col.a TO a_renamed"); + assertEquals(getColumnType(table.getName(), "col"), "row(a_renamed integer, b row(x integer))"); + assertThat(query("SELECT * FROM " + table.getName())).matches("SELECT CAST(row(1, row(10)) AS row(a_renamed integer, b row(x integer)))"); + + // Rename a nested field + assertUpdate("ALTER TABLE " + table.getName() + " RENAME COLUMN col.b.x TO x_renamed"); + assertEquals(getColumnType(table.getName(), "col"), "row(a_renamed integer, b row(x_renamed integer))"); + assertThat(query("SELECT * FROM " + table.getName())).matches("SELECT CAST(row(1, row(10)) AS row(a_renamed integer, b row(x_renamed integer)))"); + + // Specify not existing fields with IF EXISTS option + assertUpdate("ALTER TABLE " + table.getName() + " RENAME COLUMN IF EXISTS col.a_missing TO a_missing_renamed"); + assertUpdate("ALTER TABLE " + table.getName() + " RENAME COLUMN IF EXISTS col.b.x_missing TO x_missing_renamed"); + assertEquals(getColumnType(table.getName(), "col"), "row(a_renamed integer, b row(x_renamed integer))"); + + // Specify existing fields without IF EXISTS option + assertQueryFails("ALTER TABLE " + table.getName() + " RENAME COLUMN col.a_renamed TO a_renamed", ".* Field 'a_renamed' already exists"); + } + } + + @Test + public void testRenameRowFieldCaseSensitivity() + { + skipTestUnless(hasBehavior(SUPPORTS_RENAME_FIELD)); + + try (TestTable table = new TestTable(getQueryRunner()::execute, + "test_add_row_field_case_sensitivity_", + "AS SELECT CAST(row(1, 2) AS row(lower integer, \"UPPER\" integer)) AS col")) { + assertEquals(getColumnType(table.getName(), "col"), "row(lower integer, UPPER integer)"); + + assertQueryFails("ALTER TABLE " + table.getName() + " RENAME COLUMN col.lower TO UPPER", ".* Field 'upper' already exists"); + assertQueryFails("ALTER TABLE " + table.getName() + " RENAME COLUMN col.lower TO upper", ".* Field 'upper' already exists"); + + assertUpdate("ALTER TABLE " + table.getName() + " RENAME COLUMN col.lower TO LOWER_RENAMED"); + assertEquals(getColumnType(table.getName(), "col"), "row(lower_renamed integer, UPPER integer)"); + + assertUpdate("ALTER TABLE " + table.getName() + " RENAME COLUMN col.\"UPPER\" TO upper_renamed"); + assertEquals(getColumnType(table.getName(), "col"), "row(lower_renamed integer, upper_renamed integer)"); + + assertThat(query("SELECT * FROM " + table.getName())) + .matches("SELECT CAST(row(1, 2) AS row(lower_renamed integer, upper_renamed integer))"); + } + } + @Test public void testSetColumnType() { diff --git a/testing/trino-testing/src/main/java/io/trino/testing/TestingConnectorBehavior.java b/testing/trino-testing/src/main/java/io/trino/testing/TestingConnectorBehavior.java index 99245134c322..13e61516672a 100644 --- a/testing/trino-testing/src/main/java/io/trino/testing/TestingConnectorBehavior.java +++ b/testing/trino-testing/src/main/java/io/trino/testing/TestingConnectorBehavior.java @@ -86,6 +86,7 @@ public enum TestingConnectorBehavior SUPPORTS_DROP_COLUMN(SUPPORTS_ADD_COLUMN), SUPPORTS_DROP_FIELD(and(SUPPORTS_DROP_COLUMN, SUPPORTS_ROW_TYPE)), SUPPORTS_RENAME_COLUMN, + SUPPORTS_RENAME_FIELD(fallback -> fallback.test(SUPPORTS_RENAME_COLUMN) && fallback.test(SUPPORTS_ROW_TYPE)), SUPPORTS_SET_COLUMN_TYPE, SUPPORTS_COMMENT_ON_TABLE,