blob: ebbb80243e52098b93287fd2077050ba95d98e80 [file]
/*
* 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