| /* |
| * 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/table_update.h" |
| |
| #include <functional> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include <gtest/gtest.h> |
| |
| #include "iceberg/partition_spec.h" |
| #include "iceberg/schema.h" |
| #include "iceberg/snapshot.h" |
| #include "iceberg/sort_field.h" |
| #include "iceberg/sort_order.h" |
| #include "iceberg/table_metadata.h" |
| #include "iceberg/table_requirement.h" |
| #include "iceberg/table_requirements.h" |
| #include "iceberg/test/matchers.h" |
| #include "iceberg/transform.h" |
| #include "iceberg/type.h" |
| #include "iceberg/util/timepoint.h" |
| |
| namespace iceberg { |
| |
| namespace { |
| |
| // Helper function to create a simple schema for testing |
| std::shared_ptr<Schema> CreateTestSchema() { |
| auto field1 = SchemaField::MakeRequired(1, "id", int32()); |
| auto field2 = SchemaField::MakeRequired(2, "data", string()); |
| auto field3 = SchemaField::MakeRequired(3, "ts", timestamp()); |
| return std::make_shared<Schema>(std::vector<SchemaField>{field1, field2, field3}, 0); |
| } |
| |
| // Helper function to generate requirements |
| std::vector<std::unique_ptr<TableRequirement>> GenerateRequirements( |
| const TableUpdate& update, const TableMetadata* base) { |
| TableUpdateContext context(base, /*is_replace=*/false); |
| update.GenerateRequirements(context), IsOk(); |
| auto requirements = context.Build(); |
| EXPECT_THAT(requirements, IsOk()); |
| return std::move(requirements.value()); |
| } |
| |
| // Helper function to create base metadata for tests |
| std::unique_ptr<TableMetadata> CreateBaseMetadata() { |
| auto metadata = std::make_unique<TableMetadata>(); |
| metadata->format_version = 2; |
| metadata->table_uuid = "test-uuid-1234"; |
| metadata->location = "s3://bucket/test"; |
| metadata->last_sequence_number = 0; |
| metadata->last_updated_ms = TimePointMs{std::chrono::milliseconds(1000)}; |
| metadata->last_column_id = 3; |
| metadata->current_schema_id = 0; |
| metadata->schemas.push_back(CreateTestSchema()); |
| metadata->partition_specs.push_back(PartitionSpec::Unpartitioned()); |
| metadata->default_spec_id = PartitionSpec::kInitialSpecId; |
| metadata->last_partition_id = 0; |
| metadata->current_snapshot_id = kInvalidSnapshotId; |
| metadata->sort_orders.push_back(SortOrder::Unsorted()); |
| metadata->default_sort_order_id = SortOrder::kUnsortedOrderId; |
| metadata->next_row_id = TableMetadata::kInitialRowId; |
| return metadata; |
| } |
| |
| } // namespace |
| |
| // Parameter struct for testing GenerateRequirements behavior |
| struct GenerateRequirementsTestParam { |
| std::string test_name; |
| std::function<std::unique_ptr<TableUpdate>()> update_factory; |
| // Expected number of requirements for existing table (new table always expects 0) |
| size_t expected_existing_table_count; |
| // Optional validator function to check the generated requirements |
| std::function<void(const std::vector<std::unique_ptr<TableRequirement>>&, |
| const TableMetadata*)> |
| validator; |
| }; |
| |
| class GenerateRequirementsTest |
| : public ::testing::TestWithParam<GenerateRequirementsTestParam> {}; |
| |
| TEST_P(GenerateRequirementsTest, GeneratesExpectedRequirements) { |
| const auto& param = GetParam(); |
| auto update = param.update_factory(); |
| |
| // New table - always no requirements |
| auto new_table_reqs = GenerateRequirements(*update, nullptr); |
| EXPECT_TRUE(new_table_reqs.empty()); |
| |
| // Existing table - check expected count |
| auto base = CreateBaseMetadata(); |
| auto existing_table_reqs = GenerateRequirements(*update, base.get()); |
| ASSERT_EQ(existing_table_reqs.size(), param.expected_existing_table_count); |
| |
| // Validate the requirements if validator is provided |
| if (param.validator) { |
| param.validator(existing_table_reqs, base.get()); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| TableUpdateGenerateRequirements, GenerateRequirementsTest, |
| ::testing::Values( |
| // Updates that generate no requirements |
| GenerateRequirementsTestParam{ |
| .test_name = "AssignUUID", |
| .update_factory = |
| [] { return std::make_unique<table::AssignUUID>("new-uuid"); }, |
| .expected_existing_table_count = 0, |
| .validator = nullptr}, |
| GenerateRequirementsTestParam{ |
| .test_name = "UpgradeFormatVersion", |
| .update_factory = |
| [] { return std::make_unique<table::UpgradeFormatVersion>(3); }, |
| .expected_existing_table_count = 0, |
| .validator = nullptr}, |
| GenerateRequirementsTestParam{ |
| .test_name = "AddSortOrder", |
| .update_factory = |
| [] { |
| auto schema = CreateTestSchema(); |
| SortField sort_field(1, Transform::Identity(), |
| SortDirection::kAscending, NullOrder::kFirst); |
| auto sort_order = |
| SortOrder::Make(*schema, 1, std::vector<SortField>{sort_field}) |
| .value(); |
| return std::make_unique<table::AddSortOrder>(std::move(sort_order)); |
| }, |
| .expected_existing_table_count = 0, |
| .validator = nullptr}, |
| GenerateRequirementsTestParam{.test_name = "AddSnapshot", |
| .update_factory = |
| [] { |
| auto snapshot = std::make_shared<Snapshot>(); |
| return std::make_unique<table::AddSnapshot>( |
| snapshot); |
| }, |
| .expected_existing_table_count = 0, |
| .validator = nullptr}, |
| GenerateRequirementsTestParam{ |
| .test_name = "RemoveSnapshotRef", |
| .update_factory = |
| [] { return std::make_unique<table::RemoveSnapshotRef>("my-branch"); }, |
| .expected_existing_table_count = 0, |
| .validator = nullptr}, |
| GenerateRequirementsTestParam{ |
| .test_name = "SetProperties", |
| .update_factory = |
| [] { |
| return std::make_unique<table::SetProperties>( |
| std::unordered_map<std::string, std::string>{{"key", "value"}}); |
| }, |
| .expected_existing_table_count = 0, |
| .validator = nullptr}, |
| GenerateRequirementsTestParam{ |
| .test_name = "RemoveProperties", |
| .update_factory = |
| [] { |
| return std::make_unique<table::RemoveProperties>( |
| std::unordered_set<std::string>{"key"}); |
| }, |
| .expected_existing_table_count = 0, |
| .validator = nullptr}, |
| GenerateRequirementsTestParam{ |
| .test_name = "SetLocation", |
| .update_factory = |
| [] { return std::make_unique<table::SetLocation>("s3://new/location"); }, |
| .expected_existing_table_count = 0, |
| .validator = nullptr}, |
| |
| // Updates that generate single requirement for existing tables |
| GenerateRequirementsTestParam{ |
| .test_name = "AddSchema", |
| .update_factory = |
| [] { |
| auto new_schema = std::make_shared<Schema>( |
| std::vector<SchemaField>{ |
| SchemaField::MakeRequired(4, "new_col", string())}, |
| 3); |
| return std::make_unique<table::AddSchema>(new_schema, 3); |
| }, |
| .expected_existing_table_count = 1, |
| .validator = |
| [](const std::vector<std::unique_ptr<TableRequirement>>& reqs, |
| const TableMetadata* base) { |
| auto* assert_id = dynamic_cast<const table::AssertLastAssignedFieldId*>( |
| reqs[0].get()); |
| ASSERT_NE(assert_id, nullptr); |
| EXPECT_EQ(assert_id->last_assigned_field_id(), base->last_column_id); |
| }}, |
| GenerateRequirementsTestParam{ |
| .test_name = "SetCurrentSchema", |
| .update_factory = [] { return std::make_unique<table::SetCurrentSchema>(1); }, |
| .expected_existing_table_count = 1, |
| .validator = |
| [](const std::vector<std::unique_ptr<TableRequirement>>& reqs, |
| const TableMetadata* base) { |
| auto* assert_id = |
| dynamic_cast<const table::AssertCurrentSchemaID*>(reqs[0].get()); |
| ASSERT_NE(assert_id, nullptr); |
| EXPECT_EQ(assert_id->schema_id(), base->current_schema_id); |
| }}, |
| GenerateRequirementsTestParam{ |
| .test_name = "AddPartitionSpec", |
| .update_factory = |
| [] { |
| PartitionField partition_field(1, 1, "id_identity", |
| Transform::Identity()); |
| auto spec = std::shared_ptr<PartitionSpec>( |
| PartitionSpec::Make(1, {partition_field}).value().release()); |
| return std::make_unique<table::AddPartitionSpec>(spec); |
| }, |
| .expected_existing_table_count = 1, |
| .validator = |
| [](const std::vector<std::unique_ptr<TableRequirement>>& reqs, |
| const TableMetadata* base) { |
| auto* assert_id = |
| dynamic_cast<const table::AssertLastAssignedPartitionId*>( |
| reqs[0].get()); |
| ASSERT_NE(assert_id, nullptr); |
| EXPECT_EQ(assert_id->last_assigned_partition_id(), |
| base->last_partition_id); |
| }}, |
| GenerateRequirementsTestParam{ |
| .test_name = "SetDefaultPartitionSpec", |
| .update_factory = |
| [] { return std::make_unique<table::SetDefaultPartitionSpec>(1); }, |
| .expected_existing_table_count = 1, |
| .validator = |
| [](const std::vector<std::unique_ptr<TableRequirement>>& reqs, |
| const TableMetadata* base) { |
| auto* assert_id = |
| dynamic_cast<const table::AssertDefaultSpecID*>(reqs[0].get()); |
| ASSERT_NE(assert_id, nullptr); |
| EXPECT_EQ(assert_id->spec_id(), base->default_spec_id); |
| }}, |
| GenerateRequirementsTestParam{ |
| .test_name = "SetDefaultSortOrder", |
| .update_factory = |
| [] { return std::make_unique<table::SetDefaultSortOrder>(1); }, |
| .expected_existing_table_count = 1, |
| .validator = |
| [](const std::vector<std::unique_ptr<TableRequirement>>& reqs, |
| const TableMetadata* base) { |
| auto* assert_sort_order = |
| dynamic_cast<const table::AssertDefaultSortOrderID*>(reqs[0].get()); |
| ASSERT_NE(assert_sort_order, nullptr); |
| EXPECT_EQ(assert_sort_order->sort_order_id(), |
| base->default_sort_order_id); |
| }}, |
| GenerateRequirementsTestParam{ |
| .test_name = "RemovePartitionSpecs", |
| .update_factory = |
| [] { |
| return std::make_unique<table::RemovePartitionSpecs>( |
| std::vector<int>{1}); |
| }, |
| .expected_existing_table_count = 1, |
| .validator = |
| [](const std::vector<std::unique_ptr<TableRequirement>>& reqs, |
| const TableMetadata* base) { |
| auto* assert_id = |
| dynamic_cast<const table::AssertDefaultSpecID*>(reqs[0].get()); |
| ASSERT_NE(assert_id, nullptr); |
| EXPECT_EQ(assert_id->spec_id(), base->default_spec_id); |
| }}, |
| GenerateRequirementsTestParam{ |
| .test_name = "RemoveSchemas", |
| .update_factory = |
| [] { |
| return std::make_unique<table::RemoveSchemas>( |
| std::unordered_set<int>{1}); |
| }, |
| .expected_existing_table_count = 1, |
| .validator = |
| [](const std::vector<std::unique_ptr<TableRequirement>>& reqs, |
| const TableMetadata* base) { |
| auto* assert_id = |
| dynamic_cast<const table::AssertCurrentSchemaID*>(reqs[0].get()); |
| ASSERT_NE(assert_id, nullptr); |
| EXPECT_EQ(assert_id->schema_id(), base->current_schema_id); |
| }}), |
| [](const testing::TestParamInfo<GenerateRequirementsTestParam>& info) { |
| return info.param.test_name; |
| }); |
| |
| // Test AssignUUID ApplyTo |
| TEST(TableUpdateTest, AssignUUIDApplyUpdate) { |
| auto base = CreateBaseMetadata(); |
| auto builder = TableMetadataBuilder::BuildFrom(base.get()); |
| |
| // Apply AssignUUID update |
| table::AssignUUID uuid_update("apply-uuid"); |
| uuid_update.ApplyTo(*builder); |
| |
| ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build()); |
| EXPECT_EQ(metadata->table_uuid, "apply-uuid"); |
| } |
| |
| // Test AddSortOrder ApplyTo |
| TEST(TableUpdateTest, AddSortOrderApplyUpdate) { |
| auto base = CreateBaseMetadata(); |
| auto builder = TableMetadataBuilder::BuildFrom(base.get()); |
| |
| // Create a sort order |
| auto schema = CreateTestSchema(); |
| SortField sort_field(1, Transform::Identity(), SortDirection::kAscending, |
| NullOrder::kFirst); |
| auto sort_order = std::shared_ptr<SortOrder>( |
| SortOrder::Make(*schema, 1, std::vector<SortField>{sort_field}).value().release()); |
| |
| // Apply AddSortOrder update |
| table::AddSortOrder add_sort_order(sort_order); |
| add_sort_order.ApplyTo(*builder); |
| |
| ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build()); |
| |
| // Verify the sort order was added |
| ASSERT_EQ(metadata->sort_orders.size(), 2); // unsorted + new order |
| auto& added_order = metadata->sort_orders[1]; |
| EXPECT_EQ(added_order->order_id(), 1); |
| EXPECT_EQ(added_order->fields().size(), 1); |
| EXPECT_EQ(added_order->fields()[0].source_id(), 1); |
| EXPECT_EQ(added_order->fields()[0].direction(), SortDirection::kAscending); |
| EXPECT_EQ(added_order->fields()[0].null_order(), NullOrder::kFirst); |
| } |
| |
| // Test SetDefaultSortOrder ApplyTo |
| TEST(TableUpdateTest, SetDefaultSortOrderApplyUpdate) { |
| auto base = CreateBaseMetadata(); |
| |
| // add a sort order to the base metadata |
| auto schema = CreateTestSchema(); |
| SortField sort_field(1, Transform::Identity(), SortDirection::kDescending, |
| NullOrder::kLast); |
| auto sort_order = std::shared_ptr<SortOrder>( |
| SortOrder::Make(*schema, 1, std::vector<SortField>{sort_field}).value().release()); |
| base->sort_orders.push_back(sort_order); |
| |
| auto builder = TableMetadataBuilder::BuildFrom(base.get()); |
| |
| // Apply SetDefaultSortOrder update to set the new sort order as default |
| table::SetDefaultSortOrder set_default_sort_order(1); |
| set_default_sort_order.ApplyTo(*builder); |
| |
| ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build()); |
| |
| // Verify the default sort order was changed |
| EXPECT_EQ(metadata->default_sort_order_id, 1); |
| } |
| |
| // Test SetSnapshotRef ApplyTo for both branch and tag |
| TEST(TableUpdateTest, SetSnapshotRefApplyUpdate) { |
| // Test branch ref |
| { |
| auto base = CreateBaseMetadata(); |
| auto builder = TableMetadataBuilder::BuildFrom(base.get()); |
| |
| // Add a snapshot that the ref will point to |
| auto snapshot = std::make_shared<Snapshot>( |
| Snapshot{.snapshot_id = 123456789, |
| .sequence_number = 1, |
| .timestamp_ms = TimePointMsFromUnixMs(1000000), |
| .manifest_list = "s3://bucket/manifest-list.avro"}); |
| builder->AddSnapshot(snapshot); |
| |
| // Apply SetSnapshotRef update for a branch |
| table::SetSnapshotRef branch_update("my-branch", 123456789, SnapshotRefType::kBranch, |
| 5, 86400000, 604800000); |
| branch_update.ApplyTo(*builder); |
| |
| ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build()); |
| |
| // Verify the branch ref was added |
| ASSERT_EQ(metadata->refs.size(), 1); |
| auto it = metadata->refs.find("my-branch"); |
| ASSERT_NE(it, metadata->refs.end()); |
| |
| const auto& ref = it->second; |
| EXPECT_EQ(ref->snapshot_id, 123456789); |
| EXPECT_EQ(ref->type(), SnapshotRefType::kBranch); |
| |
| const auto& branch = std::get<SnapshotRef::Branch>(ref->retention); |
| EXPECT_EQ(branch.min_snapshots_to_keep.value(), 5); |
| EXPECT_EQ(branch.max_snapshot_age_ms.value(), 86400000); |
| EXPECT_EQ(branch.max_ref_age_ms.value(), 604800000); |
| } |
| |
| // Test tag ref |
| { |
| auto base = CreateBaseMetadata(); |
| auto builder = TableMetadataBuilder::BuildFrom(base.get()); |
| |
| // Add a snapshot that the ref will point to |
| auto snapshot = std::make_shared<Snapshot>( |
| Snapshot{.snapshot_id = 987654321, |
| .sequence_number = 1, |
| .timestamp_ms = TimePointMsFromUnixMs(2000000), |
| .manifest_list = "s3://bucket/manifest-list.avro"}); |
| builder->AddSnapshot(snapshot); |
| |
| // Apply SetSnapshotRef update for a tag |
| table::SetSnapshotRef tag_update("release-1.0", 987654321, SnapshotRefType::kTag, |
| std::nullopt, std::nullopt, 259200000); |
| tag_update.ApplyTo(*builder); |
| |
| ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build()); |
| |
| // Verify the tag ref was added |
| ASSERT_EQ(metadata->refs.size(), 1); |
| auto it = metadata->refs.find("release-1.0"); |
| ASSERT_NE(it, metadata->refs.end()); |
| |
| const auto& ref = it->second; |
| EXPECT_EQ(ref->snapshot_id, 987654321); |
| EXPECT_EQ(ref->type(), SnapshotRefType::kTag); |
| |
| const auto& tag = std::get<SnapshotRef::Tag>(ref->retention); |
| EXPECT_EQ(tag.max_ref_age_ms.value(), 259200000); |
| } |
| } |
| |
| } // namespace iceberg |