diff --git a/src/iceberg/manifest_reader_internal.cc b/src/iceberg/manifest_reader_internal.cc index 0ff743edb..126bd7557 100644 --- a/src/iceberg/manifest_reader_internal.cc +++ b/src/iceberg/manifest_reader_internal.cc @@ -222,8 +222,8 @@ Result> ParseManifestList(ArrowSchema* schema, if (!field.has_value()) { return InvalidSchema("Field index {} is not found in schema", idx); } - auto field_name = field.value().get().name(); - bool required = !field.value().get().optional(); + auto field_name = field.value()->get().name(); + bool required = !field.value()->get().optional(); auto view_of_column = array_view.children[idx]; switch (idx) { case 0: @@ -340,8 +340,8 @@ Status ParseDataFile(const std::shared_ptr& data_file_schema, data_file_schema->fields().size(), view_of_column->n_children); } for (int64_t col_idx = 0; col_idx < view_of_column->n_children; ++col_idx) { - auto field_name = data_file_schema->GetFieldByIndex(col_idx).value().get().name(); - auto required = !data_file_schema->GetFieldByIndex(col_idx).value().get().optional(); + auto field_name = data_file_schema->GetFieldByIndex(col_idx).value()->get().name(); + auto required = !data_file_schema->GetFieldByIndex(col_idx).value()->get().optional(); auto view_of_file_field = view_of_column->children[col_idx]; auto manifest_entry_count = view_of_file_field->length; @@ -487,8 +487,8 @@ Result> ParseManifestEntry(ArrowSchema* schema, if (!field.has_value()) { return InvalidManifest("Field not found in schema: {}", idx); } - auto field_name = field.value().get().name(); - bool required = !field.value().get().optional(); + auto field_name = field.value()->get().name(); + bool required = !field.value()->get().optional(); auto view_of_column = array_view.children[idx]; switch (idx) { @@ -510,7 +510,7 @@ Result> ParseManifestEntry(ArrowSchema* schema, break; case 4: { auto data_file_schema = - dynamic_pointer_cast(field.value().get().type()); + dynamic_pointer_cast(field.value()->get().type()); ICEBERG_RETURN_UNEXPECTED( ParseDataFile(data_file_schema, view_of_column, manifest_entries)); break; diff --git a/src/iceberg/table_scan.cc b/src/iceberg/table_scan.cc index a7edd5d79..b52db2265 100644 --- a/src/iceberg/table_scan.cc +++ b/src/iceberg/table_scan.cc @@ -115,7 +115,7 @@ Result> TableScanBuilder::Build() { return InvalidArgument("Column {} not found in schema '{}'", column_name, *schema_id); } - projected_fields.emplace_back(field_opt.value().get()); + projected_fields.emplace_back(field_opt.value()->get()); } context_.projected_schema = std::make_shared(std::move(projected_fields), schema->schema_id()); diff --git a/src/iceberg/type.cc b/src/iceberg/type.cc index e66f96daf..c50bb7cb5 100644 --- a/src/iceberg/type.cc +++ b/src/iceberg/type.cc @@ -25,23 +25,18 @@ #include "iceberg/exception.h" #include "iceberg/util/formatter.h" // IWYU pragma: keep +#include "iceberg/util/macros.h" +#include "iceberg/util/string_util.h" namespace iceberg { -StructType::StructType(std::vector fields) : fields_(std::move(fields)) { - size_t index = 0; - for (const auto& field : fields_) { - auto [it, inserted] = field_id_to_index_.try_emplace(field.field_id(), index); - if (!inserted) { - throw IcebergError( - std::format("StructType: duplicate field ID {} (field indices {} and {})", - field.field_id(), it->second, index)); - } - - ++index; - } +Result> NestedType::GetFieldByName( + std::string_view name) const { + return GetFieldByName(name, /*case_sensitive=*/true); } +StructType::StructType(std::vector fields) : fields_(std::move(fields)) {} + TypeId StructType::type_id() const { return kTypeId; } std::string StructType::ToString() const { @@ -53,27 +48,34 @@ std::string StructType::ToString() const { return repr; } std::span StructType::fields() const { return fields_; } -std::optional> StructType::GetFieldById( +Result> StructType::GetFieldById( int32_t field_id) const { - auto it = field_id_to_index_.find(field_id); - if (it == field_id_to_index_.end()) return std::nullopt; - return fields_[it->second]; + ICEBERG_RETURN_UNEXPECTED(InitFieldById()); + auto it = field_by_id_.find(field_id); + if (it == field_by_id_.end()) return std::nullopt; + return it->second; } -std::optional> StructType::GetFieldByIndex( +Result> StructType::GetFieldByIndex( int32_t index) const { - if (index < 0 || index >= static_cast(fields_.size())) { - return std::nullopt; + if (index < 0 || static_cast(index) >= fields_.size()) { + return InvalidArgument("Invalid index {} to get field from struct", index); } return fields_[index]; } -std::optional> StructType::GetFieldByName( - std::string_view name) const { - // N.B. duplicate names are not permitted (looking at the Java - // implementation) so there is nothing in particular we need to do here - for (const auto& field : fields_) { - if (field.name() == name) { - return field; +Result> StructType::GetFieldByName( + std::string_view name, bool case_sensitive) const { + if (case_sensitive) { + ICEBERG_RETURN_UNEXPECTED(InitFieldByName()); + auto it = field_by_name_.find(name); + if (it != field_by_name_.end()) { + return it->second; } + return std::nullopt; + } + ICEBERG_RETURN_UNEXPECTED(InitFieldByLowerCaseName()); + auto it = field_by_lowercase_name_.find(StringUtils::ToLower(name)); + if (it != field_by_lowercase_name_.end()) { + return it->second; } return std::nullopt; } @@ -84,6 +86,48 @@ bool StructType::Equals(const Type& other) const { const auto& struct_ = static_cast(other); return fields_ == struct_.fields_; } +Status StructType::InitFieldById() const { + if (!field_by_id_.empty()) { + return {}; + } + for (const auto& field : fields_) { + auto it = field_by_id_.try_emplace(field.field_id(), field); + if (!it.second) { + return InvalidSchema("Duplicate field id found: {} (prev name: {}, curr name: {})", + field.field_id(), it.first->second.get().name(), field.name()); + } + } + return {}; +} +Status StructType::InitFieldByName() const { + if (!field_by_name_.empty()) { + return {}; + } + for (const auto& field : fields_) { + auto it = field_by_name_.try_emplace(field.name(), field); + if (!it.second) { + return InvalidSchema("Duplicate field name found: {} (prev id: {}, curr id: {})", + it.first->first, it.first->second.get().field_id(), + field.field_id()); + } + } + return {}; +} +Status StructType::InitFieldByLowerCaseName() const { + if (!field_by_lowercase_name_.empty()) { + return {}; + } + for (const auto& field : fields_) { + auto it = + field_by_lowercase_name_.try_emplace(StringUtils::ToLower(field.name()), field); + if (!it.second) { + return InvalidSchema( + "Duplicate lowercase field name found: {} (prev id: {}, curr id: {})", + it.first->first, it.first->second.get().field_id(), field.field_id()); + } + } + return {}; +} ListType::ListType(SchemaField element) : element_(std::move(element)) { if (element_.name() != kElementName) { @@ -105,23 +149,29 @@ std::string ListType::ToString() const { return repr; } std::span ListType::fields() const { return {&element_, 1}; } -std::optional> ListType::GetFieldById( +Result> ListType::GetFieldById( int32_t field_id) const { if (field_id == element_.field_id()) { return std::cref(element_); } return std::nullopt; } -std::optional> ListType::GetFieldByIndex( +Result> ListType::GetFieldByIndex( int index) const { if (index == 0) { return std::cref(element_); } - return std::nullopt; + return InvalidArgument("Invalid index {} to get field from list", index); } -std::optional> ListType::GetFieldByName( - std::string_view name) const { - if (name == element_.name()) { +Result> ListType::GetFieldByName( + std::string_view name, bool case_sensitive) const { + if (case_sensitive) { + if (name == kElementName) { + return std::cref(element_); + } + return std::nullopt; + } + if (StringUtils::ToLower(name) == kElementName) { return std::cref(element_); } return std::nullopt; @@ -159,7 +209,7 @@ std::string MapType::ToString() const { return repr; } std::span MapType::fields() const { return fields_; } -std::optional> MapType::GetFieldById( +Result> MapType::GetFieldById( int32_t field_id) const { if (field_id == key().field_id()) { return key(); @@ -168,20 +218,29 @@ std::optional> MapType::GetFieldById( } return std::nullopt; } -std::optional> MapType::GetFieldByIndex( +Result> MapType::GetFieldByIndex( int32_t index) const { if (index == 0) { return key(); } else if (index == 1) { return value(); } - return std::nullopt; + return InvalidArgument("Invalid index {} to get field from map", index); } -std::optional> MapType::GetFieldByName( - std::string_view name) const { - if (name == kKeyName) { +Result> MapType::GetFieldByName( + std::string_view name, bool case_sensitive) const { + if (case_sensitive) { + if (name == kKeyName) { + return key(); + } else if (name == kValueName) { + return value(); + } + return std::nullopt; + } + const auto lower_case_name = StringUtils::ToLower(name); + if (lower_case_name == kKeyName) { return key(); - } else if (name == kValueName) { + } else if (lower_case_name == kValueName) { return value(); } return std::nullopt; diff --git a/src/iceberg/type.h b/src/iceberg/type.h index 78c0141b1..4cb405c04 100644 --- a/src/iceberg/type.h +++ b/src/iceberg/type.h @@ -33,6 +33,7 @@ #include #include "iceberg/iceberg_export.h" +#include "iceberg/result.h" #include "iceberg/schema_field.h" #include "iceberg/util/formattable.h" @@ -75,23 +76,27 @@ class ICEBERG_EXPORT NestedType : public Type { /// \brief Get a view of the child fields. [[nodiscard]] virtual std::span fields() const = 0; + using SchemaFieldConstRef = std::reference_wrapper; /// \brief Get a field by field ID. /// /// \note This is O(1) complexity. - [[nodiscard]] virtual std::optional> - GetFieldById(int32_t field_id) const = 0; + [[nodiscard]] virtual Result> GetFieldById( + int32_t field_id) const = 0; /// \brief Get a field by index. /// /// \note This is O(1) complexity. - [[nodiscard]] virtual std::optional> - GetFieldByIndex(int32_t index) const = 0; - /// \brief Get a field by name (case-sensitive). Behavior is undefined if + [[nodiscard]] virtual Result> GetFieldByIndex( + int32_t index) const = 0; + /// \brief Get a field by name. Return an error Status if /// the field name is not unique; prefer GetFieldById or GetFieldByIndex /// when possible. /// - /// \note This is currently O(n) complexity. - [[nodiscard]] virtual std::optional> - GetFieldByName(std::string_view name) const = 0; + /// \note This is O(1) complexity. + [[nodiscard]] virtual Result> GetFieldByName( + std::string_view name, bool case_sensitive) const = 0; + /// \brief Get a field by name (case-sensitive). + [[nodiscard]] Result> GetFieldByName( + std::string_view name) const; }; /// \defgroup type-nested Nested Types @@ -109,18 +114,26 @@ class ICEBERG_EXPORT StructType : public NestedType { std::string ToString() const override; std::span fields() const override; - std::optional> GetFieldById( + Result> GetFieldById( int32_t field_id) const override; - std::optional> GetFieldByIndex( + Result> GetFieldByIndex( int32_t index) const override; - std::optional> GetFieldByName( - std::string_view name) const override; + Result> GetFieldByName( + std::string_view name, bool case_sensitive) const override; + using NestedType::GetFieldByName; protected: bool Equals(const Type& other) const override; + // TODO(nullccxsy): Lazy initialization has concurrency issues, need to add proper + // synchronization mechanism + Status InitFieldById() const; + Status InitFieldByName() const; + Status InitFieldByLowerCaseName() const; std::vector fields_; - std::unordered_map field_id_to_index_; + mutable std::unordered_map field_by_id_; + mutable std::unordered_map field_by_name_; + mutable std::unordered_map field_by_lowercase_name_; }; /// \brief A data type representing a list of values. @@ -140,12 +153,13 @@ class ICEBERG_EXPORT ListType : public NestedType { std::string ToString() const override; std::span fields() const override; - std::optional> GetFieldById( + Result> GetFieldById( int32_t field_id) const override; - std::optional> GetFieldByIndex( + Result> GetFieldByIndex( int32_t index) const override; - std::optional> GetFieldByName( - std::string_view name) const override; + Result> GetFieldByName( + std::string_view name, bool case_sensitive) const override; + using NestedType::GetFieldByName; protected: bool Equals(const Type& other) const override; @@ -172,12 +186,13 @@ class ICEBERG_EXPORT MapType : public NestedType { std::string ToString() const override; std::span fields() const override; - std::optional> GetFieldById( + Result> GetFieldById( int32_t field_id) const override; - std::optional> GetFieldByIndex( + Result> GetFieldByIndex( int32_t index) const override; - std::optional> GetFieldByName( - std::string_view name) const override; + Result> GetFieldByName( + std::string_view name, bool case_sensitive) const override; + using NestedType::GetFieldByName; protected: bool Equals(const Type& other) const override; diff --git a/src/iceberg/util/macros.h b/src/iceberg/util/macros.h index 3519c9a63..f11a680cc 100644 --- a/src/iceberg/util/macros.h +++ b/src/iceberg/util/macros.h @@ -19,13 +19,10 @@ #pragma once -#define ICEBERG_RETURN_UNEXPECTED(result) \ - do { \ - auto&& result_name = (result); \ - if (!result_name) [[unlikely]] { \ - return std::unexpected(result_name.error()); \ - } \ - } while (false); +#define ICEBERG_RETURN_UNEXPECTED(result) \ + if (auto&& result_name = result; !result_name) [[unlikely]] { \ + return std::unexpected(result_name.error()); \ + } #define ICEBERG_ASSIGN_OR_RAISE_IMPL(result_name, lhs, rexpr) \ auto&& result_name = (rexpr); \ diff --git a/test/schema_test.cc b/test/schema_test.cc index d282cea9d..272c6e75a 100644 --- a/test/schema_test.cc +++ b/test/schema_test.cc @@ -27,6 +27,7 @@ #include "iceberg/schema_field.h" #include "iceberg/util/formatter.h" // IWYU pragma: keep +#include "matchers.h" TEST(SchemaTest, Basics) { { @@ -47,18 +48,16 @@ TEST(SchemaTest, Basics) { ASSERT_THAT(schema.GetFieldByName("bar"), ::testing::Optional(field2)); ASSERT_EQ(std::nullopt, schema.GetFieldById(0)); - ASSERT_EQ(std::nullopt, schema.GetFieldByIndex(2)); - ASSERT_EQ(std::nullopt, schema.GetFieldByIndex(-1)); + auto result = schema.GetFieldByIndex(2); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index 2 to get field from struct")); + result = schema.GetFieldByIndex(-1); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index -1 to get field from struct")); ASSERT_EQ(std::nullopt, schema.GetFieldByName("element")); } - ASSERT_THAT( - []() { - iceberg::SchemaField field1(5, "foo", iceberg::int32(), true); - iceberg::SchemaField field2(5, "bar", iceberg::string(), true); - iceberg::Schema schema({field1, field2}, 100); - }, - ::testing::ThrowsMessage( - ::testing::HasSubstr("duplicate field ID 5"))); } TEST(SchemaTest, Equality) { diff --git a/test/type_test.cc b/test/type_test.cc index fca886928..9963ab364 100644 --- a/test/type_test.cc +++ b/test/type_test.cc @@ -28,6 +28,7 @@ #include "iceberg/exception.h" #include "iceberg/util/formatter.h" // IWYU pragma: keep +#include "matchers.h" struct TypeTestCase { /// Test case name, must be safe for Googletest (alphanumeric + underscore) @@ -315,13 +316,22 @@ TEST(TypeTest, List) { std::span fields = list.fields(); ASSERT_EQ(1, fields.size()); ASSERT_EQ(field, fields[0]); - ASSERT_THAT(list.GetFieldById(5), ::testing::Optional(field)); + auto result = list.GetFieldByIndex(5); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index 5 to get field from list")); ASSERT_THAT(list.GetFieldByIndex(0), ::testing::Optional(field)); ASSERT_THAT(list.GetFieldByName("element"), ::testing::Optional(field)); ASSERT_EQ(std::nullopt, list.GetFieldById(0)); - ASSERT_EQ(std::nullopt, list.GetFieldByIndex(1)); - ASSERT_EQ(std::nullopt, list.GetFieldByIndex(-1)); + result = list.GetFieldByIndex(1); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index 1 to get field from list")); + result = list.GetFieldByIndex(-1); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index -1 to get field from list")); ASSERT_EQ(std::nullopt, list.GetFieldByName("foo")); } ASSERT_THAT( @@ -350,8 +360,14 @@ TEST(TypeTest, Map) { ASSERT_THAT(map.GetFieldByName("value"), ::testing::Optional(value)); ASSERT_EQ(std::nullopt, map.GetFieldById(0)); - ASSERT_EQ(std::nullopt, map.GetFieldByIndex(2)); - ASSERT_EQ(std::nullopt, map.GetFieldByIndex(-1)); + auto result = map.GetFieldByIndex(2); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index 2 to get field from map")); + result = map.GetFieldByIndex(-1); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index -1 to get field from map")); ASSERT_EQ(std::nullopt, map.GetFieldByName("element")); } ASSERT_THAT( @@ -389,16 +405,109 @@ TEST(TypeTest, Struct) { ASSERT_THAT(struct_.GetFieldByName("bar"), ::testing::Optional(field2)); ASSERT_EQ(std::nullopt, struct_.GetFieldById(0)); - ASSERT_EQ(std::nullopt, struct_.GetFieldByIndex(2)); - ASSERT_EQ(std::nullopt, struct_.GetFieldByIndex(-1)); + auto result = struct_.GetFieldByIndex(2); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index 2 to get field from struct")); + result = struct_.GetFieldByIndex(-1); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidArgument)); + ASSERT_THAT(result, + iceberg::HasErrorMessage("Invalid index -1 to get field from struct")); ASSERT_EQ(std::nullopt, struct_.GetFieldByName("element")); } - ASSERT_THAT( - []() { - iceberg::SchemaField field1(5, "foo", iceberg::int32(), true); - iceberg::SchemaField field2(5, "bar", iceberg::string(), true); - iceberg::StructType struct_({field1, field2}); - }, - ::testing::ThrowsMessage( - ::testing::HasSubstr("duplicate field ID 5"))); +} + +TEST(TypeTest, StructTypeGetFieldByName) { + iceberg::SchemaField field1(1, "Foo", iceberg::int32(), true); + iceberg::SchemaField field2(2, "Bar", iceberg::string(), false); + iceberg::StructType struct_({field1, field2}); + + // Case-sensitive: exact match + ASSERT_THAT(struct_.GetFieldByName("Foo"), ::testing::Optional(field1)); + ASSERT_THAT(struct_.GetFieldByName("foo"), ::testing::Eq(std::nullopt)); + + // Case-insensitive + ASSERT_THAT(struct_.GetFieldByName("foo", false), ::testing::Optional(field1)); + ASSERT_THAT(struct_.GetFieldByName("fOO", false), ::testing::Optional(field1)); + ASSERT_THAT(struct_.GetFieldByName("FOO", false), ::testing::Optional(field1)); + ASSERT_THAT(struct_.GetFieldByName("bar", false), ::testing::Optional(field2)); + ASSERT_THAT(struct_.GetFieldByName("BaR", false), ::testing::Optional(field2)); + ASSERT_THAT(struct_.GetFieldByName("BAR", false), ::testing::Optional(field2)); + ASSERT_THAT(struct_.GetFieldByName("baz", false), ::testing::Eq(std::nullopt)); +} + +TEST(TypeTest, ListTypeGetFieldByName) { + iceberg::SchemaField element(1, "element", iceberg::int32(), true); + iceberg::ListType list(element); + + // Case-sensitive: exact match + ASSERT_THAT(list.GetFieldByName("element"), ::testing::Optional(element)); + ASSERT_THAT(list.GetFieldByName("Element"), ::testing::Eq(std::nullopt)); + + // Case-insensitive + ASSERT_THAT(list.GetFieldByName("element", false), ::testing::Optional(element)); + ASSERT_THAT(list.GetFieldByName("Element", false), ::testing::Optional(element)); + ASSERT_THAT(list.GetFieldByName("ELEMENT", false), ::testing::Optional(element)); + ASSERT_THAT(list.GetFieldByName("eLeMeNt", false), ::testing::Optional(element)); + ASSERT_THAT(list.GetFieldByName("foo", false), ::testing::Eq(std::nullopt)); +} + +TEST(TypeTest, MapTypeGetFieldByName) { + iceberg::SchemaField key(1, "key", iceberg::int32(), true); + iceberg::SchemaField value(2, "value", iceberg::string(), false); + iceberg::MapType map(key, value); + + // Case-sensitive: exact match + ASSERT_THAT(map.GetFieldByName("key"), ::testing::Optional(key)); + ASSERT_THAT(map.GetFieldByName("Key"), ::testing::Eq(std::nullopt)); + ASSERT_THAT(map.GetFieldByName("value"), ::testing::Optional(value)); + ASSERT_THAT(map.GetFieldByName("Value"), ::testing::Eq(std::nullopt)); + + // Case-insensitive + ASSERT_THAT(map.GetFieldByName("Key", false), ::testing::Optional(key)); + ASSERT_THAT(map.GetFieldByName("KEY", false), ::testing::Optional(key)); + ASSERT_THAT(map.GetFieldByName("kEy", false), ::testing::Optional(key)); + ASSERT_THAT(map.GetFieldByName("value", false), ::testing::Optional(value)); + ASSERT_THAT(map.GetFieldByName("Value", false), ::testing::Optional(value)); + ASSERT_THAT(map.GetFieldByName("VALUE", false), ::testing::Optional(value)); + ASSERT_THAT(map.GetFieldByName("vAlUe", false), ::testing::Optional(value)); + ASSERT_THAT(map.GetFieldByName("foo", false), ::testing::Eq(std::nullopt)); +} + +TEST(TypeTest, StructDuplicateId) { + iceberg::SchemaField field1(5, "foo", iceberg::int32(), true); + iceberg::SchemaField field2(5, "bar", iceberg::string(), true); + iceberg::StructType struct_({field1, field2}); + + auto result = struct_.GetFieldById(5); + ASSERT_FALSE(result.has_value()); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidSchema)); + ASSERT_THAT(result, + iceberg::HasErrorMessage( + "Duplicate field id found: 5 (prev name: foo, curr name: bar)")); +} + +TEST(TypeTest, StructDuplicateName) { + iceberg::SchemaField field1(1, "foo", iceberg::int32(), true); + iceberg::SchemaField field2(2, "foo", iceberg::string(), true); + iceberg::StructType struct_({field1, field2}); + + auto result = struct_.GetFieldByName("foo", true); + ASSERT_FALSE(result.has_value()); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidSchema)); + ASSERT_THAT(result, iceberg::HasErrorMessage( + "Duplicate field name found: foo (prev id: 1, curr id: 2)")); +} + +TEST(TypeTest, StructDuplicateLowerCaseName) { + iceberg::SchemaField field1(1, "Foo", iceberg::int32(), true); + iceberg::SchemaField field2(2, "foo", iceberg::string(), true); + iceberg::StructType struct_({field1, field2}); + + auto result = struct_.GetFieldByName("foo", false); + ASSERT_FALSE(result.has_value()); + ASSERT_THAT(result, IsError(iceberg::ErrorKind::kInvalidSchema)); + ASSERT_THAT(result, + iceberg::HasErrorMessage( + "Duplicate lowercase field name found: foo (prev id: 1, curr id: 2)")); }