You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by fo...@apache.org on 2025/09/03 19:45:37 UTC

(iceberg-cpp) branch main updated: feat: add find field (by name) support to NestedType (#194)

This is an automated email from the ASF dual-hosted git repository.

fokko 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 b80bf8d  feat: add find field (by name) support to NestedType (#194)
b80bf8d is described below

commit b80bf8d36c6df808b28b36dee0367444e0845cae
Author: chao liu <81...@users.noreply.github.com>
AuthorDate: Thu Sep 4 03:45:31 2025 +0800

    feat: add find field (by name) support to NestedType (#194)
    
    … MapType
    
    - Implemented case-insensitive GetFieldByName in NestedType subclasses.
        - Added lazy initialization for maps in StructType
        - Handled duplicate names/IDs with Status returns instead of throws.
    
    ---------
    
    Co-authored-by: nullccxsy <32...@qq.com>
---
 src/iceberg/manifest_reader_internal.cc |  14 ++--
 src/iceberg/table_scan.cc               |   2 +-
 src/iceberg/type.cc                     | 137 ++++++++++++++++++++++---------
 src/iceberg/type.h                      |  57 ++++++++-----
 src/iceberg/util/macros.h               |  11 +--
 test/schema_test.cc                     |  19 +++--
 test/type_test.cc                       | 139 ++++++++++++++++++++++++++++----
 7 files changed, 279 insertions(+), 100 deletions(-)

diff --git a/src/iceberg/manifest_reader_internal.cc b/src/iceberg/manifest_reader_internal.cc
index 0ff743e..126bd75 100644
--- a/src/iceberg/manifest_reader_internal.cc
+++ b/src/iceberg/manifest_reader_internal.cc
@@ -222,8 +222,8 @@ Result<std::vector<ManifestFile>> 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<StructType>& 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<std::vector<ManifestEntry>> 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<std::vector<ManifestEntry>> ParseManifestEntry(ArrowSchema* schema,
         break;
       case 4: {
         auto data_file_schema =
-            dynamic_pointer_cast<StructType>(field.value().get().type());
+            dynamic_pointer_cast<StructType>(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 a7edd5d..b52db22 100644
--- a/src/iceberg/table_scan.cc
+++ b/src/iceberg/table_scan.cc
@@ -115,7 +115,7 @@ Result<std::unique_ptr<TableScan>> 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<Schema>(std::move(projected_fields), schema->schema_id());
diff --git a/src/iceberg/type.cc b/src/iceberg/type.cc
index e66f96d..c50bb7c 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<SchemaField> 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<std::optional<NestedType::SchemaFieldConstRef>> NestedType::GetFieldByName(
+    std::string_view name) const {
+  return GetFieldByName(name, /*case_sensitive=*/true);
 }
 
+StructType::StructType(std::vector<SchemaField> 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<const SchemaField> StructType::fields() const { return fields_; }
-std::optional<std::reference_wrapper<const SchemaField>> StructType::GetFieldById(
+Result<std::optional<NestedType::SchemaFieldConstRef>> 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<std::reference_wrapper<const SchemaField>> StructType::GetFieldByIndex(
+Result<std::optional<NestedType::SchemaFieldConstRef>> StructType::GetFieldByIndex(
     int32_t index) const {
-  if (index < 0 || index >= static_cast<int32_t>(fields_.size())) {
-    return std::nullopt;
+  if (index < 0 || static_cast<size_t>(index) >= fields_.size()) {
+    return InvalidArgument("Invalid index {} to get field from struct", index);
   }
   return fields_[index];
 }
-std::optional<std::reference_wrapper<const SchemaField>> 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<std::optional<NestedType::SchemaFieldConstRef>> 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<const StructType&>(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<const SchemaField> ListType::fields() const { return {&element_, 1}; }
-std::optional<std::reference_wrapper<const SchemaField>> ListType::GetFieldById(
+Result<std::optional<NestedType::SchemaFieldConstRef>> ListType::GetFieldById(
     int32_t field_id) const {
   if (field_id == element_.field_id()) {
     return std::cref(element_);
   }
   return std::nullopt;
 }
-std::optional<std::reference_wrapper<const SchemaField>> ListType::GetFieldByIndex(
+Result<std::optional<NestedType::SchemaFieldConstRef>> 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<std::reference_wrapper<const SchemaField>> ListType::GetFieldByName(
-    std::string_view name) const {
-  if (name == element_.name()) {
+Result<std::optional<NestedType::SchemaFieldConstRef>> 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<const SchemaField> MapType::fields() const { return fields_; }
-std::optional<std::reference_wrapper<const SchemaField>> MapType::GetFieldById(
+Result<std::optional<NestedType::SchemaFieldConstRef>> MapType::GetFieldById(
     int32_t field_id) const {
   if (field_id == key().field_id()) {
     return key();
@@ -168,20 +218,29 @@ std::optional<std::reference_wrapper<const SchemaField>> MapType::GetFieldById(
   }
   return std::nullopt;
 }
-std::optional<std::reference_wrapper<const SchemaField>> MapType::GetFieldByIndex(
+Result<std::optional<NestedType::SchemaFieldConstRef>> 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<std::reference_wrapper<const SchemaField>> MapType::GetFieldByName(
-    std::string_view name) const {
-  if (name == kKeyName) {
+Result<std::optional<NestedType::SchemaFieldConstRef>> 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 78c0141..4cb405c 100644
--- a/src/iceberg/type.h
+++ b/src/iceberg/type.h
@@ -33,6 +33,7 @@
 #include <vector>
 
 #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<const SchemaField> fields() const = 0;
+  using SchemaFieldConstRef = std::reference_wrapper<const SchemaField>;
   /// \brief Get a field by field ID.
   ///
   /// \note This is O(1) complexity.
-  [[nodiscard]] virtual std::optional<std::reference_wrapper<const SchemaField>>
-  GetFieldById(int32_t field_id) const = 0;
+  [[nodiscard]] virtual Result<std::optional<SchemaFieldConstRef>> GetFieldById(
+      int32_t field_id) const = 0;
   /// \brief Get a field by index.
   ///
   /// \note This is O(1) complexity.
-  [[nodiscard]] virtual std::optional<std::reference_wrapper<const SchemaField>>
-  GetFieldByIndex(int32_t index) const = 0;
-  /// \brief Get a field by name (case-sensitive).  Behavior is undefined if
+  [[nodiscard]] virtual Result<std::optional<SchemaFieldConstRef>> 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<std::reference_wrapper<const SchemaField>>
-  GetFieldByName(std::string_view name) const = 0;
+  /// \note This is O(1) complexity.
+  [[nodiscard]] virtual Result<std::optional<SchemaFieldConstRef>> GetFieldByName(
+      std::string_view name, bool case_sensitive) const = 0;
+  /// \brief Get a field by name (case-sensitive).
+  [[nodiscard]] Result<std::optional<SchemaFieldConstRef>> 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<const SchemaField> fields() const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldById(
+  Result<std::optional<SchemaFieldConstRef>> GetFieldById(
       int32_t field_id) const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldByIndex(
+  Result<std::optional<SchemaFieldConstRef>> GetFieldByIndex(
       int32_t index) const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldByName(
-      std::string_view name) const override;
+  Result<std::optional<SchemaFieldConstRef>> 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<SchemaField> fields_;
-  std::unordered_map<int32_t, size_t> field_id_to_index_;
+  mutable std::unordered_map<int32_t, SchemaFieldConstRef> field_by_id_;
+  mutable std::unordered_map<std::string_view, SchemaFieldConstRef> field_by_name_;
+  mutable std::unordered_map<std::string, SchemaFieldConstRef> 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<const SchemaField> fields() const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldById(
+  Result<std::optional<SchemaFieldConstRef>> GetFieldById(
       int32_t field_id) const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldByIndex(
+  Result<std::optional<SchemaFieldConstRef>> GetFieldByIndex(
       int32_t index) const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldByName(
-      std::string_view name) const override;
+  Result<std::optional<SchemaFieldConstRef>> 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<const SchemaField> fields() const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldById(
+  Result<std::optional<SchemaFieldConstRef>> GetFieldById(
       int32_t field_id) const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldByIndex(
+  Result<std::optional<SchemaFieldConstRef>> GetFieldByIndex(
       int32_t index) const override;
-  std::optional<std::reference_wrapper<const SchemaField>> GetFieldByName(
-      std::string_view name) const override;
+  Result<std::optional<SchemaFieldConstRef>> 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 3519c9a..f11a680 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<Error>(result_name.error()); \
-    }                                                     \
-  } while (false);
+#define ICEBERG_RETURN_UNEXPECTED(result)                       \
+  if (auto&& result_name = result; !result_name) [[unlikely]] { \
+    return std::unexpected<Error>(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 d282cea..272c6e7 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<std::runtime_error>(
-          ::testing::HasSubstr("duplicate field ID 5")));
 }
 
 TEST(SchemaTest, Equality) {
diff --git a/test/type_test.cc b/test/type_test.cc
index fca8869..9963ab3 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<const iceberg::SchemaField> 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<iceberg::IcebergError>(
-          ::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)"));
 }