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 e1acefdb feat: add sort order to table metadata builder (#345)
e1acefdb is described below
commit e1acefdb2297a37ebc797dc025a7ad5e50bf1761
Author: Feiyang Li <[email protected]>
AuthorDate: Thu Dec 4 15:33:12 2025 +0800
feat: add sort order to table metadata builder (#345)
---
src/iceberg/table_metadata.cc | 118 +++++++++++++++-
src/iceberg/table_metadata.h | 11 ++
src/iceberg/test/table_metadata_builder_test.cc | 170 ++++++++++++++++++++++--
src/iceberg/test/table_update_test.cc | 52 ++++++++
src/iceberg/util/error_collector.h | 15 +++
5 files changed, 348 insertions(+), 18 deletions(-)
diff --git a/src/iceberg/table_metadata.cc b/src/iceberg/table_metadata.cc
index 780ab61d..a90c5b0c 100644
--- a/src/iceberg/table_metadata.cc
+++ b/src/iceberg/table_metadata.cc
@@ -21,9 +21,12 @@
#include <algorithm>
#include <chrono>
+#include <cstdint>
#include <format>
+#include <optional>
#include <ranges>
#include <string>
+#include <unordered_map>
#include <nlohmann/json.hpp>
@@ -39,12 +42,11 @@
#include "iceberg/util/gzip_internal.h"
#include "iceberg/util/macros.h"
#include "iceberg/util/uuid.h"
-
namespace iceberg {
-
namespace {
const TimePointMs kInvalidLastUpdatedMs = TimePointMs::min();
-}
+constexpr int32_t kLastAdded = -1;
+} // namespace
std::string ToString(const SnapshotLogEntry& entry) {
return std::format("SnapshotLogEntry[timestampMillis={},snapshotId={}]",
@@ -274,11 +276,19 @@ struct TableMetadataBuilder::Impl {
// Change tracking
std::vector<std::unique_ptr<TableUpdate>> changes;
+ std::optional<int32_t> last_added_schema_id;
+ std::optional<int32_t> last_added_order_id;
+ std::optional<int32_t> last_added_spec_id;
// Metadata location tracking
std::optional<std::string> metadata_location;
std::optional<std::string> previous_metadata_location;
+ // indexes for convenience
+ std::unordered_map<int32_t, std::shared_ptr<Schema>> schemas_by_id;
+ std::unordered_map<int32_t, std::shared_ptr<PartitionSpec>> specs_by_id;
+ std::unordered_map<int32_t, std::shared_ptr<SortOrder>> sort_orders_by_id;
+
// Constructor for new table
explicit Impl(int8_t format_version) : base(nullptr), metadata{} {
metadata.format_version = format_version;
@@ -294,7 +304,22 @@ struct TableMetadataBuilder::Impl {
// Constructor from existing metadata
explicit Impl(const TableMetadata* base_metadata)
- : base(base_metadata), metadata(*base_metadata) {}
+ : base(base_metadata), metadata(*base_metadata) {
+ // Initialize index maps from base metadata
+ for (const auto& schema : metadata.schemas) {
+ if (schema->schema_id().has_value()) {
+ schemas_by_id.emplace(schema->schema_id().value(), schema);
+ }
+ }
+
+ for (const auto& spec : metadata.partition_specs) {
+ specs_by_id.emplace(spec->spec_id(), spec);
+ }
+
+ for (const auto& order : metadata.sort_orders) {
+ sort_orders_by_id.emplace(order->order_id(), order);
+ }
+ }
};
TableMetadataBuilder::TableMetadataBuilder(int8_t format_version)
@@ -434,16 +459,95 @@ TableMetadataBuilder& TableMetadataBuilder::RemoveSchemas(
TableMetadataBuilder& TableMetadataBuilder::SetDefaultSortOrder(
std::shared_ptr<SortOrder> order) {
- throw IcebergError(std::format("{} not implemented", __FUNCTION__));
+ BUILDER_ASSIGN_OR_RETURN(auto order_id, AddSortOrderInternal(*order));
+ return SetDefaultSortOrder(order_id);
}
TableMetadataBuilder& TableMetadataBuilder::SetDefaultSortOrder(int32_t
order_id) {
- throw IcebergError(std::format("{} not implemented", __FUNCTION__));
+ if (order_id == -1) {
+ if (!impl_->last_added_order_id.has_value()) {
+ return AddError(ErrorKind::kInvalidArgument,
+ "Cannot set last added sort order: no sort order has
been added");
+ }
+ return SetDefaultSortOrder(impl_->last_added_order_id.value());
+ }
+
+ if (order_id == impl_->metadata.default_sort_order_id) {
+ return *this;
+ }
+
+ impl_->metadata.default_sort_order_id = order_id;
+
+ if (impl_->last_added_order_id == std::make_optional(order_id)) {
+
impl_->changes.push_back(std::make_unique<table::SetDefaultSortOrder>(kLastAdded));
+ } else {
+
impl_->changes.push_back(std::make_unique<table::SetDefaultSortOrder>(order_id));
+ }
+ return *this;
+}
+
+Result<int32_t> TableMetadataBuilder::AddSortOrderInternal(const SortOrder&
order) {
+ int32_t new_order_id = ReuseOrCreateNewSortOrderId(order);
+
+ if (impl_->sort_orders_by_id.find(new_order_id) !=
impl_->sort_orders_by_id.end()) {
+ // update last_added_order_id if the order was added in this set of
changes (since it
+ // is now the last)
+ bool is_new_order =
+ impl_->last_added_order_id.has_value() &&
+ std::ranges::find_if(impl_->changes, [new_order_id](const auto&
change) {
+ auto* add_sort_order =
dynamic_cast<table::AddSortOrder*>(change.get());
+ return add_sort_order &&
+ add_sort_order->sort_order()->order_id() == new_order_id;
+ }) != impl_->changes.cend();
+ impl_->last_added_order_id =
+ is_new_order ? std::make_optional(new_order_id) : std::nullopt;
+ return new_order_id;
+ }
+
+ // Get current schema and validate the sort order against it
+ ICEBERG_ASSIGN_OR_RAISE(auto schema, impl_->metadata.Schema());
+ ICEBERG_RETURN_UNEXPECTED(order.Validate(*schema));
+
+ std::shared_ptr<SortOrder> new_order;
+ if (order.is_unsorted()) {
+ new_order = SortOrder::Unsorted();
+ } else {
+ // Unlike freshSortOrder from Java impl, we don't use field name from old
bound
+ // schema to rebuild the sort order.
+ ICEBERG_ASSIGN_OR_RAISE(
+ new_order,
+ SortOrder::Make(new_order_id,
std::vector<SortField>(order.fields().begin(),
+
order.fields().end())));
+ }
+
+ impl_->metadata.sort_orders.push_back(new_order);
+ impl_->sort_orders_by_id.emplace(new_order_id, new_order);
+
+ impl_->changes.push_back(std::make_unique<table::AddSortOrder>(new_order));
+ impl_->last_added_order_id = new_order_id;
+ return new_order_id;
}
TableMetadataBuilder& TableMetadataBuilder::AddSortOrder(
std::shared_ptr<SortOrder> order) {
- throw IcebergError(std::format("{} not implemented", __FUNCTION__));
+ BUILDER_ASSIGN_OR_RETURN(auto order_id, AddSortOrderInternal(*order));
+ return *this;
+}
+
+int32_t TableMetadataBuilder::ReuseOrCreateNewSortOrderId(const SortOrder&
new_order) {
+ if (new_order.is_unsorted()) {
+ return SortOrder::kUnsortedOrderId;
+ }
+ // determine the next order id
+ int32_t new_order_id = SortOrder::kInitialSortOrderId;
+ for (const auto& order : impl_->metadata.sort_orders) {
+ if (order->SameOrder(new_order)) {
+ return order->order_id();
+ } else if (new_order_id <= order->order_id()) {
+ new_order_id = order->order_id() + 1;
+ }
+ }
+ return new_order_id;
}
TableMetadataBuilder& TableMetadataBuilder::AddSnapshot(
diff --git a/src/iceberg/table_metadata.h b/src/iceberg/table_metadata.h
index 503b9b14..2d53fcb0 100644
--- a/src/iceberg/table_metadata.h
+++ b/src/iceberg/table_metadata.h
@@ -436,6 +436,17 @@ class ICEBERG_EXPORT TableMetadataBuilder : public
ErrorCollector {
/// \brief Private constructor for building from existing metadata
explicit TableMetadataBuilder(const TableMetadata* base);
+ /// \brief Internal method to add a sort order and return its ID
+ /// \param order The sort order to add
+ /// \return The ID of the added or reused sort order
+ Result<int32_t> AddSortOrderInternal(const SortOrder& order);
+
+ /// \brief Internal method to check for existing sort order and reuse its ID
or create a
+ /// new one
+ /// \param new_order The sort order to check
+ /// \return The ID to use for this sort order (reused if exists, new
otherwise)
+ int32_t ReuseOrCreateNewSortOrderId(const SortOrder& new_order);
+
/// Internal state members
struct Impl;
std::unique_ptr<Impl> impl_;
diff --git a/src/iceberg/test/table_metadata_builder_test.cc
b/src/iceberg/test/table_metadata_builder_test.cc
index ff41ae18..a1e46615 100644
--- a/src/iceberg/test/table_metadata_builder_test.cc
+++ b/src/iceberg/test/table_metadata_builder_test.cc
@@ -19,20 +19,33 @@
#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_update.h"
#include "iceberg/test/matchers.h"
+#include "iceberg/transform.h"
+#include "iceberg/type.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 create base metadata for tests
std::unique_ptr<TableMetadata> CreateBaseMetadata() {
auto metadata = std::make_unique<TableMetadata>();
@@ -41,11 +54,14 @@ std::unique_ptr<TableMetadata> CreateBaseMetadata() {
metadata->location = "s3://bucket/test";
metadata->last_sequence_number = 0;
metadata->last_updated_ms = TimePointMs{std::chrono::milliseconds(1000)};
- metadata->last_column_id = 0;
+ metadata->last_column_id = 3;
+ metadata->current_schema_id = 0;
+ metadata->schemas.push_back(CreateTestSchema());
metadata->default_spec_id = PartitionSpec::kInitialSpecId;
metadata->last_partition_id = 0;
metadata->current_snapshot_id = Snapshot::kInvalidSnapshotId;
metadata->default_sort_order_id = SortOrder::kInitialSortOrderId;
+ metadata->sort_orders.push_back(SortOrder::Unsorted());
metadata->next_row_id = TableMetadata::kInitialRowId;
return metadata;
}
@@ -82,7 +98,7 @@ TEST(TableMetadataBuilderTest, BuildFromExisting) {
EXPECT_EQ(metadata->location, "s3://bucket/test");
}
-// Test AssignUUID method
+// Test AssignUUID
TEST(TableMetadataBuilderTest, AssignUUID) {
// Assign UUID for new table
auto builder = TableMetadataBuilder::BuildFromEmpty(2);
@@ -174,17 +190,149 @@ TEST(TableMetadataBuilderTest, UpgradeFormatVersion) {
EXPECT_THAT(builder->Build(), HasErrorMessage("Cannot downgrade"));
}
-// Test applying TableUpdate to builder
-TEST(TableMetadataBuilderTest, ApplyUpdate) {
- // Apply AssignUUID update
- auto builder = TableMetadataBuilder::BuildFromEmpty(2);
- table::AssignUUID update("apply-uuid");
- update.ApplyTo(*builder);
- // TODO(Li Feiyang): Add more update and `apply` once other build methods are
- // implemented
+// Test AddSortOrder
+TEST(TableMetadataBuilderTest, AddSortOrderBasic) {
+ auto base = CreateBaseMetadata();
+ auto builder = TableMetadataBuilder::BuildFrom(base.get());
+ auto schema = CreateTestSchema();
+ // 1. Add unsorted - should reuse existing unsorted order
+ builder->AddSortOrder(SortOrder::Unsorted());
ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
- EXPECT_EQ(metadata->table_uuid, "apply-uuid");
+ ASSERT_EQ(metadata->sort_orders.size(), 1);
+ EXPECT_TRUE(metadata->sort_orders[0]->is_unsorted());
+
+ // 2. Add basic sort order
+ builder = TableMetadataBuilder::BuildFrom(base.get());
+ SortField field1(1, Transform::Identity(), SortDirection::kAscending,
+ NullOrder::kFirst);
+ ICEBERG_UNWRAP_OR_FAIL(auto order1,
+ SortOrder::Make(*schema, 1,
std::vector<SortField>{field1}));
+ builder->AddSortOrder(std::move(order1));
+ ICEBERG_UNWRAP_OR_FAIL(metadata, builder->Build());
+ ASSERT_EQ(metadata->sort_orders.size(), 2);
+ EXPECT_EQ(metadata->sort_orders[1]->order_id(), 1);
+
+ // 3. Add duplicate - should be idempotent
+ builder = TableMetadataBuilder::BuildFrom(base.get());
+ ICEBERG_UNWRAP_OR_FAIL(auto order2,
+ SortOrder::Make(*schema, 1,
std::vector<SortField>{field1}));
+ ICEBERG_UNWRAP_OR_FAIL(auto order3,
+ SortOrder::Make(*schema, 1,
std::vector<SortField>{field1}));
+ builder->AddSortOrder(std::move(order2));
+ builder->AddSortOrder(std::move(order3)); // Duplicate
+ ICEBERG_UNWRAP_OR_FAIL(metadata, builder->Build());
+ ASSERT_EQ(metadata->sort_orders.size(), 2); // Only one added
+
+ // 4. Add multiple different orders + verify ID reassignment
+ builder = TableMetadataBuilder::BuildFrom(base.get());
+ SortField field2(2, Transform::Identity(), SortDirection::kDescending,
+ NullOrder::kLast);
+ // User provides ID=99, Builder should reassign to ID=1
+ ICEBERG_UNWRAP_OR_FAIL(auto order4,
+ SortOrder::Make(*schema, 99,
std::vector<SortField>{field1}));
+ ICEBERG_UNWRAP_OR_FAIL(
+ auto order5, SortOrder::Make(*schema, 2, std::vector<SortField>{field1,
field2}));
+ builder->AddSortOrder(std::move(order4));
+ builder->AddSortOrder(std::move(order5));
+ ICEBERG_UNWRAP_OR_FAIL(metadata, builder->Build());
+ ASSERT_EQ(metadata->sort_orders.size(), 3);
+ EXPECT_EQ(metadata->sort_orders[1]->order_id(), 1); // Reassigned from 99
+ EXPECT_EQ(metadata->sort_orders[2]->order_id(), 2);
+}
+
+TEST(TableMetadataBuilderTest, AddSortOrderInvalid) {
+ auto base = CreateBaseMetadata();
+ auto schema = CreateTestSchema();
+
+ // 1. Invalid field ID
+ auto builder = TableMetadataBuilder::BuildFrom(base.get());
+ SortField invalid_field(999, Transform::Identity(),
SortDirection::kAscending,
+ NullOrder::kFirst);
+ ICEBERG_UNWRAP_OR_FAIL(auto order1,
+ SortOrder::Make(1,
std::vector<SortField>{invalid_field}));
+ builder->AddSortOrder(std::move(order1));
+ ASSERT_THAT(builder->Build(), IsError(ErrorKind::kValidationFailed));
+ ASSERT_THAT(builder->Build(), HasErrorMessage("Cannot find source column"));
+
+ // 2. Invalid transform (Day transform on string type)
+ builder = TableMetadataBuilder::BuildFrom(base.get());
+ SortField invalid_transform(2, Transform::Day(), SortDirection::kAscending,
+ NullOrder::kFirst);
+ ICEBERG_UNWRAP_OR_FAIL(auto order2,
+ SortOrder::Make(1,
std::vector<SortField>{invalid_transform}));
+ builder->AddSortOrder(std::move(order2));
+ ASSERT_THAT(builder->Build(), IsError(ErrorKind::kValidationFailed));
+ ASSERT_THAT(builder->Build(), HasErrorMessage("Invalid source type"));
+
+ // 3. Without schema
+ builder = TableMetadataBuilder::BuildFromEmpty(2);
+ builder->AssignUUID("test-uuid");
+ SortField field(1, Transform::Identity(), SortDirection::kAscending,
NullOrder::kFirst);
+ ICEBERG_UNWRAP_OR_FAIL(auto order3,
+ SortOrder::Make(*schema, 1,
std::vector<SortField>{field}));
+ builder->AddSortOrder(std::move(order3));
+ ASSERT_THAT(builder->Build(), IsError(ErrorKind::kValidationFailed));
+ ASSERT_THAT(builder->Build(), HasErrorMessage("Schema with ID"));
+}
+
+// Test SetDefaultSortOrder
+TEST(TableMetadataBuilderTest, SetDefaultSortOrderBasic) {
+ auto base = CreateBaseMetadata();
+ auto schema = CreateTestSchema();
+
+ // 1. Set default sort order by SortOrder object
+ auto builder = TableMetadataBuilder::BuildFrom(base.get());
+ SortField field1(1, Transform::Identity(), SortDirection::kAscending,
+ NullOrder::kFirst);
+ ICEBERG_UNWRAP_OR_FAIL(auto order1_unique,
+ SortOrder::Make(*schema, 1,
std::vector<SortField>{field1}));
+ auto order1 = std::shared_ptr<SortOrder>(std::move(order1_unique));
+ builder->SetDefaultSortOrder(order1);
+ ICEBERG_UNWRAP_OR_FAIL(auto metadata, builder->Build());
+ ASSERT_EQ(metadata->sort_orders.size(), 2);
+ EXPECT_EQ(metadata->default_sort_order_id, 1);
+ EXPECT_EQ(metadata->sort_orders[1]->order_id(), 1);
+
+ // 2. Set default sort order by order ID
+ builder = TableMetadataBuilder::BuildFrom(base.get());
+ SortField field2(1, Transform::Identity(), SortDirection::kAscending,
+ NullOrder::kFirst);
+ ICEBERG_UNWRAP_OR_FAIL(auto order2_unique,
+ SortOrder::Make(*schema, 1,
std::vector<SortField>{field2}));
+ auto order2 = std::shared_ptr<SortOrder>(std::move(order2_unique));
+ builder->AddSortOrder(order2);
+ builder->SetDefaultSortOrder(1);
+ ICEBERG_UNWRAP_OR_FAIL(metadata, builder->Build());
+ EXPECT_EQ(metadata->default_sort_order_id, 1);
+
+ // 3. Set default sort order using -1 (last added)
+ builder = TableMetadataBuilder::BuildFrom(base.get());
+ SortField field3(2, Transform::Identity(), SortDirection::kDescending,
+ NullOrder::kLast);
+ ICEBERG_UNWRAP_OR_FAIL(auto order3_unique,
+ SortOrder::Make(*schema, 1,
std::vector<SortField>{field3}));
+ auto order3 = std::shared_ptr<SortOrder>(std::move(order3_unique));
+ builder->AddSortOrder(order3);
+ builder->SetDefaultSortOrder(-1); // Use last added
+ ICEBERG_UNWRAP_OR_FAIL(metadata, builder->Build());
+ EXPECT_EQ(metadata->default_sort_order_id, 1);
+
+ // 4. Setting same order is no-op
+ builder = TableMetadataBuilder::BuildFrom(base.get());
+ builder->SetDefaultSortOrder(0);
+ ICEBERG_UNWRAP_OR_FAIL(metadata, builder->Build());
+ EXPECT_EQ(metadata->default_sort_order_id, 0);
+}
+
+TEST(TableMetadataBuilderTest, SetDefaultSortOrderInvalid) {
+ auto base = CreateBaseMetadata();
+
+ // Try to use -1 (last added) when no order has been added
+ auto builder = TableMetadataBuilder::BuildFrom(base.get());
+ builder->SetDefaultSortOrder(-1);
+ ASSERT_THAT(builder->Build(), IsError(ErrorKind::kValidationFailed));
+ ASSERT_THAT(builder->Build(), HasErrorMessage("no sort order has been
added"));
}
} // namespace iceberg
diff --git a/src/iceberg/test/table_update_test.cc
b/src/iceberg/test/table_update_test.cc
index d250b6d2..afa70894 100644
--- a/src/iceberg/test/table_update_test.cc
+++ b/src/iceberg/test/table_update_test.cc
@@ -314,4 +314,56 @@ TEST(TableUpdateTest, AssignUUIDApplyUpdate) {
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);
+}
+
} // namespace iceberg
diff --git a/src/iceberg/util/error_collector.h
b/src/iceberg/util/error_collector.h
index f94967f9..48ea717e 100644
--- a/src/iceberg/util/error_collector.h
+++ b/src/iceberg/util/error_collector.h
@@ -30,6 +30,21 @@
namespace iceberg {
+#define BUILDER_RETURN_IF_ERROR(result) \
+ if (auto&& result_name = result; !result_name) [[unlikely]] { \
+ errors_.emplace_back(std::move(result_name.error())); \
+ return *this; \
+ }
+
+#define BUILDER_ASSIGN_OR_RETURN_IMPL(result_name, lhs, rexpr) \
+ auto&& result_name = (rexpr); \
+ BUILDER_RETURN_IF_ERROR(result_name) \
+ lhs = std::move(result_name.value());
+
+#define BUILDER_ASSIGN_OR_RETURN(lhs, rexpr)
\
+ BUILDER_ASSIGN_OR_RETURN_IMPL(ICEBERG_ASSIGN_OR_RAISE_NAME(result_,
__COUNTER__), lhs, \
+ rexpr)
+
/// \brief Base class for collecting validation errors in builder patterns
///
/// This class provides error accumulation functionality for builders that