| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you 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. | |
| */ | |
| #include "iceberg/update/update_partition_spec.h" | |
| #include <format> | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include <arrow/filesystem/mockfs.h> | |
| #include <gmock/gmock.h> | |
| #include <gtest/gtest.h> | |
| #include "iceberg/arrow/arrow_io_internal.h" | |
| #include "iceberg/catalog/memory/in_memory_catalog.h" | |
| #include "iceberg/expression/expressions.h" | |
| #include "iceberg/partition_spec.h" | |
| #include "iceberg/result.h" | |
| #include "iceberg/schema.h" | |
| #include "iceberg/snapshot.h" | |
| #include "iceberg/sort_order.h" | |
| #include "iceberg/table.h" | |
| #include "iceberg/table_identifier.h" | |
| #include "iceberg/table_metadata.h" | |
| #include "iceberg/test/matchers.h" | |
| #include "iceberg/transaction.h" | |
| #include "iceberg/transform.h" | |
| #include "iceberg/type.h" | |
| #include "iceberg/util/uuid.h" | |
| namespace iceberg { | |
| class UpdatePartitionSpecTest : public ::testing::TestWithParam<int8_t> { | |
| protected: | |
| void SetUp() override { | |
| file_io_ = arrow::ArrowFileSystemFileIO::MakeMockFileIO(); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| catalog_, InMemoryCatalog::Make("test_catalog", file_io_, "/warehouse/", {})); | |
| format_version_ = GetParam(); | |
| test_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "id", int64()), | |
| SchemaField::MakeRequired(2, "ts", timestamp_tz()), | |
| SchemaField::MakeRequired(3, "category", string()), | |
| SchemaField::MakeOptional(4, "data", string())}, | |
| 0); | |
| // Create unpartitioned and partitioned specs matching Java test | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto unpartitioned_spec, | |
| PartitionSpec::Make(PartitionSpec::kInitialSpecId, std::vector<PartitionField>{}, | |
| PartitionSpec::kLegacyPartitionDataIdStart - 1)); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| partitioned_spec_, | |
| PartitionSpec::Make( | |
| PartitionSpec::kInitialSpecId, | |
| std::vector<PartitionField>{ | |
| PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16))}, | |
| 1002)); | |
| auto partitioned_metadata = | |
| CreateBaseMetadata(format_version_, test_schema_, partitioned_spec_); | |
| auto unpartitioned_metadata = | |
| CreateBaseMetadata(format_version_, test_schema_, std::move(unpartitioned_spec)); | |
| // Write metadata files | |
| partitioned_metadata->location = partitioned_table_location_; | |
| unpartitioned_metadata->location = unpartitioned_table_location_; | |
| // Arrow MockFS cannot automatically create directories. | |
| auto arrow_fs = std::dynamic_pointer_cast<::arrow::fs::internal::MockFileSystem>( | |
| static_cast<arrow::ArrowFileSystemFileIO&>(*file_io_).fs()); | |
| ASSERT_TRUE(arrow_fs != nullptr); | |
| ASSERT_TRUE(arrow_fs->CreateDir(partitioned_table_location_ + "/metadata").ok()); | |
| ASSERT_TRUE(arrow_fs->CreateDir(unpartitioned_table_location_ + "/metadata").ok()); | |
| // Write table metadata to the table location. | |
| std::string partitioned_metadata_location = | |
| std::format("{}/metadata/00001-{}.metadata.json", partitioned_table_location_, | |
| Uuid::GenerateV7().ToString()); | |
| std::string unpartitioned_metadata_location = | |
| std::format("{}/metadata/00001-{}.metadata.json", unpartitioned_table_location_, | |
| Uuid::GenerateV7().ToString()); | |
| ASSERT_THAT(TableMetadataUtil::Write(*file_io_, partitioned_metadata_location, | |
| *partitioned_metadata), | |
| IsOk()); | |
| ASSERT_THAT(TableMetadataUtil::Write(*file_io_, unpartitioned_metadata_location, | |
| *unpartitioned_metadata), | |
| IsOk()); | |
| // Register the tables in the catalog. | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| partitioned_table_, | |
| catalog_->RegisterTable(partitioned_table_ident_, partitioned_metadata_location)); | |
| ICEBERG_UNWRAP_OR_FAIL(unpartitioned_table_, | |
| catalog_->RegisterTable(unpartitioned_table_ident_, | |
| unpartitioned_metadata_location)); | |
| } | |
| // Helper to create base metadata with a specific partition spec | |
| std::unique_ptr<TableMetadata> CreateBaseMetadata(int8_t format_version, | |
| std::shared_ptr<Schema> schema, | |
| std::shared_ptr<PartitionSpec> spec) { | |
| auto metadata = std::make_unique<TableMetadata>(); | |
| metadata->format_version = format_version; | |
| metadata->table_uuid = "test-uuid-1234"; | |
| metadata->location = "/warehouse/test_table"; | |
| metadata->last_sequence_number = 0; | |
| metadata->last_updated_ms = TimePointMs{std::chrono::milliseconds(1000)}; | |
| metadata->last_column_id = 4; | |
| metadata->current_schema_id = 0; | |
| metadata->schemas.push_back(std::move(schema)); | |
| metadata->default_spec_id = spec->spec_id(); | |
| metadata->last_partition_id = spec->last_assigned_field_id(); | |
| metadata->current_snapshot_id = kInvalidSnapshotId; | |
| metadata->default_sort_order_id = SortOrder::kUnsortedOrderId; | |
| metadata->sort_orders.push_back(SortOrder::Unsorted()); | |
| metadata->next_row_id = TableMetadata::kInitialRowId; | |
| metadata->partition_specs.push_back(std::move(spec)); | |
| return metadata; | |
| } | |
| // Helper to create UpdatePartitionSpec with a specific partition spec | |
| std::shared_ptr<UpdatePartitionSpec> CreateUpdatePartitionSpec(bool partitioned) { | |
| if (partitioned) { | |
| auto update_result = partitioned_table_->NewUpdatePartitionSpec(); | |
| if (!update_result.has_value()) { | |
| ADD_FAILURE() << "Failed to create update: " << update_result.error().message; | |
| return nullptr; | |
| } | |
| return update_result.value(); | |
| } else { | |
| auto update_result = unpartitioned_table_->NewUpdatePartitionSpec(); | |
| if (!update_result.has_value()) { | |
| ADD_FAILURE() << "Failed to create update: " << update_result.error().message; | |
| return nullptr; | |
| } | |
| return update_result.value(); | |
| } | |
| } | |
| // Helper to create an expected partition spec | |
| std::shared_ptr<PartitionSpec> MakeExpectedSpec( | |
| const std::vector<PartitionField>& fields, int32_t last_assigned_field_id) { | |
| auto spec_result = PartitionSpec::Make(PartitionSpec::kInitialSpecId, fields, | |
| last_assigned_field_id); | |
| if (!spec_result.has_value()) { | |
| ADD_FAILURE() << "Failed to create expected spec: " << spec_result.error().message; | |
| return nullptr; | |
| } | |
| return std::shared_ptr<PartitionSpec>(spec_result.value().release()); | |
| } | |
| // Helper to apply update and get the resulting spec | |
| std::shared_ptr<PartitionSpec> ApplyUpdateAndGetSpec( | |
| std::shared_ptr<UpdatePartitionSpec> update) { | |
| auto result = update->Apply(); | |
| if (!result.has_value()) { | |
| ADD_FAILURE() << "Failed to apply update: " << result.error().message; | |
| return nullptr; | |
| } | |
| return result.value().spec; | |
| } | |
| // Helper to apply update and assert spec equality | |
| void ApplyUpdateAndAssertSpec(std::shared_ptr<UpdatePartitionSpec> update, | |
| const std::vector<PartitionField>& expected_fields, | |
| int32_t last_assigned_field_id) { | |
| auto updated_spec = ApplyUpdateAndGetSpec(update); | |
| auto expected_spec = MakeExpectedSpec(expected_fields, last_assigned_field_id); | |
| AssertPartitionSpecEquals(*expected_spec, *updated_spec); | |
| } | |
| // Helper to assert partition spec equality | |
| void AssertPartitionSpecEquals(const PartitionSpec& expected, | |
| const PartitionSpec& actual) { | |
| ASSERT_EQ(expected.fields().size(), actual.fields().size()); | |
| for (size_t i = 0; i < expected.fields().size(); ++i) { | |
| const auto& expected_field = expected.fields()[i]; | |
| const auto& actual_field = actual.fields()[i]; | |
| EXPECT_EQ(expected_field.source_id(), actual_field.source_id()); | |
| EXPECT_EQ(expected_field.field_id(), actual_field.field_id()); | |
| EXPECT_EQ(expected_field.name(), actual_field.name()); | |
| EXPECT_EQ(*expected_field.transform(), *actual_field.transform()); | |
| } | |
| } | |
| // Helper to expect an error with a specific message | |
| void ExpectError(std::shared_ptr<UpdatePartitionSpec> update, ErrorKind expected_kind, | |
| const std::string& expected_message) { | |
| auto result = update->Apply(); | |
| ASSERT_THAT(result, IsError(expected_kind)); | |
| ASSERT_THAT(result, HasErrorMessage(expected_message)); | |
| } | |
| // Helper to create a table with a custom partition spec | |
| std::shared_ptr<Table> CreateTableWithSpec(std::shared_ptr<PartitionSpec> spec, | |
| const std::string& table_name) { | |
| auto metadata = CreateBaseMetadata(format_version_, test_schema_, spec); | |
| TableIdentifier identifier{.ns = Namespace{.levels = {"test"}}, .name = table_name}; | |
| std::string metadata_location = | |
| std::format("/warehouse/{}/metadata/00000-{}.metadata.json", table_name, | |
| Uuid::GenerateV7().ToString()); | |
| auto table_result = | |
| Table::Make(identifier, std::shared_ptr<TableMetadata>(metadata.release()), | |
| metadata_location, file_io_, catalog_); | |
| if (!table_result.has_value()) { | |
| ADD_FAILURE() << "Failed to create table: " << table_result.error().message; | |
| return nullptr; | |
| } | |
| return table_result.value(); | |
| } | |
| // Helper to create UpdatePartitionSpec from a table | |
| std::shared_ptr<UpdatePartitionSpec> CreateUpdateFromTable( | |
| std::shared_ptr<Table> table) { | |
| auto ctx_result = TransactionContext::Make(table, TransactionKind::kUpdate); | |
| if (!ctx_result.has_value()) { | |
| ADD_FAILURE() << "Failed to create context: " << ctx_result.error().message; | |
| return nullptr; | |
| } | |
| auto update_result = UpdatePartitionSpec::Make(std::move(ctx_result.value())); | |
| if (!update_result.has_value()) { | |
| ADD_FAILURE() << "Failed to create UpdatePartitionSpec: " | |
| << update_result.error().message; | |
| return nullptr; | |
| } | |
| return update_result.value(); | |
| } | |
| const TableIdentifier partitioned_table_ident_{.name = "partitioned_table"}; | |
| const TableIdentifier unpartitioned_table_ident_{.name = "unpartitioned_table"}; | |
| const std::string partitioned_table_location_{"/warehouse/partitioned_table"}; | |
| const std::string unpartitioned_table_location_{"/warehouse/unpartitioned_table"}; | |
| std::shared_ptr<FileIO> file_io_; | |
| std::shared_ptr<InMemoryCatalog> catalog_; | |
| std::shared_ptr<Schema> test_schema_; | |
| std::shared_ptr<PartitionSpec> partitioned_spec_; | |
| std::shared_ptr<Table> partitioned_table_; | |
| std::shared_ptr<Table> unpartitioned_table_; | |
| int8_t format_version_; | |
| }; | |
| INSTANTIATE_TEST_SUITE_P(FormatVersions, UpdatePartitionSpecTest, ::testing::Values(1, 2), | |
| [](const ::testing::TestParamInfo<int8_t>& info) { | |
| return std::format("V{}", info.param); | |
| }); | |
| TEST_P(UpdatePartitionSpecTest, TestAddIdentityByName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField("category"); | |
| ApplyUpdateAndAssertSpec( | |
| update, {PartitionField(3, 1000, "category", Transform::Identity())}, 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddIdentityByTerm) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Ref("category")); | |
| ApplyUpdateAndAssertSpec( | |
| update, {PartitionField(3, 1000, "category", Transform::Identity())}, 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddYear) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Year("ts")); | |
| ApplyUpdateAndAssertSpec(update, | |
| {PartitionField(2, 1000, "ts_year", Transform::Year())}, 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddMonth) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Month("ts")); | |
| ApplyUpdateAndAssertSpec( | |
| update, {PartitionField(2, 1000, "ts_month", Transform::Month())}, 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddDay) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Day("ts")); | |
| ApplyUpdateAndAssertSpec(update, {PartitionField(2, 1000, "ts_day", Transform::Day())}, | |
| 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddHour) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Hour("ts")); | |
| ApplyUpdateAndAssertSpec(update, | |
| {PartitionField(2, 1000, "ts_hour", Transform::Hour())}, 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddBucket) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Bucket("id", 16)); | |
| ApplyUpdateAndAssertSpec( | |
| update, {PartitionField(1, 1000, "id_bucket_16", Transform::Bucket(16))}, 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddTruncate) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Truncate("data", 4)); | |
| ApplyUpdateAndAssertSpec( | |
| update, {PartitionField(4, 1000, "data_trunc_4", Transform::Truncate(4))}, 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddNamedPartition) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Bucket("id", 16), "shard"); | |
| ApplyUpdateAndAssertSpec( | |
| update, {PartitionField(1, 1000, "shard", Transform::Bucket(16))}, 1000); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddToExisting) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Truncate("data", 4)); | |
| ApplyUpdateAndAssertSpec( | |
| update, | |
| {PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16)), | |
| PartitionField(4, 1003, "data_trunc_4", Transform::Truncate(4))}, | |
| 1003); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestMultipleAdds) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField("category") | |
| .AddField(Expressions::Day("ts")) | |
| .AddField(Expressions::Bucket("id", 16), "shard") | |
| .AddField(Expressions::Truncate("data", 4), "prefix"); | |
| ApplyUpdateAndAssertSpec(update, | |
| {PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16)), | |
| PartitionField(4, 1003, "prefix", Transform::Truncate(4))}, | |
| 1003); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddHourToDay) { | |
| // First add day partition | |
| ICEBERG_UNWRAP_OR_FAIL(auto update1, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update1->AddField(Expressions::Day("ts")); | |
| auto by_day_spec = ApplyUpdateAndGetSpec(update1); | |
| // Then add hour partition | |
| auto table = CreateTableWithSpec(by_day_spec, "test_table"); | |
| auto update2 = CreateUpdateFromTable(table); | |
| update2->AddField(Expressions::Hour("ts")); | |
| auto by_hour_spec = ApplyUpdateAndGetSpec(update2); | |
| ASSERT_EQ(by_hour_spec->fields().size(), 2); | |
| EXPECT_EQ(by_hour_spec->fields()[0].source_id(), 2); | |
| EXPECT_EQ(by_hour_spec->fields()[0].name(), "ts_day"); | |
| EXPECT_EQ(*by_hour_spec->fields()[0].transform(), *Transform::Day()); | |
| EXPECT_EQ(by_hour_spec->fields()[1].source_id(), 2); | |
| EXPECT_EQ(by_hour_spec->fields()[1].name(), "ts_hour"); | |
| EXPECT_EQ(*by_hour_spec->fields()[1].transform(), *Transform::Hour()); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddMultipleBuckets) { | |
| // First add bucket 16 | |
| ICEBERG_UNWRAP_OR_FAIL(auto update1, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update1->AddField(Expressions::Bucket("id", 16)); | |
| auto bucket16_spec = ApplyUpdateAndGetSpec(update1); | |
| // Then add bucket 8 | |
| auto table = CreateTableWithSpec(bucket16_spec, "test_table"); | |
| auto update2 = CreateUpdateFromTable(table); | |
| update2->AddField(Expressions::Bucket("id", 8)); | |
| ApplyUpdateAndAssertSpec( | |
| update2, | |
| {PartitionField(1, 1000, "id_bucket_16", Transform::Bucket(16)), | |
| PartitionField(1, 1001, "id_bucket_8", Transform::Bucket(8))}, | |
| 1001); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveIdentityByName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField("category"); | |
| auto updated_spec = ApplyUpdateAndGetSpec(update); | |
| if (format_version_ == 1) { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Void()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16))}, | |
| 1002); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } else { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16))}, | |
| 1002); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveBucketByName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField("shard"); | |
| auto updated_spec = ApplyUpdateAndGetSpec(update); | |
| if (format_version_ == 1) { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Void())}, | |
| 1002); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } else { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day())}, | |
| 1001); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveIdentityByEquivalent) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField(Expressions::Ref("category")); | |
| auto updated_spec = ApplyUpdateAndGetSpec(update); | |
| if (format_version_ == 1) { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Void()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16))}, | |
| 1002); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } else { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16))}, | |
| 1002); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveDayByEquivalent) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField(Expressions::Day("ts")); | |
| auto updated_spec = ApplyUpdateAndGetSpec(update); | |
| if (format_version_ == 1) { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Void()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16))}, | |
| 1002); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } else { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(1, 1002, "shard", Transform::Bucket(16))}, | |
| 1002); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveBucketByEquivalent) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField(Expressions::Bucket("id", 16)); | |
| auto updated_spec = ApplyUpdateAndGetSpec(update); | |
| if (format_version_ == 1) { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Void())}, | |
| 1002); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } else { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day())}, | |
| 1001); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRename) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RenameField("shard", "id_bucket"); | |
| ApplyUpdateAndAssertSpec(update, | |
| {PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "id_bucket", Transform::Bucket(16))}, | |
| 1002); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestMultipleChanges) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RenameField("shard", "id_bucket") | |
| .RemoveField(Expressions::Day("ts")) | |
| .AddField(Expressions::Truncate("data", 4), "prefix"); | |
| auto updated_spec = ApplyUpdateAndGetSpec(update); | |
| if (format_version_ == 1) { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Void()), | |
| PartitionField(1, 1002, "id_bucket", Transform::Bucket(16)), | |
| PartitionField(4, 1003, "prefix", Transform::Truncate(4))}, | |
| 1003); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } else { | |
| auto expected = | |
| MakeExpectedSpec({PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(1, 1002, "id_bucket", Transform::Bucket(16)), | |
| PartitionField(4, 1003, "prefix", Transform::Truncate(4))}, | |
| 1003); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddDeletedName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField(Expressions::Bucket("id", 16)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto result, update->Apply()); | |
| auto updated_spec = result.spec; | |
| if (format_version_ == 1) { | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto expected_spec, | |
| PartitionSpec::Make( | |
| PartitionSpec::kInitialSpecId, | |
| std::vector<PartitionField>{ | |
| PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day()), | |
| PartitionField(1, 1002, "shard", Transform::Void())}, | |
| 1002)); | |
| auto expected = std::shared_ptr<PartitionSpec>(expected_spec.release()); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } else { | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto expected_spec, | |
| PartitionSpec::Make( | |
| PartitionSpec::kInitialSpecId, | |
| std::vector<PartitionField>{ | |
| PartitionField(3, 1000, "category", Transform::Identity()), | |
| PartitionField(2, 1001, "ts_day", Transform::Day())}, | |
| 1001)); | |
| auto expected = std::shared_ptr<PartitionSpec>(expected_spec.release()); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveNewlyAddedFieldByName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Truncate("data", 4), "prefix"); | |
| update->RemoveField("prefix"); | |
| ExpectError(update, ErrorKind::kValidationFailed, "Cannot delete newly added field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveNewlyAddedFieldByTransform) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Truncate("data", 4), "prefix"); | |
| update->RemoveField(Expressions::Truncate("data", 4)); | |
| ExpectError(update, ErrorKind::kValidationFailed, "Cannot delete newly added field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddAlreadyAddedFieldByTransform) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Truncate("data", 4), "prefix"); | |
| update->AddField(Expressions::Truncate("data", 4)); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot add duplicate partition field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddAlreadyAddedFieldByName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Truncate("data", 4), "prefix"); | |
| update->AddField(Expressions::Truncate("data", 6), "prefix"); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot add duplicate partition field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddRedundantTimePartition) { | |
| // Test day + hour conflict | |
| ICEBERG_UNWRAP_OR_FAIL(auto update1, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update1->AddField(Expressions::Day("ts")); | |
| update1->AddField(Expressions::Hour("ts")); | |
| ExpectError(update1, ErrorKind::kValidationFailed, | |
| "Cannot add redundant partition field"); | |
| // Test hour + month conflict after adding hour to existing day | |
| ICEBERG_UNWRAP_OR_FAIL(auto update2, partitioned_table_->NewUpdatePartitionSpec()); | |
| update2->AddField(Expressions::Hour("ts")); // day already exists, so hour is OK | |
| update2->AddField(Expressions::Month("ts")); // conflicts with hour | |
| ExpectError(update2, ErrorKind::kValidationFailed, | |
| "Cannot add redundant partition field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestNoEffectAddDeletedSameFieldWithSameName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update1, partitioned_table_->NewUpdatePartitionSpec()); | |
| update1->RemoveField("shard"); | |
| update1->AddField(Expressions::Bucket("id", 16), "shard"); | |
| ICEBERG_UNWRAP_OR_FAIL(auto result1, update1->Apply()); | |
| auto spec1 = result1.spec; | |
| AssertPartitionSpecEquals(*partitioned_spec_, *spec1); | |
| ICEBERG_UNWRAP_OR_FAIL(auto update2, partitioned_table_->NewUpdatePartitionSpec()); | |
| update2->RemoveField("shard"); | |
| update2->AddField(Expressions::Bucket("id", 16)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto result2, update2->Apply()); | |
| auto spec2 = result2.spec; | |
| AssertPartitionSpecEquals(*partitioned_spec_, *spec2); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestGenerateNewSpecAddDeletedSameFieldWithDifferentName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField("shard"); | |
| update->AddField(Expressions::Bucket("id", 16), "new_shard"); | |
| ICEBERG_UNWRAP_OR_FAIL(auto result, update->Apply()); | |
| auto updated_spec = result.spec; | |
| ASSERT_EQ(updated_spec->fields().size(), 3); | |
| EXPECT_EQ(updated_spec->fields()[0].name(), "category"); | |
| EXPECT_EQ(updated_spec->fields()[1].name(), "ts_day"); | |
| EXPECT_EQ(updated_spec->fields()[2].name(), "new_shard"); | |
| EXPECT_EQ(*updated_spec->fields()[0].transform(), *Transform::Identity()); | |
| EXPECT_EQ(*updated_spec->fields()[1].transform(), *Transform::Day()); | |
| EXPECT_EQ(*updated_spec->fields()[2].transform(), *Transform::Bucket(16)); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddDuplicateByName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField("category"); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot add duplicate partition field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddDuplicateByRef) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Ref("category")); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot add duplicate partition field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddDuplicateTransform) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Bucket("id", 16)); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot add duplicate partition field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestAddNamedDuplicate) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Bucket("id", 16), "b16"); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot add duplicate partition field"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveUnknownFieldByName) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField("moon"); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot find partition field to remove"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveUnknownFieldByEquivalent) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField(Expressions::Hour("ts")); // day(ts) exists, not hour | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot find partition field to remove"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRenameUnknownField) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RenameField("shake", "seal"); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot find partition field to rename: shake"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRenameAfterAdd) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Truncate("data", 4), "data_trunc"); | |
| update->RenameField("data_trunc", "prefix"); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot rename newly added partition field: data_trunc"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRenameAndDelete) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RenameField("shard", "id_bucket"); | |
| update->RemoveField(Expressions::Bucket("id", 16)); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot rename and delete partition field: shard"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestDeleteAndRename) { | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, partitioned_table_->NewUpdatePartitionSpec()); | |
| update->RemoveField(Expressions::Bucket("id", 16)); | |
| update->RenameField("shard", "id_bucket"); | |
| ExpectError(update, ErrorKind::kValidationFailed, | |
| "Cannot delete and rename partition field: shard"); | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveAndAddMultiTimes) { | |
| // Add first time | |
| ICEBERG_UNWRAP_OR_FAIL(auto update1, unpartitioned_table_->NewUpdatePartitionSpec()); | |
| update1->AddField(Expressions::Day("ts"), "ts_date"); | |
| auto add_first_time_spec = ApplyUpdateAndGetSpec(update1); | |
| // Remove first time | |
| auto table1 = CreateTableWithSpec(add_first_time_spec, "test_table"); | |
| auto update2 = CreateUpdateFromTable(table1); | |
| update2->RemoveField(Expressions::Day("ts")); | |
| auto remove_first_time_spec = ApplyUpdateAndGetSpec(update2); | |
| // Add second time | |
| auto table2 = CreateTableWithSpec(remove_first_time_spec, "test_table2"); | |
| auto update3 = CreateUpdateFromTable(table2); | |
| update3->AddField(Expressions::Day("ts"), "ts_date"); | |
| auto add_second_time_spec = ApplyUpdateAndGetSpec(update3); | |
| // Add third time with month | |
| auto table4 = CreateTableWithSpec(add_second_time_spec, "test_table4"); | |
| auto update5 = CreateUpdateFromTable(table4); | |
| update5->AddField(Expressions::Month("ts"), "ts_month"); | |
| auto add_third_time_spec = ApplyUpdateAndGetSpec(update5); | |
| // Rename ts_month to ts_date | |
| auto table5 = CreateTableWithSpec(add_third_time_spec, "test_table5"); | |
| auto update6 = CreateUpdateFromTable(table5); | |
| update6->RenameField("ts_month", "__ts_date"); | |
| auto updated_spec = ApplyUpdateAndGetSpec(update6); | |
| if (format_version_ == 1) { | |
| // Remove second time | |
| auto table3 = CreateTableWithSpec(add_second_time_spec, "test_table3"); | |
| auto update4 = CreateUpdateFromTable(table3); | |
| update4->RemoveField(Expressions::Day("ts")); | |
| ExpectError(update4, ErrorKind::kValidationFailed, | |
| "Cannot add redundant partition: ts_date"); | |
| ASSERT_EQ(updated_spec->fields().size(), 3); | |
| // In V1, we expect void transforms for deleted fields | |
| EXPECT_TRUE(updated_spec->fields()[0].name().find("ts_date") == 0); | |
| EXPECT_TRUE(updated_spec->fields()[1].name().find("ts_date") == 0); | |
| EXPECT_EQ(updated_spec->fields()[2].name(), "__ts_date"); | |
| EXPECT_EQ(*updated_spec->fields()[0].transform(), *Transform::Void()); | |
| EXPECT_EQ(*updated_spec->fields()[1].transform(), *Transform::Day()); | |
| EXPECT_EQ(*updated_spec->fields()[2].transform(), *Transform::Month()); | |
| } else { | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto expected_spec, | |
| PartitionSpec::Make(PartitionSpec::kInitialSpecId, | |
| std::vector<PartitionField>{ | |
| PartitionField(2, 1000, "ts_date", Transform::Day()), | |
| PartitionField(2, 1001, "__ts_date", Transform::Month())}, | |
| 1000)); | |
| auto expected = std::shared_ptr<PartitionSpec>(expected_spec.release()); | |
| AssertPartitionSpecEquals(*expected, *updated_spec); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, TestRemoveAndUpdateWithDifferentTransformation) { | |
| auto initial_spec = MakeExpectedSpec( | |
| {PartitionField(2, 1000, "ts_transformed", Transform::Month())}, 1000); | |
| auto table = CreateTableWithSpec(initial_spec, "test_table"); | |
| auto update = CreateUpdateFromTable(table); | |
| update->RemoveField("ts_transformed"); | |
| update->AddField(Expressions::Day("ts"), "ts_transformed"); | |
| auto updated_spec = ApplyUpdateAndGetSpec(update); | |
| if (format_version_ == 1) { | |
| ASSERT_EQ(updated_spec->fields().size(), 2); | |
| EXPECT_TRUE(updated_spec->fields()[0].name().find("ts_transformed") == 0); | |
| EXPECT_EQ(updated_spec->fields()[1].name(), "ts_transformed"); | |
| EXPECT_EQ(*updated_spec->fields()[0].transform(), *Transform::Void()); | |
| EXPECT_EQ(*updated_spec->fields()[1].transform(), *Transform::Day()); | |
| } else { | |
| ASSERT_EQ(updated_spec->fields().size(), 1); | |
| EXPECT_EQ(updated_spec->fields()[0].name(), "ts_transformed"); | |
| EXPECT_EQ(*updated_spec->fields()[0].transform(), *Transform::Day()); | |
| } | |
| } | |
| TEST_P(UpdatePartitionSpecTest, CommitSuccess) { | |
| // Test empty commit | |
| ICEBERG_UNWRAP_OR_FAIL(auto empty_update, partitioned_table_->NewUpdatePartitionSpec()); | |
| EXPECT_THAT(empty_update->Commit(), IsOk()); | |
| // Reload table after first commit | |
| ICEBERG_UNWRAP_OR_FAIL(auto reloaded, catalog_->LoadTable(partitioned_table_ident_)); | |
| // Test commit with partition spec changes | |
| ICEBERG_UNWRAP_OR_FAIL(auto update, reloaded->NewUpdatePartitionSpec()); | |
| update->AddField(Expressions::Truncate("data", 4), "prefix"); | |
| EXPECT_THAT(update->Commit(), IsOk()); | |
| // Verify the partition spec was committed | |
| ICEBERG_UNWRAP_OR_FAIL(auto final_table, catalog_->LoadTable(partitioned_table_ident_)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto spec, final_table->spec()); | |
| ASSERT_EQ(spec->fields().size(), 4); | |
| EXPECT_EQ(spec->fields()[0].name(), "category"); | |
| EXPECT_EQ(spec->fields()[1].name(), "ts_day"); | |
| EXPECT_EQ(spec->fields()[2].name(), "shard"); | |
| EXPECT_EQ(spec->fields()[3].name(), "prefix"); | |
| EXPECT_EQ(*spec->fields()[0].transform(), *Transform::Identity()); | |
| EXPECT_EQ(*spec->fields()[1].transform(), *Transform::Day()); | |
| EXPECT_EQ(*spec->fields()[2].transform(), *Transform::Bucket(16)); | |
| EXPECT_EQ(*spec->fields()[3].transform(), *Transform::Truncate(4)); | |
| } | |
| } // namespace iceberg |