You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by xu...@apache.org on 2025/05/13 05:22:29 UTC

(iceberg-cpp) branch main updated: feat: add visit type support (#94)

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

xuanwo 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 a994322  feat: add visit type support (#94)
a994322 is described below

commit a994322bb8b4204d38a566f02a5bf7851d67463d
Author: Junwang Zhao <zh...@gmail.com>
AuthorDate: Tue May 13 13:22:25 2025 +0800

    feat: add visit type support (#94)
    
    Add a `TypeId` field to each concrete type to enable macro-based type
    visitor support.
    
    ---------
    
    Signed-off-by: Junwang Zhao <zh...@gmail.com>
---
 LICENSE                             |  14 ++
 NOTICE                              |   4 +
 src/iceberg/CMakeLists.txt          |   3 +-
 src/iceberg/type.cc                 |  82 +++++------
 src/iceberg/type.h                  |  30 +++++
 src/iceberg/util/checked_cast.h     |  66 +++++++++
 src/iceberg/util/unreachable.cc     |  31 +++++
 src/iceberg/util/unreachable.h      |  33 +++++
 src/iceberg/util/visit_type.h       | 126 +++++++++++++++++
 src/iceberg/util/visitor_generate.h |  43 ++++++
 test/CMakeLists.txt                 |   3 +-
 test/type_test.cc                   |   8 +-
 test/visit_type_test.cc             | 261 ++++++++++++++++++++++++++++++++++++
 13 files changed, 647 insertions(+), 57 deletions(-)

diff --git a/LICENSE b/LICENSE
index ce4ab84..c1b9df2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -267,3 +267,17 @@ https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp
 
 MurmurHash3 was written by Austin Appleby, and is placed in the public
 domain. The author disclaims copyright to this source code.
+
+--------------------------------------------------------------------------------
+
+The file src/iceberg/util/checked_cast.h contains code adapted from
+
+https://github.com/apache/arrow/blob/main/cpp/src/arrow/util/checked_cast.h
+
+The file src/iceberg/util/visit_type.h contains code adapted from
+
+https://github.com/apache/arrow/blob/main/cpp/src/arrow/visit_type_inline.h
+
+Copyright: 2016-2025 The Apache Software Foundation.
+Home page: https://arrow.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/NOTICE b/NOTICE
index cad0c1a..b287ab0 100644
--- a/NOTICE
+++ b/NOTICE
@@ -12,3 +12,7 @@ This product includes code from smhasher
  * MurmurHash3 was written by Austin Appleby, and is placed in the public
  * domain. The author hereby disclaims copyright to this source code.
  * https://github.com/aappleby/smhasher
+
+This product includes code from Apache Arrow
+ * Copyright 2016-2025 The Apache Software Foundation
+ * https://github.com/apache/arrow
diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt
index c9fe783..2ce3012 100644
--- a/src/iceberg/CMakeLists.txt
+++ b/src/iceberg/CMakeLists.txt
@@ -38,7 +38,8 @@ set(ICEBERG_SOURCES
     transform_function.cc
     type.cc
     util/murmurhash3_internal.cc
-    util/timepoint.cc)
+    util/timepoint.cc
+    util/unreachable.cc)
 
 set(ICEBERG_STATIC_BUILD_INTERFACE_LIBS)
 set(ICEBERG_SHARED_BUILD_INTERFACE_LIBS)
diff --git a/src/iceberg/type.cc b/src/iceberg/type.cc
index e594a12..e58cd0e 100644
--- a/src/iceberg/type.cc
+++ b/src/iceberg/type.cc
@@ -41,7 +41,7 @@ StructType::StructType(std::vector<SchemaField> fields) : fields_(std::move(fiel
   }
 }
 
-TypeId StructType::type_id() const { return TypeId::kStruct; }
+TypeId StructType::type_id() const { return kTypeId; }
 std::string StructType::ToString() const {
   std::string repr = "struct<\n";
   for (const auto& field : fields_) {
@@ -93,7 +93,7 @@ ListType::ListType(SchemaField element) : element_(std::move(element)) {
 ListType::ListType(int32_t field_id, std::shared_ptr<Type> type, bool optional)
     : element_(field_id, std::string(kElementName), std::move(type), optional) {}
 
-TypeId ListType::type_id() const { return TypeId::kList; }
+TypeId ListType::type_id() const { return kTypeId; }
 std::string ListType::ToString() const {
   // XXX: work around Clang/libc++: "<{}>" in a format string appears to get
   // parsed as {<>} or something; split up the format string to avoid that
@@ -146,7 +146,7 @@ MapType::MapType(SchemaField key, SchemaField value)
 
 const SchemaField& MapType::key() const { return fields_[0]; }
 const SchemaField& MapType::value() const { return fields_[1]; }
-TypeId MapType::type_id() const { return TypeId::kMap; }
+TypeId MapType::type_id() const { return kTypeId; }
 std::string MapType::ToString() const {
   // XXX: work around Clang/libc++: "<{}>" in a format string appears to get
   // parsed as {<>} or something; split up the format string to avoid that
@@ -192,33 +192,25 @@ bool MapType::Equals(const Type& other) const {
   return fields_ == map.fields_;
 }
 
-TypeId BooleanType::type_id() const { return TypeId::kBoolean; }
+TypeId BooleanType::type_id() const { return kTypeId; }
 std::string BooleanType::ToString() const { return "boolean"; }
-bool BooleanType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kBoolean;
-}
+bool BooleanType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
-TypeId IntType::type_id() const { return TypeId::kInt; }
+TypeId IntType::type_id() const { return kTypeId; }
 std::string IntType::ToString() const { return "int"; }
-bool IntType::Equals(const Type& other) const { return other.type_id() == TypeId::kInt; }
+bool IntType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
-TypeId LongType::type_id() const { return TypeId::kLong; }
+TypeId LongType::type_id() const { return kTypeId; }
 std::string LongType::ToString() const { return "long"; }
-bool LongType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kLong;
-}
+bool LongType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
-TypeId FloatType::type_id() const { return TypeId::kFloat; }
+TypeId FloatType::type_id() const { return kTypeId; }
 std::string FloatType::ToString() const { return "float"; }
-bool FloatType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kFloat;
-}
+bool FloatType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
-TypeId DoubleType::type_id() const { return TypeId::kDouble; }
+TypeId DoubleType::type_id() const { return kTypeId; }
 std::string DoubleType::ToString() const { return "double"; }
-bool DoubleType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kDouble;
-}
+bool DoubleType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
 DecimalType::DecimalType(int32_t precision, int32_t scale)
     : precision_(precision), scale_(scale) {
@@ -230,57 +222,47 @@ DecimalType::DecimalType(int32_t precision, int32_t scale)
 
 int32_t DecimalType::precision() const { return precision_; }
 int32_t DecimalType::scale() const { return scale_; }
-TypeId DecimalType::type_id() const { return TypeId::kDecimal; }
+TypeId DecimalType::type_id() const { return kTypeId; }
 std::string DecimalType::ToString() const {
   return std::format("decimal({}, {})", precision_, scale_);
 }
 bool DecimalType::Equals(const Type& other) const {
-  if (other.type_id() != TypeId::kDecimal) {
+  if (other.type_id() != kTypeId) {
     return false;
   }
   const auto& decimal = static_cast<const DecimalType&>(other);
   return precision_ == decimal.precision_ && scale_ == decimal.scale_;
 }
 
-TypeId DateType::type_id() const { return TypeId::kDate; }
+TypeId DateType::type_id() const { return kTypeId; }
 std::string DateType::ToString() const { return "date"; }
-bool DateType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kDate;
-}
+bool DateType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
-TypeId TimeType::type_id() const { return TypeId::kTime; }
+TypeId TimeType::type_id() const { return kTypeId; }
 std::string TimeType::ToString() const { return "time"; }
-bool TimeType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kTime;
-}
+bool TimeType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
 bool TimestampType::is_zoned() const { return false; }
 TimeUnit TimestampType::time_unit() const { return TimeUnit::kMicrosecond; }
-TypeId TimestampType::type_id() const { return TypeId::kTimestamp; }
+TypeId TimestampType::type_id() const { return kTypeId; }
 std::string TimestampType::ToString() const { return "timestamp"; }
-bool TimestampType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kTimestamp;
-}
+bool TimestampType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
 bool TimestampTzType::is_zoned() const { return true; }
 TimeUnit TimestampTzType::time_unit() const { return TimeUnit::kMicrosecond; }
-TypeId TimestampTzType::type_id() const { return TypeId::kTimestampTz; }
+TypeId TimestampTzType::type_id() const { return kTypeId; }
 std::string TimestampTzType::ToString() const { return "timestamptz"; }
 bool TimestampTzType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kTimestampTz;
+  return other.type_id() == kTypeId;
 }
 
-TypeId StringType::type_id() const { return TypeId::kString; }
+TypeId StringType::type_id() const { return kTypeId; }
 std::string StringType::ToString() const { return "string"; }
-bool StringType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kString;
-}
+bool StringType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
-TypeId UuidType::type_id() const { return TypeId::kUuid; }
+TypeId UuidType::type_id() const { return kTypeId; }
 std::string UuidType::ToString() const { return "uuid"; }
-bool UuidType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kUuid;
-}
+bool UuidType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
 FixedType::FixedType(int32_t length) : length_(length) {
   if (length < 0) {
@@ -289,20 +271,18 @@ FixedType::FixedType(int32_t length) : length_(length) {
 }
 
 int32_t FixedType::length() const { return length_; }
-TypeId FixedType::type_id() const { return TypeId::kFixed; }
+TypeId FixedType::type_id() const { return kTypeId; }
 std::string FixedType::ToString() const { return std::format("fixed({})", length_); }
 bool FixedType::Equals(const Type& other) const {
-  if (other.type_id() != TypeId::kFixed) {
+  if (other.type_id() != kTypeId) {
     return false;
   }
   const auto& fixed = static_cast<const FixedType&>(other);
   return length_ == fixed.length_;
 }
 
-TypeId BinaryType::type_id() const { return TypeId::kBinary; }
+TypeId BinaryType::type_id() const { return kTypeId; }
 std::string BinaryType::ToString() const { return "binary"; }
-bool BinaryType::Equals(const Type& other) const {
-  return other.type_id() == TypeId::kBinary;
-}
+bool BinaryType::Equals(const Type& other) const { return other.type_id() == kTypeId; }
 
 }  // namespace iceberg
diff --git a/src/iceberg/type.h b/src/iceberg/type.h
index 5405106..9bb8623 100644
--- a/src/iceberg/type.h
+++ b/src/iceberg/type.h
@@ -104,6 +104,7 @@ class ICEBERG_EXPORT NestedType : public Type {
 /// \brief A data type representing a struct with nested fields.
 class ICEBERG_EXPORT StructType : public NestedType {
  public:
+  constexpr static TypeId kTypeId = TypeId::kStruct;
   explicit StructType(std::vector<SchemaField> fields);
   ~StructType() override = default;
 
@@ -128,6 +129,7 @@ class ICEBERG_EXPORT StructType : public NestedType {
 /// \brief A data type representing a list of values.
 class ICEBERG_EXPORT ListType : public NestedType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kList;
   constexpr static const std::string_view kElementName = "element";
 
   /// \brief Construct a list of the given element.  The name of the child
@@ -157,6 +159,7 @@ class ICEBERG_EXPORT ListType : public NestedType {
 /// \brief A data type representing a dictionary of values.
 class ICEBERG_EXPORT MapType : public NestedType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kMap;
   constexpr static const std::string_view kKeyName = "key";
   constexpr static const std::string_view kValueName = "value";
 
@@ -194,6 +197,8 @@ class ICEBERG_EXPORT MapType : public NestedType {
 /// \brief A data type representing a boolean (true or false).
 class ICEBERG_EXPORT BooleanType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kBoolean;
+
   BooleanType() = default;
   ~BooleanType() override = default;
 
@@ -207,6 +212,8 @@ class ICEBERG_EXPORT BooleanType : public PrimitiveType {
 /// \brief A data type representing a 32-bit signed integer.
 class ICEBERG_EXPORT IntType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kInt;
+
   IntType() = default;
   ~IntType() override = default;
 
@@ -220,6 +227,8 @@ class ICEBERG_EXPORT IntType : public PrimitiveType {
 /// \brief A data type representing a 64-bit signed integer.
 class ICEBERG_EXPORT LongType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kLong;
+
   LongType() = default;
   ~LongType() override = default;
 
@@ -234,6 +243,8 @@ class ICEBERG_EXPORT LongType : public PrimitiveType {
 ///   float.
 class ICEBERG_EXPORT FloatType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kFloat;
+
   FloatType() = default;
   ~FloatType() override = default;
 
@@ -248,6 +259,8 @@ class ICEBERG_EXPORT FloatType : public PrimitiveType {
 ///   float.
 class ICEBERG_EXPORT DoubleType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kDouble;
+
   DoubleType() = default;
   ~DoubleType() override = default;
 
@@ -261,6 +274,7 @@ class ICEBERG_EXPORT DoubleType : public PrimitiveType {
 /// \brief A data type representing a fixed-precision decimal.
 class ICEBERG_EXPORT DecimalType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kDecimal;
   constexpr static const int32_t kMaxPrecision = 38;
 
   /// \brief Construct a decimal type with the given precision and scale.
@@ -288,6 +302,8 @@ class ICEBERG_EXPORT DecimalType : public PrimitiveType {
 ///   timezone or time.
 class ICEBERG_EXPORT DateType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kDate;
+
   DateType() = default;
   ~DateType() override = default;
 
@@ -302,6 +318,8 @@ class ICEBERG_EXPORT DateType : public PrimitiveType {
 ///   reference to a timezone or date.
 class ICEBERG_EXPORT TimeType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kTime;
+
   TimeType() = default;
   ~TimeType() override = default;
 
@@ -326,6 +344,8 @@ class ICEBERG_EXPORT TimestampBase : public PrimitiveType {
 ///   reference to a timezone.
 class ICEBERG_EXPORT TimestampType : public TimestampBase {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kTimestamp;
+
   TimestampType() = default;
   ~TimestampType() override = default;
 
@@ -343,6 +363,8 @@ class ICEBERG_EXPORT TimestampType : public TimestampBase {
 ///   epoch in UTC.  A time zone or offset is not stored.
 class ICEBERG_EXPORT TimestampTzType : public TimestampBase {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kTimestampTz;
+
   TimestampTzType() = default;
   ~TimestampTzType() override = default;
 
@@ -359,6 +381,8 @@ class ICEBERG_EXPORT TimestampTzType : public TimestampBase {
 /// \brief A data type representing an arbitrary-length byte sequence.
 class ICEBERG_EXPORT BinaryType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kBinary;
+
   BinaryType() = default;
   ~BinaryType() override = default;
 
@@ -373,6 +397,8 @@ class ICEBERG_EXPORT BinaryType : public PrimitiveType {
 ///   (encoded in UTF-8).
 class ICEBERG_EXPORT StringType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kString;
+
   StringType() = default;
   ~StringType() override = default;
 
@@ -386,6 +412,8 @@ class ICEBERG_EXPORT StringType : public PrimitiveType {
 /// \brief A data type representing a fixed-length bytestring.
 class ICEBERG_EXPORT FixedType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kFixed;
+
   /// \brief Construct a fixed type with the given length.
   explicit FixedType(int32_t length);
   ~FixedType() override = default;
@@ -407,6 +435,8 @@ class ICEBERG_EXPORT FixedType : public PrimitiveType {
 ///   it is effectively a fixed(16).
 class ICEBERG_EXPORT UuidType : public PrimitiveType {
  public:
+  constexpr static const TypeId kTypeId = TypeId::kUuid;
+
   UuidType() = default;
   ~UuidType() override = default;
 
diff --git a/src/iceberg/util/checked_cast.h b/src/iceberg/util/checked_cast.h
new file mode 100644
index 0000000..0b966b8
--- /dev/null
+++ b/src/iceberg/util/checked_cast.h
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+/// \file iceberg/util/checked_cast.h
+/// \brief Checked cast functions for dynamic_cast and static_cast.
+/// Adapted from Apache Arrow
+/// https://github.com/apache/arrow/blob/main/cpp/src/arrow/util/checked_cast.h
+
+#include <memory>
+#include <type_traits>
+#include <utility>
+
+namespace iceberg::internal {
+
+template <typename OutputType, typename InputType>
+inline OutputType checked_cast(InputType&& value) {
+  static_assert(
+      std::is_class_v<std::remove_pointer_t<std::remove_reference_t<InputType>>>,
+      "checked_cast input type must be a class");
+  static_assert(
+      std::is_class_v<std::remove_pointer_t<std::remove_reference_t<OutputType>>>,
+      "checked_cast output type must be a class");
+#ifdef NDEBUG
+  return static_cast<OutputType>(value);
+#else
+  return dynamic_cast<OutputType>(value);
+#endif
+}
+
+template <class T, class U>
+std::shared_ptr<T> checked_pointer_cast(std::shared_ptr<U> r) noexcept {
+#ifdef NDEBUG
+  return std::static_pointer_cast<T>(std::move(r));
+#else
+  return std::dynamic_pointer_cast<T>(std::move(r));
+#endif
+}
+
+template <class T, class U>
+std::unique_ptr<T> checked_pointer_cast(std::unique_ptr<U> r) noexcept {
+#ifdef NDEBUG
+  return std::unique_ptr<T>(static_cast<T*>(r.release()));
+#else
+  return std::unique_ptr<T>(dynamic_cast<T*>(r.release()));
+#endif
+}
+
+}  // namespace iceberg::internal
diff --git a/src/iceberg/util/unreachable.cc b/src/iceberg/util/unreachable.cc
new file mode 100644
index 0000000..e86a0f2
--- /dev/null
+++ b/src/iceberg/util/unreachable.cc
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "iceberg/util/unreachable.h"
+
+#include <cstdlib>
+
+namespace iceberg::internal {
+
+// TODO(anyone): log the message before aborting
+[[noreturn]] void Unreachable([[maybe_unused]] const char* message) { std::abort(); }
+
+[[noreturn]] void Unreachable([[maybe_unused]] std::string_view message) { std::abort(); }
+
+}  // namespace iceberg::internal
diff --git a/src/iceberg/util/unreachable.h b/src/iceberg/util/unreachable.h
new file mode 100644
index 0000000..03c4b9d
--- /dev/null
+++ b/src/iceberg/util/unreachable.h
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <string_view>
+
+#include "iceberg/iceberg_export.h"
+
+namespace iceberg::internal {
+
+[[noreturn]] ICEBERG_EXPORT void Unreachable(
+    [[maybe_unused]] const char* message = "Unreachable");
+
+[[noreturn]] ICEBERG_EXPORT void Unreachable([[maybe_unused]] std::string_view message);
+
+}  // namespace iceberg::internal
diff --git a/src/iceberg/util/visit_type.h b/src/iceberg/util/visit_type.h
new file mode 100644
index 0000000..404a0a6
--- /dev/null
+++ b/src/iceberg/util/visit_type.h
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+/// \file iceberg/util/visit_type.h
+/// \brief Visitor pattern for Iceberg types
+/// Adapted from Apache Arrow
+/// https://github.com/apache/arrow/blob/main/cpp/src/arrow/visit_type_inline.h
+
+#include "iceberg/result.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/unreachable.h"
+#include "iceberg/util/visitor_generate.h"
+
+namespace iceberg {
+
+#define TYPE_VISIT_INLINE(TYPE_CLASS)                                   \
+  case TYPE_CLASS##Type::kTypeId:                                       \
+    return visitor->Visit(                                              \
+        iceberg::internal::checked_cast<const TYPE_CLASS##Type&>(type), \
+        std::forward<ARGS>(args)...);
+
+/// \brief Calls `visitor` with the corresponding concrete type class
+///
+/// \tparam VISITOR Visitor type that implements Visit() for all Iceberg types.
+/// \tparam ARGS Additional arguments, if any, will be passed to the Visit function after
+/// the `type` argument
+/// \return Status
+///
+/// A visitor is a type that implements specialized logic for each Iceberg type.
+/// Example usage:
+///
+/// ```
+/// class ExampleVisitor {
+///   Status Visit(const IntType& type) { ... }
+///   Status Visit(const LongType& type) { ... }
+///   ...
+/// }
+/// ExampleVisitor visitor;
+/// VisitTypeInline(some_type, &visitor);
+/// ```
+
+template <typename VISITOR, typename... ARGS>
+inline Status VisitTypeInline(const Type& type, VISITOR* visitor, ARGS&&... args) {
+  switch (type.type_id()) {
+    ICEBERG_GENERATE_FOR_ALL_TYPES(TYPE_VISIT_INLINE);
+    default:
+      break;
+  }
+  return NotImplemented("Type not implemented");
+}
+
+#undef TYPE_VISIT_INLINE
+
+#define TYPE_VISIT_INLINE(TYPE_CLASS)                          \
+  case TYPE_CLASS##Type::kTypeId:                              \
+    return std::forward<VISITOR>(visitor)(                     \
+        internal::checked_cast<const TYPE_CLASS##Type&>(type), \
+        std::forward<ARGS>(args)...);
+
+/// \brief Call `visitor` with the corresponding concrete type class
+/// \tparam ARGS Additional arguments, if any, will be passed to the Visit function after
+/// the `type` argument
+///
+/// Unlike VisitTypeInline which calls `visitor.Visit`, here `visitor`
+/// itself is called.
+/// `visitor` must support a `const Type&` argument as a fallback,
+/// in addition to concrete type classes.
+///
+/// The intent is for this to be called on a generic lambda
+/// that may internally use `if constexpr` or similar constructs.
+template <typename VISITOR, typename... ARGS>
+inline auto VisitType(const Type& type, VISITOR&& visitor, ARGS&&... args)
+    -> decltype(std::forward<VISITOR>(visitor)(type, args...)) {
+  switch (type.type_id()) {
+    ICEBERG_GENERATE_FOR_ALL_TYPES(TYPE_VISIT_INLINE);
+    default:
+      internal::Unreachable("Type not implemented");
+  }
+}
+
+#undef TYPE_VISIT_INLINE
+
+#define TYPE_ID_VISIT_INLINE(TYPE_CLASS)                              \
+  case TYPE_CLASS##Type::kTypeId: {                                   \
+    const TYPE_CLASS##Type* concrete_ptr = nullptr;                   \
+    return visitor->Visit(concrete_ptr, std::forward<ARGS>(args)...); \
+  }
+
+/// \brief Calls `visitor` with a nullptr of the corresponding concrete type class
+///
+/// \tparam VISITOR Visitor type that implements Visit() for all Iceberg types.
+/// \tparam ARGS Additional arguments, if any, will be passed to the Visit function after
+/// the `type` argument
+/// \return Status
+template <typename VISITOR, typename... ARGS>
+inline Status VisitTypeIdInline(TypeId id, VISITOR* visitor, ARGS&&... args) {
+  switch (id) {
+    ICEBERG_GENERATE_FOR_ALL_TYPES(TYPE_ID_VISIT_INLINE);
+    default:
+      break;
+  }
+  return NotImplemented("Type not implemented");
+}
+
+#undef TYPE_ID_VISIT_INLINE
+
+}  // namespace iceberg
diff --git a/src/iceberg/util/visitor_generate.h b/src/iceberg/util/visitor_generate.h
new file mode 100644
index 0000000..2ea8282
--- /dev/null
+++ b/src/iceberg/util/visitor_generate.h
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+namespace iceberg {
+
+#define ICEBERG_GENERATE_FOR_ALL_TYPES(ACTION) \
+  ACTION(Boolean);                             \
+  ACTION(Int);                                 \
+  ACTION(Long);                                \
+  ACTION(Float);                               \
+  ACTION(Double);                              \
+  ACTION(Decimal);                             \
+  ACTION(Date);                                \
+  ACTION(Time);                                \
+  ACTION(Timestamp);                           \
+  ACTION(TimestampTz);                         \
+  ACTION(String);                              \
+  ACTION(Uuid);                                \
+  ACTION(Fixed);                               \
+  ACTION(Binary);                              \
+  ACTION(Struct);                              \
+  ACTION(List);                                \
+  ACTION(Map);
+
+}  // namespace iceberg
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 652d40c..31569a7 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -58,7 +58,8 @@ target_link_libraries(json_serde_test PRIVATE iceberg_static GTest::gtest_main
 add_test(NAME json_serde_test COMMAND json_serde_test)
 
 add_executable(util_test)
-target_sources(util_test PRIVATE expected_test.cc formatter_test.cc config_test.cc)
+target_sources(util_test PRIVATE expected_test.cc formatter_test.cc config_test.cc
+                                 visit_type_test.cc)
 target_link_libraries(util_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock)
 add_test(NAME util_test COMMAND util_test)
 
diff --git a/test/type_test.cc b/test/type_test.cc
index 607ddd5..c371753 100644
--- a/test/type_test.cc
+++ b/test/type_test.cc
@@ -88,7 +88,7 @@ TEST_P(TypeTest, StdFormat) {
   ASSERT_EQ(test_case.repr, std::format("{}", *test_case.type));
 }
 
-const static TypeTestCase kPrimitiveTypes[] = {
+const static std::array<TypeTestCase, 16> kPrimitiveTypes = {{
     {
         .name = "boolean",
         .type = std::make_shared<iceberg::BooleanType>(),
@@ -201,9 +201,9 @@ const static TypeTestCase kPrimitiveTypes[] = {
         .primitive = true,
         .repr = "uuid",
     },
-};
+}};
 
-const static TypeTestCase kNestedTypes[] = {
+const static std::array<TypeTestCase, 4> kNestedTypes = {{
     {
         .name = "list_int",
         .type = std::make_shared<iceberg::ListType>(
@@ -249,7 +249,7 @@ const static TypeTestCase kNestedTypes[] = {
   bar (2): string (optional)
 >)",
     },
-};
+}};
 
 INSTANTIATE_TEST_SUITE_P(Primitive, TypeTest, ::testing::ValuesIn(kPrimitiveTypes),
                          TypeTestCaseToString);
diff --git a/test/visit_type_test.cc b/test/visit_type_test.cc
new file mode 100644
index 0000000..2215ae5
--- /dev/null
+++ b/test/visit_type_test.cc
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "iceberg/util/visit_type.h"
+
+#include <sstream>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <iceberg/type.h>
+
+#include "gmock/gmock.h"
+#include "iceberg/result.h"
+#include "matchers.h"
+
+namespace iceberg {
+
+namespace {
+
+class TypeNameVisitor {
+ public:
+  Status Visit(const Type& type, std::ostringstream& oss) {
+    oss << type.ToString();
+    return {};
+  }
+};
+
+struct TypeTestCase {
+  /// Test case name, must be safe for Googletest (alphanumeric + underscore)
+  std::string name;
+  std::shared_ptr<iceberg::Type> type;
+  iceberg::TypeId type_id;
+  bool primitive;
+  std::string repr;
+};
+
+std::string TypeTestCaseToString(const ::testing::TestParamInfo<TypeTestCase>& info) {
+  return info.param.name;
+}
+
+const static std::array<TypeTestCase, 16> kPrimitiveTypes = {{
+    {
+        .name = "boolean",
+        .type = std::make_shared<iceberg::BooleanType>(),
+        .type_id = iceberg::TypeId::kBoolean,
+        .primitive = true,
+        .repr = "boolean",
+    },
+    {
+        .name = "int",
+        .type = std::make_shared<iceberg::IntType>(),
+        .type_id = iceberg::TypeId::kInt,
+        .primitive = true,
+        .repr = "int",
+    },
+    {
+        .name = "long",
+        .type = std::make_shared<iceberg::LongType>(),
+        .type_id = iceberg::TypeId::kLong,
+        .primitive = true,
+        .repr = "long",
+    },
+    {
+        .name = "float",
+        .type = std::make_shared<iceberg::FloatType>(),
+        .type_id = iceberg::TypeId::kFloat,
+        .primitive = true,
+        .repr = "float",
+    },
+    {
+        .name = "double",
+        .type = std::make_shared<iceberg::DoubleType>(),
+        .type_id = iceberg::TypeId::kDouble,
+        .primitive = true,
+        .repr = "double",
+    },
+    {
+        .name = "decimal9_2",
+        .type = std::make_shared<iceberg::DecimalType>(9, 2),
+        .type_id = iceberg::TypeId::kDecimal,
+        .primitive = true,
+        .repr = "decimal(9, 2)",
+    },
+    {
+        .name = "decimal38_10",
+        .type = std::make_shared<iceberg::DecimalType>(38, 10),
+        .type_id = iceberg::TypeId::kDecimal,
+        .primitive = true,
+        .repr = "decimal(38, 10)",
+    },
+    {
+        .name = "date",
+        .type = std::make_shared<iceberg::DateType>(),
+        .type_id = iceberg::TypeId::kDate,
+        .primitive = true,
+        .repr = "date",
+    },
+    {
+        .name = "time",
+        .type = std::make_shared<iceberg::TimeType>(),
+        .type_id = iceberg::TypeId::kTime,
+        .primitive = true,
+        .repr = "time",
+    },
+    {
+        .name = "timestamp",
+        .type = std::make_shared<iceberg::TimestampType>(),
+        .type_id = iceberg::TypeId::kTimestamp,
+        .primitive = true,
+        .repr = "timestamp",
+    },
+    {
+        .name = "timestamptz",
+        .type = std::make_shared<iceberg::TimestampTzType>(),
+        .type_id = iceberg::TypeId::kTimestampTz,
+        .primitive = true,
+        .repr = "timestamptz",
+    },
+    {
+        .name = "binary",
+        .type = std::make_shared<iceberg::BinaryType>(),
+        .type_id = iceberg::TypeId::kBinary,
+        .primitive = true,
+        .repr = "binary",
+    },
+    {
+        .name = "string",
+        .type = std::make_shared<iceberg::StringType>(),
+        .type_id = iceberg::TypeId::kString,
+        .primitive = true,
+        .repr = "string",
+    },
+    {
+        .name = "fixed10",
+        .type = std::make_shared<iceberg::FixedType>(10),
+        .type_id = iceberg::TypeId::kFixed,
+        .primitive = true,
+        .repr = "fixed(10)",
+    },
+    {
+        .name = "fixed255",
+        .type = std::make_shared<iceberg::FixedType>(255),
+        .type_id = iceberg::TypeId::kFixed,
+        .primitive = true,
+        .repr = "fixed(255)",
+    },
+    {
+        .name = "uuid",
+        .type = std::make_shared<iceberg::UuidType>(),
+        .type_id = iceberg::TypeId::kUuid,
+        .primitive = true,
+        .repr = "uuid",
+    },
+}};
+
+const static std::array<TypeTestCase, 4> kNestedTypes = {{
+    {
+        .name = "list_int",
+        .type = std::make_shared<iceberg::ListType>(
+            1, std::make_shared<iceberg::IntType>(), true),
+        .type_id = iceberg::TypeId::kList,
+        .primitive = false,
+        .repr = "list<element (1): int (optional)>",
+    },
+    {
+        .name = "list_list_int",
+        .type = std::make_shared<iceberg::ListType>(
+            1,
+            std::make_shared<iceberg::ListType>(2, std::make_shared<iceberg::IntType>(),
+                                                true),
+            false),
+        .type_id = iceberg::TypeId::kList,
+        .primitive = false,
+        .repr = "list<element (1): list<element (2): int (optional)> (required)>",
+    },
+    {
+        .name = "map_int_string",
+        .type = std::make_shared<iceberg::MapType>(
+            iceberg::SchemaField::MakeRequired(1, "key",
+                                               std::make_shared<iceberg::LongType>()),
+            iceberg::SchemaField::MakeRequired(2, "value",
+                                               std::make_shared<iceberg::StringType>())),
+        .type_id = iceberg::TypeId::kMap,
+        .primitive = false,
+        .repr = "map<key (1): long (required): value (2): string (required)>",
+    },
+    {
+        .name = "struct",
+        .type = std::make_shared<iceberg::StructType>(std::vector<iceberg::SchemaField>{
+            iceberg::SchemaField::MakeRequired(1, "foo",
+                                               std::make_shared<iceberg::LongType>()),
+            iceberg::SchemaField::MakeOptional(2, "bar",
+                                               std::make_shared<iceberg::StringType>()),
+        }),
+        .type_id = iceberg::TypeId::kStruct,
+        .primitive = false,
+        .repr = R"(struct<
+  foo (1): long (required)
+  bar (2): string (optional)
+>)",
+    },
+}};
+
+}  // namespace
+
+class TypeTest : public ::testing::TestWithParam<TypeTestCase> {};
+
+INSTANTIATE_TEST_SUITE_P(Primitive, TypeTest, ::testing::ValuesIn(kPrimitiveTypes),
+                         TypeTestCaseToString);
+
+INSTANTIATE_TEST_SUITE_P(Nested, TypeTest, ::testing::ValuesIn(kNestedTypes),
+                         TypeTestCaseToString);
+
+TEST_P(TypeTest, VisitTypePrintToString) {
+  TypeNameVisitor visitor;
+  std::ostringstream oss;
+  const auto& test_case = GetParam();
+  ASSERT_THAT(VisitTypeInline(*test_case.type, &visitor, oss), IsOk());
+  ASSERT_EQ(oss.str(), test_case.repr);
+}
+
+TEST_P(TypeTest, VisitTypeReturnNestedTypeId) {
+  auto visitor = [&](auto&& type) -> Result<TypeId> {
+    using Type = std::decay_t<decltype(type)>;
+    // Check if the type is a nested type
+    if constexpr (std::is_base_of_v<NestedType, Type>) {
+      return type.type_id();
+    } else {
+      return NotImplemented("Type is not a nested type");
+    }
+  };
+
+  const auto& test_case = GetParam();
+  auto result = VisitType(*test_case.type, visitor);
+
+  if (test_case.primitive) {
+    ASSERT_THAT(result, IsError(ErrorKind::kNotImplemented));
+    ASSERT_THAT(result, HasErrorMessage("Type is not a nested type"));
+  } else {
+    ASSERT_THAT(result, IsOk());
+    ASSERT_EQ(result.value(), test_case.type_id);
+  }
+}
+
+}  // namespace iceberg