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)"));
}