This is an automated email from the ASF dual-hosted git repository.
gangwu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 10cc2e0 feat: implement TableMetadataBuilder with AssignUUID (#268)
10cc2e0 is described below
commit 10cc2e0a6fe8fe91fad35def92297c5410115108
Author: Guotao Yu <[email protected]>
AuthorDate: Mon Oct 27 13:17:45 2025 +0800
feat: implement TableMetadataBuilder with AssignUUID (#268)
This commit implements the core TableMetadataBuilder pattern following
the established design from the previous session.
Pattern established:
1. Builder validates input and collects errors (no exceptions)
2. Changes are tracked for serialization
3. Requirements generated for optimistic concurrency
4. Build() validates metadata consistency before returning
---
src/iceberg/partition_spec.h | 1 +
src/iceberg/schema.h | 1 +
src/iceberg/table_metadata.cc | 101 ++++++++-
src/iceberg/table_metadata.h | 7 -
src/iceberg/table_requirement.cc | 17 +-
src/iceberg/table_requirements.cc | 4 +-
src/iceberg/table_update.cc | 21 +-
src/iceberg/test/CMakeLists.txt | 3 +-
src/iceberg/test/matchers.h | 14 ++
src/iceberg/test/meson.build | 1 +
src/iceberg/test/table_metadata_builder_test.cc | 265 ++++++++++++++++++++++++
src/iceberg/util/string_util.h | 2 +-
12 files changed, 409 insertions(+), 28 deletions(-)
diff --git a/src/iceberg/partition_spec.h b/src/iceberg/partition_spec.h
index 5546922..88a081b 100644
--- a/src/iceberg/partition_spec.h
+++ b/src/iceberg/partition_spec.h
@@ -47,6 +47,7 @@ class ICEBERG_EXPORT PartitionSpec : public util::Formattable
{
/// \brief The start ID for partition field. It is only used to generate
/// partition field id for v1 metadata where it is tracked.
static constexpr int32_t kLegacyPartitionDataIdStart = 1000;
+ static constexpr int32_t kInvalidPartitionFieldId = -1;
/// \brief Create a new partition spec.
///
diff --git a/src/iceberg/schema.h b/src/iceberg/schema.h
index 2b30a7d..32914be 100644
--- a/src/iceberg/schema.h
+++ b/src/iceberg/schema.h
@@ -46,6 +46,7 @@ namespace iceberg {
class ICEBERG_EXPORT Schema : public StructType {
public:
static constexpr int32_t kInitialSchemaId = 0;
+ static constexpr int32_t kInvalidColumnId = -1;
explicit Schema(std::vector<SchemaField> fields,
std::optional<int32_t> schema_id = std::nullopt);
diff --git a/src/iceberg/table_metadata.cc b/src/iceberg/table_metadata.cc
index e32e75e..669e5a1 100644
--- a/src/iceberg/table_metadata.cc
+++ b/src/iceberg/table_metadata.cc
@@ -20,6 +20,7 @@
#include "iceberg/table_metadata.h"
#include <algorithm>
+#include <chrono>
#include <format>
#include <string>
@@ -36,9 +37,14 @@
#include "iceberg/table_update.h"
#include "iceberg/util/gzip_internal.h"
#include "iceberg/util/macros.h"
+#include "iceberg/util/uuid.h"
namespace iceberg {
+namespace {
+const TimePointMs kInvalidLastUpdatedMs = TimePointMs::min();
+}
+
std::string ToString(const SnapshotLogEntry& entry) {
return std::format("SnapshotLogEntry[timestampMillis={},snapshotId={}]",
entry.timestamp_ms, entry.snapshot_id);
@@ -201,13 +207,46 @@ Status TableMetadataUtil::Write(FileIO& io, const
std::string& location,
// TableMetadataBuilder implementation
-struct TableMetadataBuilder::Impl {};
+struct TableMetadataBuilder::Impl {
+ // Base metadata (nullptr for new tables)
+ const TableMetadata* base;
+
+ // Working metadata copy
+ TableMetadata metadata;
+
+ // Change tracking
+ std::vector<std::unique_ptr<TableUpdate>> changes;
+
+ // Error collection (since methods return *this and cannot throw)
+ std::vector<Error> errors;
+
+ // Metadata location tracking
+ std::optional<std::string> metadata_location;
+ std::optional<std::string> previous_metadata_location;
+
+ // Constructor for new table
+ explicit Impl(int8_t format_version) : base(nullptr), metadata{} {
+ metadata.format_version = format_version;
+ metadata.last_sequence_number = TableMetadata::kInitialSequenceNumber;
+ metadata.last_updated_ms = kInvalidLastUpdatedMs;
+ metadata.last_column_id = Schema::kInvalidColumnId;
+ metadata.default_spec_id = PartitionSpec::kInitialSpecId;
+ metadata.last_partition_id = PartitionSpec::kInvalidPartitionFieldId;
+ metadata.current_snapshot_id = Snapshot::kInvalidSnapshotId;
+ metadata.default_sort_order_id = SortOrder::kInitialSortOrderId;
+ metadata.next_row_id = TableMetadata::kInitialRowId;
+ }
+
+ // Constructor from existing metadata
+ explicit Impl(const TableMetadata* base_metadata)
+ : base(base_metadata), metadata(*base_metadata) {}
+};
TableMetadataBuilder::TableMetadataBuilder(int8_t format_version)
- : impl_(std::make_unique<Impl>()) {}
+ : impl_(std::make_unique<Impl>(format_version)) {}
TableMetadataBuilder::TableMetadataBuilder(const TableMetadata* base)
- : impl_(std::make_unique<Impl>()) {}
+ : impl_(std::make_unique<Impl>(base)) {}
TableMetadataBuilder::~TableMetadataBuilder() = default;
@@ -238,12 +277,35 @@ TableMetadataBuilder&
TableMetadataBuilder::SetPreviousMetadataLocation(
}
TableMetadataBuilder& TableMetadataBuilder::AssignUUID() {
- throw IcebergError(std::format("{} not implemented", __FUNCTION__));
+ if (impl_->metadata.table_uuid.empty()) {
+ // Generate a random UUID
+ return AssignUUID(Uuid::GenerateV4().ToString());
+ }
+
+ return *this;
}
TableMetadataBuilder& TableMetadataBuilder::AssignUUID(std::string_view uuid) {
- throw IcebergError(std::format("{} not implemented", __FUNCTION__));
- ;
+ std::string uuid_str(uuid);
+
+ // Validation: UUID cannot be empty
+ if (uuid_str.empty()) {
+ impl_->errors.emplace_back(ErrorKind::kInvalidArgument, "Cannot assign
empty UUID");
+ return *this;
+ }
+
+ // Check if UUID is already set to the same value (no-op)
+ if (StringUtils::EqualsIgnoreCase(impl_->metadata.table_uuid, uuid_str)) {
+ return *this;
+ }
+
+ // Update the metadata
+ impl_->metadata.table_uuid = uuid_str;
+
+ // Record the change
+
impl_->changes.push_back(std::make_unique<table::AssignUUID>(std::move(uuid_str)));
+
+ return *this;
}
TableMetadataBuilder& TableMetadataBuilder::UpgradeFormatVersion(
@@ -377,12 +439,29 @@ TableMetadataBuilder&
TableMetadataBuilder::RemoveEncryptionKey(std::string_view
throw IcebergError(std::format("{} not implemented", __FUNCTION__));
}
-TableMetadataBuilder& TableMetadataBuilder::DiscardChanges() {
- throw IcebergError(std::format("{} not implemented", __FUNCTION__));
-}
-
Result<std::unique_ptr<TableMetadata>> TableMetadataBuilder::Build() {
- return NotImplemented("TableMetadataBuilder::Build not implemented");
+ // 1. Check for accumulated errors
+ if (!impl_->errors.empty()) {
+ std::string error_msg = "Failed to build TableMetadata due to validation
errors:\n";
+ for (const auto& [kind, message] : impl_->errors) {
+ error_msg += " - " + message + "\n";
+ }
+ return CommitFailed("{}", error_msg);
+ }
+
+ // 2. Validate metadata consistency through TableMetadata#Validate
+
+ // 3. Update last_updated_ms if there are changes
+ if (impl_->metadata.last_updated_ms == kInvalidLastUpdatedMs) {
+ impl_->metadata.last_updated_ms =
+ TimePointMs{std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::system_clock::now().time_since_epoch())};
+ }
+
+ // 4. Create and return the TableMetadata
+ auto result = std::make_unique<TableMetadata>(std::move(impl_->metadata));
+
+ return result;
}
} // namespace iceberg
diff --git a/src/iceberg/table_metadata.h b/src/iceberg/table_metadata.h
index 11b17eb..2a998c7 100644
--- a/src/iceberg/table_metadata.h
+++ b/src/iceberg/table_metadata.h
@@ -377,13 +377,6 @@ class ICEBERG_EXPORT TableMetadataBuilder {
/// \return Reference to this builder for method chaining
TableMetadataBuilder& RemoveEncryptionKey(std::string_view key_id);
- /// \brief Discard all accumulated changes
- ///
- /// This is useful when you want to reset the builder state without
- /// creating a new builder instance.
- /// \return Reference to this builder for method chaining
- TableMetadataBuilder& DiscardChanges();
-
/// \brief Build the TableMetadata object
///
/// \return A Result containing the constructed TableMetadata or an error
diff --git a/src/iceberg/table_requirement.cc b/src/iceberg/table_requirement.cc
index 4ca4b91..e951d70 100644
--- a/src/iceberg/table_requirement.cc
+++ b/src/iceberg/table_requirement.cc
@@ -20,15 +20,28 @@
#include "iceberg/table_requirement.h"
#include "iceberg/table_metadata.h"
+#include "iceberg/util/string_util.h"
namespace iceberg::table {
Status AssertDoesNotExist::Validate(const TableMetadata* base) const {
- return NotImplemented("AssertTableDoesNotExist::Validate not implemented");
+ return NotImplemented("AssertDoesNotExist::Validate not implemented");
}
Status AssertUUID::Validate(const TableMetadata* base) const {
- return NotImplemented("AssertTableUUID::Validate not implemented");
+ // Validate that the table UUID matches the expected value
+
+ if (base == nullptr) {
+ return CommitFailed("Requirement failed: current table metadata is
missing");
+ }
+
+ if (!StringUtils::EqualsIgnoreCase(base->table_uuid, uuid_)) {
+ return CommitFailed(
+ "Requirement failed: table UUID does not match (expected='{}',
actual='{}')",
+ uuid_, base->table_uuid);
+ }
+
+ return {};
}
Status AssertRefSnapshotID::Validate(const TableMetadata* base) const {
diff --git a/src/iceberg/table_requirements.cc
b/src/iceberg/table_requirements.cc
index 1eb870c..aae874e 100644
--- a/src/iceberg/table_requirements.cc
+++ b/src/iceberg/table_requirements.cc
@@ -26,11 +26,11 @@
namespace iceberg {
void TableUpdateContext::AddRequirement(std::unique_ptr<TableRequirement>
requirement) {
- throw IcebergError("TableUpdateContext::AddRequirement not implemented");
+ requirements_.emplace_back(std::move(requirement));
}
Result<std::vector<std::unique_ptr<TableRequirement>>>
TableUpdateContext::Build() {
- return NotImplemented("TableUpdateContext::Build not implemented");
+ return std::move(requirements_);
}
Result<std::vector<std::unique_ptr<TableRequirement>>>
TableRequirements::ForCreateTable(
diff --git a/src/iceberg/table_update.cc b/src/iceberg/table_update.cc
index 7d81dd8..fcb7a58 100644
--- a/src/iceberg/table_update.cc
+++ b/src/iceberg/table_update.cc
@@ -21,6 +21,7 @@
#include "iceberg/exception.h"
#include "iceberg/table_metadata.h"
+#include "iceberg/table_requirement.h"
#include "iceberg/table_requirements.h"
namespace iceberg::table {
@@ -28,11 +29,24 @@ namespace iceberg::table {
// AssignUUID
void AssignUUID::ApplyTo(TableMetadataBuilder& builder) const {
- throw IcebergError(std::format("{} not implemented", __FUNCTION__));
+ builder.AssignUUID(uuid_);
}
Status AssignUUID::GenerateRequirements(TableUpdateContext& context) const {
- return NotImplemented("AssignTableUUID::GenerateRequirements not
implemented");
+ // AssignUUID operation generates a requirement to assert the table's UUID
+ // if a base metadata exists (i.e., this is an update operation)
+
+ const TableMetadata* base = context.base();
+
+ if (base != nullptr && !base->table_uuid.empty()) {
+ // For table updates, assert that the current UUID matches what we expect
+ context.AddRequirement(std::make_unique<AssertUUID>(base->table_uuid));
+ }
+
+ // Note: For table creation (base == nullptr), no UUID requirement is needed
+ // as the table doesn't exist yet
+
+ return {};
}
// UpgradeFormatVersion
@@ -42,8 +56,7 @@ void UpgradeFormatVersion::ApplyTo(TableMetadataBuilder&
builder) const {
}
Status UpgradeFormatVersion::GenerateRequirements(TableUpdateContext& context)
const {
- return NotImplemented(
- "UpgradeTableFormatVersion::GenerateRequirements not implemented");
+ return NotImplemented("UpgradeFormatVersion::GenerateRequirements not
implemented");
}
// AddSchema
diff --git a/src/iceberg/test/CMakeLists.txt b/src/iceberg/test/CMakeLists.txt
index 68af62b..100287f 100644
--- a/src/iceberg/test/CMakeLists.txt
+++ b/src/iceberg/test/CMakeLists.txt
@@ -82,7 +82,8 @@ add_iceberg_test(table_test
test_common.cc
json_internal_test.cc
table_test.cc
- schema_json_test.cc)
+ schema_json_test.cc
+ table_metadata_builder_test.cc)
add_iceberg_test(expression_test
SOURCES
diff --git a/src/iceberg/test/matchers.h b/src/iceberg/test/matchers.h
index f04d4a5..55d29be 100644
--- a/src/iceberg/test/matchers.h
+++ b/src/iceberg/test/matchers.h
@@ -23,6 +23,7 @@
#include <gtest/gtest.h>
#include "iceberg/result.h"
+#include "iceberg/util/macros.h"
/*
* \brief Define custom matchers for expected<T, Error> values
@@ -210,4 +211,17 @@ auto ErrorIs(MatcherT&& matcher) {
ResultMatcher<std::decay_t<MatcherT>>(false,
std::forward<MatcherT>(matcher)));
}
+// Evaluate `rexpr` which should return a Result<T, Error>.
+// On success: assign the contained value to `lhs`.
+// On failure: fail the test with the error message.
+#define ICEBERG_UNWRAP_OR_FAIL_IMPL(result_name, lhs, rexpr) \
+ auto&& result_name = (rexpr); \
+ ASSERT_TRUE(result_name.has_value()) \
+ << "Operation failed: " << result_name.error().message; \
+ lhs = std::move(result_name.value());
+
+#define ICEBERG_UNWRAP_OR_FAIL(lhs, rexpr)
\
+ ICEBERG_UNWRAP_OR_FAIL_IMPL(ICEBERG_ASSIGN_OR_RAISE_NAME(result_,
__COUNTER__), lhs, \
+ rexpr)
+
} // namespace iceberg
diff --git a/src/iceberg/test/meson.build b/src/iceberg/test/meson.build
index 88b1632..89cb357 100644
--- a/src/iceberg/test/meson.build
+++ b/src/iceberg/test/meson.build
@@ -47,6 +47,7 @@ iceberg_tests = {
'sources': files(
'json_internal_test.cc',
'schema_json_test.cc',
+ 'table_metadata_builder_test.cc',
'table_test.cc',
'test_common.cc',
),
diff --git a/src/iceberg/test/table_metadata_builder_test.cc
b/src/iceberg/test/table_metadata_builder_test.cc
new file mode 100644
index 0000000..aa22f85
--- /dev/null
+++ b/src/iceberg/test/table_metadata_builder_test.cc
@@ -0,0 +1,265 @@
+/*
+ * 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 <memory>
+#include <string>
+
+#include <gmock/gmock-matchers.h>
+#include <gtest/gtest.h>
+
+#include "iceberg/partition_spec.h"
+#include "iceberg/sort_order.h"
+#include "iceberg/table_metadata.h"
+#include "iceberg/table_requirement.h"
+#include "iceberg/table_requirements.h"
+#include "iceberg/table_update.h"
+#include "iceberg/test/matchers.h"
+
+namespace iceberg {
+
+// Helper functions to reduce test boilerplate
+namespace {
+
+// Generate requirements and return them
+std::vector<std::unique_ptr<TableRequirement>> GenerateRequirements(
+ const TableUpdate& update, const TableMetadata* base) {
+ TableUpdateContext context(base, /*is_replace=*/false);
+ EXPECT_THAT(update.GenerateRequirements(context), IsOk());
+
+ auto requirements = context.Build();
+ EXPECT_THAT(requirements, IsOk());
+ return std::move(requirements.value());
+}
+
+} // namespace
+
+// Test fixture for TableMetadataBuilder tests
+class TableMetadataBuilderTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ // Create a base metadata for update tests
+ base_metadata_ = std::make_unique<TableMetadata>();
+ base_metadata_->format_version = 2;
+ base_metadata_->table_uuid = "test-uuid-1234";
+ base_metadata_->location = "s3://bucket/test";
+ base_metadata_->last_sequence_number = 0;
+ base_metadata_->last_updated_ms =
TimePointMs{std::chrono::milliseconds(1000)};
+ base_metadata_->last_column_id = 0;
+ base_metadata_->default_spec_id = PartitionSpec::kInitialSpecId;
+ base_metadata_->last_partition_id = 0;
+ base_metadata_->current_snapshot_id = Snapshot::kInvalidSnapshotId;
+ base_metadata_->default_sort_order_id = SortOrder::kInitialSortOrderId;
+ base_metadata_->next_row_id = TableMetadata::kInitialRowId;
+ }
+
+ std::unique_ptr<TableMetadata> base_metadata_;
+};
+
+// ============================================================================
+// TableMetadataBuilder - Basic Construction Tests
+// ============================================================================
+
+TEST_F(TableMetadataBuilderTest, BuildFromEmpty) {
+ auto builder = TableMetadataBuilder::BuildFromEmpty(2);
+ ASSERT_NE(builder, nullptr);
+
+ builder->AssignUUID("new-uuid-5678");
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ ASSERT_NE(metadata, nullptr);
+
+ EXPECT_EQ(metadata->format_version, 2);
+ EXPECT_EQ(metadata->last_sequence_number,
TableMetadata::kInitialSequenceNumber);
+ EXPECT_EQ(metadata->default_spec_id, PartitionSpec::kInitialSpecId);
+ EXPECT_EQ(metadata->default_sort_order_id, SortOrder::kInitialSortOrderId);
+ EXPECT_EQ(metadata->current_snapshot_id, Snapshot::kInvalidSnapshotId);
+}
+
+TEST_F(TableMetadataBuilderTest, BuildFromExisting) {
+ auto builder = TableMetadataBuilder::BuildFrom(base_metadata_.get());
+ ASSERT_NE(builder, nullptr);
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ ASSERT_NE(metadata, nullptr);
+
+ EXPECT_EQ(metadata->format_version, 2);
+ EXPECT_EQ(metadata->table_uuid, "test-uuid-1234");
+ EXPECT_EQ(metadata->location, "s3://bucket/test");
+}
+
+// ============================================================================
+// TableMetadataBuilder - AssignUUID Tests
+// ============================================================================
+
+TEST_F(TableMetadataBuilderTest, AssignUUIDForNewTable) {
+ auto builder = TableMetadataBuilder::BuildFromEmpty(2);
+ builder->AssignUUID("new-uuid-5678");
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ EXPECT_EQ(metadata->table_uuid, "new-uuid-5678");
+}
+
+TEST_F(TableMetadataBuilderTest, AssignUUIDAndUpdateExisting) {
+ auto builder = TableMetadataBuilder::BuildFrom(base_metadata_.get());
+ builder->AssignUUID("updated-uuid-9999");
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ EXPECT_EQ(metadata->table_uuid, "updated-uuid-9999");
+}
+
+TEST_F(TableMetadataBuilderTest, AssignUUIDWithEmptyUUID) {
+ auto builder = TableMetadataBuilder::BuildFromEmpty(2);
+ builder->AssignUUID("");
+
+ ASSERT_THAT(builder->Build(), HasErrorMessage("Cannot assign empty UUID"));
+}
+
+TEST_F(TableMetadataBuilderTest, AssignUUIDWithSameUUID) {
+ auto builder = TableMetadataBuilder::BuildFrom(base_metadata_.get());
+ builder->AssignUUID("test-uuid-1234"); // Same UUID
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ EXPECT_EQ(metadata->table_uuid, "test-uuid-1234");
+}
+
+TEST_F(TableMetadataBuilderTest, AssignUUIDWithAutoGenerate) {
+ auto builder = TableMetadataBuilder::BuildFromEmpty(2);
+ builder->AssignUUID(); // Auto-generate
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ EXPECT_FALSE(metadata->table_uuid.empty());
+}
+
+TEST_F(TableMetadataBuilderTest, AssignUUIDAndCaseInsensitiveComparison) {
+ base_metadata_->table_uuid = "TEST-UUID-ABCD";
+ auto builder = TableMetadataBuilder::BuildFrom(base_metadata_.get());
+ builder->AssignUUID("test-uuid-abcd"); // Different case - should be no-op
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ EXPECT_EQ(metadata->table_uuid, "TEST-UUID-ABCD"); // Original case
preserved
+}
+
+// ============================================================================
+// TableUpdate - ApplyTo Tests
+// ============================================================================
+
+TEST_F(TableMetadataBuilderTest, TableUpdateWithAssignUUID) {
+ auto builder = TableMetadataBuilder::BuildFromEmpty(2);
+
+ table::AssignUUID update("apply-uuid");
+ update.ApplyTo(*builder);
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ EXPECT_EQ(metadata->table_uuid, "apply-uuid");
+}
+
+// ============================================================================
+// TableUpdate - GenerateRequirements Tests
+// ============================================================================
+
+TEST_F(TableMetadataBuilderTest,
+ TableUpdateWithAssignUUIDAndGenerateRequirementsForNewTable) {
+ table::AssignUUID update("new-uuid");
+
+ auto requirements = GenerateRequirements(update, nullptr);
+ EXPECT_TRUE(requirements.empty()); // No requirements for new table
+}
+
+TEST_F(TableMetadataBuilderTest,
+ TableUpdateWithAssignUUIDAndGenerateRequirementsForExistingTable) {
+ table::AssignUUID update("new-uuid");
+
+ auto requirements = GenerateRequirements(update, base_metadata_.get());
+ EXPECT_EQ(requirements.size(), 1); // Should generate AssertUUID requirement
+}
+
+TEST_F(TableMetadataBuilderTest,
+ TableUpdateWithAssignUUIDAndGenerateRequirementsWithEmptyUUID) {
+ base_metadata_->table_uuid = "";
+ table::AssignUUID update("new-uuid");
+
+ auto requirements = GenerateRequirements(update, base_metadata_.get());
+ EXPECT_TRUE(requirements.empty()); // No requirement when base has no UUID
+}
+
+// ============================================================================
+// TableRequirement - Validate Tests
+// ============================================================================
+
+TEST_F(TableMetadataBuilderTest, TableRequirementAssertUUIDSuccess) {
+ table::AssertUUID requirement("test-uuid-1234");
+
+ ASSERT_THAT(requirement.Validate(base_metadata_.get()), IsOk());
+}
+
+TEST_F(TableMetadataBuilderTest, TableRequirementAssertUUIDMismatch) {
+ table::AssertUUID requirement("wrong-uuid");
+
+ auto status = requirement.Validate(base_metadata_.get());
+ EXPECT_THAT(status, IsError(ErrorKind::kCommitFailed));
+ EXPECT_THAT(status, HasErrorMessage("UUID does not match"));
+}
+
+TEST_F(TableMetadataBuilderTest, TableRequirementAssertUUIDNullBase) {
+ table::AssertUUID requirement("any-uuid");
+
+ auto status = requirement.Validate(nullptr);
+ EXPECT_THAT(status, IsError(ErrorKind::kCommitFailed));
+ EXPECT_THAT(status, HasErrorMessage("metadata is missing"));
+}
+
+TEST_F(TableMetadataBuilderTest, TableRequirementAssertUUIDCaseInsensitive) {
+ base_metadata_->table_uuid = "TEST-UUID-1234";
+ table::AssertUUID requirement("test-uuid-1234");
+
+ ASSERT_THAT(requirement.Validate(base_metadata_.get()), IsOk());
+}
+
+// ============================================================================
+// Integration Tests - End-to-End Workflow
+// ============================================================================
+
+TEST_F(TableMetadataBuilderTest, IntegrationCreateTableWithUUID) {
+ auto builder = TableMetadataBuilder::BuildFromEmpty(2);
+ builder->AssignUUID("integration-test-uuid");
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ EXPECT_EQ(metadata->table_uuid, "integration-test-uuid");
+ EXPECT_EQ(metadata->format_version, 2);
+}
+
+TEST_F(TableMetadataBuilderTest, IntegrationOptimisticConcurrencyControl) {
+ table::AssignUUID update("new-uuid");
+
+ // Generate and validate requirements
+ auto requirements = GenerateRequirements(update, base_metadata_.get());
+ for (const auto& req : requirements) {
+ auto val_status = req->Validate(base_metadata_.get());
+ ASSERT_THAT(val_status, IsOk()) << "Requirement validation failed";
+ }
+
+ // Apply update and build
+ auto builder = TableMetadataBuilder::BuildFrom(base_metadata_.get());
+ update.ApplyTo(*builder);
+
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ ASSERT_NE(metadata, nullptr);
+}
+
+} // namespace iceberg
diff --git a/src/iceberg/util/string_util.h b/src/iceberg/util/string_util.h
index a22aa7a..aafb740 100644
--- a/src/iceberg/util/string_util.h
+++ b/src/iceberg/util/string_util.h
@@ -45,7 +45,7 @@ class ICEBERG_EXPORT StringUtils {
return input;
}
- static bool EqualsIgnoreCase(const std::string& lhs, const std::string& rhs)
{
+ static bool EqualsIgnoreCase(std::string_view lhs, std::string_view rhs) {
return std::ranges::equal(
lhs, rhs, [](char lc, char rc) { return std::tolower(lc) ==
std::tolower(rc); });
}