You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ga...@apache.org on 2026/07/01 07:36:30 UTC

(iceberg-cpp) branch main updated: feat: Support UUID values across row and file IO (#790)

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

wgtmac 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 7c4f15cc feat: Support UUID values across row and file IO (#790)
7c4f15cc is described below

commit 7c4f15cc9a858c023e963224ebd05685cb6a6f49
Author: Junwang Zhao <zh...@gmail.com>
AuthorDate: Wed Jul 1 15:36:24 2026 +0800

    feat: Support UUID values across row and file IO (#790)
    
    closes #314
---
 src/iceberg/avro/avro_data_util.cc         |   8 +-
 src/iceberg/avro/avro_reader.cc            |  63 ++++++++-
 src/iceberg/parquet/parquet_schema_util.cc |   3 +-
 src/iceberg/row/struct_like.cc             |  16 ++-
 src/iceberg/schema_internal.cc             |   1 -
 src/iceberg/schema_internal.h              |   3 +
 src/iceberg/test/avro_test.cc              | 205 +++++++++++++++++++++++++++++
 src/iceberg/test/parquet_test.cc           |  36 +++++
 src/iceberg/test/struct_like_test.cc       |  71 ++++++++++
 9 files changed, 399 insertions(+), 7 deletions(-)

diff --git a/src/iceberg/avro/avro_data_util.cc b/src/iceberg/avro/avro_data_util.cc
index f2bcc29f..b44e4b11 100644
--- a/src/iceberg/avro/avro_data_util.cc
+++ b/src/iceberg/avro/avro_data_util.cc
@@ -35,6 +35,7 @@
 #include "iceberg/avro/avro_schema_util_internal.h"
 #include "iceberg/metadata_columns.h"
 #include "iceberg/schema.h"
+#include "iceberg/schema_internal.h"
 #include "iceberg/schema_util.h"
 #include "iceberg/util/checked_cast.h"
 #include "iceberg/util/macros.h"
@@ -620,7 +621,9 @@ Status ExtractDatumFromArray(const ::arrow::Array& array, int64_t index,
     }
 
     case ::arrow::Type::EXTENSION: {
-      if (array.type()->name() == "arrow.uuid") {
+      const auto& extension_type =
+          internal::checked_cast<const ::arrow::ExtensionType&>(*array.type());
+      if (extension_type.extension_name() == kArrowUuidExtensionName) {
         const auto& extension_array =
             internal::checked_cast<const ::arrow::ExtensionArray&>(array);
         const auto& fixed_array =
@@ -632,7 +635,8 @@ Status ExtractDatumFromArray(const ::arrow::Array& array, int64_t index,
         return {};
       }
 
-      return NotSupported("Unsupported Arrow extension type: {}", array.type()->name());
+      return NotSupported("Unsupported Arrow extension type: {}",
+                          extension_type.extension_name());
     }
 
     case ::arrow::Type::STRUCT: {
diff --git a/src/iceberg/avro/avro_reader.cc b/src/iceberg/avro/avro_reader.cc
index 1d431c46..95218ccb 100644
--- a/src/iceberg/avro/avro_reader.cc
+++ b/src/iceberg/avro/avro_reader.cc
@@ -20,9 +20,11 @@
 #include "iceberg/avro/avro_reader.h"
 
 #include <memory>
+#include <vector>
 
 #include <arrow/array/builder_base.h>
 #include <arrow/c/bridge.h>
+#include <arrow/extension_type.h>
 #include <arrow/filesystem/filesystem.h>
 #include <arrow/record_batch.h>
 #include <arrow/result.h>
@@ -42,6 +44,7 @@
 #include "iceberg/metadata_columns.h"
 #include "iceberg/name_mapping.h"
 #include "iceberg/schema_internal.h"
+#include "iceberg/util/checked_cast.h"
 #include "iceberg/util/macros.h"
 
 namespace iceberg::avro {
@@ -209,6 +212,63 @@ struct ReadContext {
   std::shared_ptr<::arrow::ArrayBuilder> builder_;
 };
 
+std::shared_ptr<::arrow::DataType> StorageTypeForBuilder(
+    const std::shared_ptr<::arrow::DataType>& type);
+
+std::shared_ptr<::arrow::Field> StorageFieldForBuilder(
+    const std::shared_ptr<::arrow::Field>& field) {
+  return ::arrow::field(field->name(), StorageTypeForBuilder(field->type()),
+                        field->nullable(), field->metadata());
+}
+
+// Arrow cannot construct builders for arrow.uuid extension arrays yet, so build
+// with the UUID storage type while keeping the public schema unchanged.
+std::shared_ptr<::arrow::DataType> StorageTypeForBuilder(
+    const std::shared_ptr<::arrow::DataType>& type) {
+  switch (type->id()) {
+    case ::arrow::Type::EXTENSION: {
+      const auto& extension_type =
+          internal::checked_cast<const ::arrow::ExtensionType&>(*type);
+      if (extension_type.extension_name() == kArrowUuidExtensionName) {
+        return extension_type.storage_type();
+      }
+      return type;
+    }
+    case ::arrow::Type::STRUCT: {
+      const auto& struct_type = internal::checked_cast<const ::arrow::StructType&>(*type);
+      std::vector<std::shared_ptr<::arrow::Field>> fields;
+      fields.reserve(struct_type.num_fields());
+      for (const auto& field : struct_type.fields()) {
+        fields.emplace_back(StorageFieldForBuilder(field));
+      }
+      return ::arrow::struct_(std::move(fields));
+    }
+    case ::arrow::Type::LIST: {
+      const auto& list_type = internal::checked_cast<const ::arrow::ListType&>(*type);
+      return ::arrow::list(StorageFieldForBuilder(list_type.value_field()));
+    }
+    case ::arrow::Type::LARGE_LIST: {
+      const auto& list_type =
+          internal::checked_cast<const ::arrow::LargeListType&>(*type);
+      return ::arrow::large_list(StorageFieldForBuilder(list_type.value_field()));
+    }
+    case ::arrow::Type::FIXED_SIZE_LIST: {
+      const auto& list_type =
+          internal::checked_cast<const ::arrow::FixedSizeListType&>(*type);
+      return ::arrow::fixed_size_list(StorageFieldForBuilder(list_type.value_field()),
+                                      list_type.list_size());
+    }
+    case ::arrow::Type::MAP: {
+      const auto& map_type = internal::checked_cast<const ::arrow::MapType&>(*type);
+      return ::arrow::map(StorageTypeForBuilder(map_type.key_type()),
+                          StorageFieldForBuilder(map_type.item_field()),
+                          map_type.keys_sorted());
+    }
+    default:
+      return type;
+  }
+}
+
 }  // namespace
 
 // TODO(gang.wu): collect basic reader metrics
@@ -349,7 +409,8 @@ class AvroReader::Impl {
     context_->arrow_schema_ = import_result.MoveValueUnsafe();
 
     auto arrow_struct_type =
-        std::make_shared<::arrow::StructType>(context_->arrow_schema_->fields());
+        internal::checked_pointer_cast<::arrow::StructType>(StorageTypeForBuilder(
+            std::make_shared<::arrow::StructType>(context_->arrow_schema_->fields())));
     auto builder_result = ::arrow::MakeBuilder(arrow_struct_type);
     if (!builder_result.ok()) {
       return InvalidSchema("Failed to make the arrow builder: {}",
diff --git a/src/iceberg/parquet/parquet_schema_util.cc b/src/iceberg/parquet/parquet_schema_util.cc
index a5629198..09b1f963 100644
--- a/src/iceberg/parquet/parquet_schema_util.cc
+++ b/src/iceberg/parquet/parquet_schema_util.cc
@@ -28,6 +28,7 @@
 #include "iceberg/metadata_columns.h"
 #include "iceberg/parquet/parquet_schema_util_internal.h"
 #include "iceberg/result.h"
+#include "iceberg/schema_internal.h"
 #include "iceberg/schema_util_internal.h"
 #include "iceberg/util/checked_cast.h"
 #include "iceberg/util/formatter.h"  // IWYU pragma: keep
@@ -222,7 +223,7 @@ Status ValidateParquetSchemaEvolution(
       if (arrow_type->id() == ::arrow::Type::EXTENSION) {
         const auto& extension_type =
             internal::checked_cast<const ::arrow::ExtensionType&>(*arrow_type);
-        if (extension_type.extension_name() == "arrow.uuid") {
+        if (extension_type.extension_name() == kArrowUuidExtensionName) {
           return {};
         }
       }
diff --git a/src/iceberg/row/struct_like.cc b/src/iceberg/row/struct_like.cc
index 355af84c..430fd22f 100644
--- a/src/iceberg/row/struct_like.cc
+++ b/src/iceberg/row/struct_like.cc
@@ -62,6 +62,12 @@ Result<Scalar> LiteralToScalar(const Literal& literal) {
       return Scalar{
           std::string_view(reinterpret_cast<const char*>(bytes.data()), bytes.size())};
     }
+    case TypeId::kUuid: {
+      const auto& uuid = std::get<Uuid>(literal.value());
+      const auto& bytes = uuid.bytes();
+      return Scalar{
+          std::string_view(reinterpret_cast<const char*>(bytes.data()), bytes.size())};
+    }
     case TypeId::kDecimal:
       return Scalar{std::get<Decimal>(literal.value())};
     default:
@@ -162,8 +168,14 @@ Result<Literal> StructLikeAccessor::GetLiteral(const StructLike& struct_like) co
       const auto& fixed_data = std::get<std::string_view>(scalar);
       return Literal::Fixed(std::vector<uint8_t>(fixed_data.cbegin(), fixed_data.cend()));
     }
-    case TypeId::kUuid:
-      // TODO(gangwu): Implement UUID type
+    case TypeId::kUuid: {
+      const auto& uuid_data = std::get<std::string_view>(scalar);
+      ICEBERG_ASSIGN_OR_RAISE(
+          auto uuid,
+          Uuid::FromBytes(std::span<const uint8_t>(
+              reinterpret_cast<const uint8_t*>(uuid_data.data()), uuid_data.size())));
+      return Literal::UUID(uuid);
+    }
     default:
       return NotSupported("Cannot convert scalar to literal of type {}",
                           type_->ToString());
diff --git a/src/iceberg/schema_internal.cc b/src/iceberg/schema_internal.cc
index 792341ad..5dac0d3b 100644
--- a/src/iceberg/schema_internal.cc
+++ b/src/iceberg/schema_internal.cc
@@ -37,7 +37,6 @@ namespace {
 // Constants for Arrow schema metadata
 constexpr const char* kArrowExtensionName = "ARROW:extension:name";
 constexpr const char* kArrowExtensionMetadata = "ARROW:extension:metadata";
-constexpr const char* kArrowUuidExtensionName = "arrow.uuid";
 constexpr int32_t kUnknownFieldId = -1;
 
 Status CheckArrowCompatible(const Type& type) {
diff --git a/src/iceberg/schema_internal.h b/src/iceberg/schema_internal.h
index 5c7209d6..fd9ec626 100644
--- a/src/iceberg/schema_internal.h
+++ b/src/iceberg/schema_internal.h
@@ -30,6 +30,9 @@
 
 namespace iceberg {
 
+/// \brief Canonical Arrow extension name used for Iceberg UUID values.
+inline constexpr const char* kArrowUuidExtensionName = "arrow.uuid";
+
 /// \brief Convert an Iceberg schema to an Arrow schema.
 ///
 /// \param[in] schema The Iceberg schema to convert.
diff --git a/src/iceberg/test/avro_test.cc b/src/iceberg/test/avro_test.cc
index ef86ef9e..3ae1696d 100644
--- a/src/iceberg/test/avro_test.cc
+++ b/src/iceberg/test/avro_test.cc
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+#include <array>
+#include <initializer_list>
 #include <optional>
 #include <sstream>
 #include <string>
@@ -25,7 +27,10 @@
 
 #include <arrow/array.h>
 #include <arrow/array/array_base.h>
+#include <arrow/array/builder_binary.h>
+#include <arrow/array/builder_primitive.h>
 #include <arrow/c/bridge.h>
+#include <arrow/extension/uuid.h>
 #include <arrow/filesystem/filesystem.h>
 #include <arrow/json/from_string.h>
 #include <avro/DataFile.hh>
@@ -46,6 +51,7 @@
 #include "iceberg/test/temp_file_test_base.h"
 #include "iceberg/type.h"
 #include "iceberg/util/checked_cast.h"
+#include "iceberg/util/uuid.h"
 
 namespace iceberg::avro {
 
@@ -76,6 +82,41 @@ std::optional<int32_t> FieldIdAt(const ::avro::NodePtr& node, size_t index) {
   return std::stoi(field_id.value());
 }
 
+constexpr std::array<uint8_t, Uuid::kLength> kUuidBytes1 = {
+    0x12, 0x3e, 0x45, 0x67, 0xe8, 0x9b, 0x12, 0xd3,
+    0xa4, 0x56, 0x42, 0x66, 0x14, 0x17, 0x40, 0x00};
+constexpr std::array<uint8_t, Uuid::kLength> kUuidBytes2 = {
+    0xf7, 0x9c, 0x3e, 0x09, 0x67, 0x7c, 0x4b, 0xbd,
+    0xa4, 0x79, 0x3f, 0x34, 0x9c, 0xb7, 0x85, 0xe7};
+
+std::shared_ptr<::arrow::Array> MakeUuidArray(
+    std::initializer_list<const std::array<uint8_t, Uuid::kLength>*> values) {
+  ::arrow::FixedSizeBinaryBuilder uuid_storage_builder(
+      ::arrow::fixed_size_binary(Uuid::kLength));
+  for (const auto* value : values) {
+    EXPECT_TRUE(uuid_storage_builder.Append(value->data()).ok());
+  }
+  auto uuid_storage = uuid_storage_builder.Finish().ValueOrDie();
+  return ::arrow::ExtensionType::WrapArray(::arrow::extension::uuid(), uuid_storage);
+}
+
+std::shared_ptr<::arrow::Array> MakeInt32Array(std::initializer_list<int32_t> values) {
+  ::arrow::Int32Builder builder;
+  for (auto value : values) {
+    EXPECT_TRUE(builder.Append(value).ok());
+  }
+  return builder.Finish().ValueOrDie();
+}
+
+std::shared_ptr<::arrow::Array> MakeStringArray(
+    std::initializer_list<std::string_view> values) {
+  ::arrow::StringBuilder builder;
+  for (auto value : values) {
+    EXPECT_TRUE(builder.Append(value).ok());
+  }
+  return builder.Finish().ValueOrDie();
+}
+
 }  // namespace
 
 class AvroReaderTest : public TempFileTestBase {
@@ -781,6 +822,41 @@ class AvroWriterTest : public ::testing::Test,
     return avro_reader.dataSchema();
   }
 
+  void WriteArrowArrayAndVerify(std::shared_ptr<Schema> schema,
+                                const std::shared_ptr<::arrow::Array>& array) {
+    ArrowArray arrow_array;
+    ASSERT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok());
+
+    WriterProperties writer_properties;
+    writer_properties.Set(WriterProperties::kAvroSkipDatum, skip_datum_);
+
+    ICEBERG_UNWRAP_OR_FAIL(writer_, WriterFactoryRegistry::Open(
+                                        FileFormatType::kAvro,
+                                        {.path = temp_avro_file_,
+                                         .schema = schema,
+                                         .io = file_io_,
+                                         .properties = std::move(writer_properties)}));
+    ASSERT_THAT(writer_->Write(&arrow_array), IsOk());
+    ASSERT_THAT(writer_->Close(), IsOk());
+
+    ICEBERG_UNWRAP_OR_FAIL(auto written_length, writer_->length());
+    ICEBERG_UNWRAP_OR_FAIL(
+        auto reader,
+        ReaderFactoryRegistry::Open(FileFormatType::kAvro, {.path = temp_avro_file_,
+                                                            .length = written_length,
+                                                            .io = file_io_,
+                                                            .projection = schema}));
+    ICEBERG_UNWRAP_OR_FAIL(auto data, reader->Next());
+    ASSERT_TRUE(data.has_value());
+
+    ICEBERG_UNWRAP_OR_FAIL(auto arrow_c_schema, reader->Schema());
+    auto read_array = ::arrow::ImportArray(&data.value(), &arrow_c_schema).ValueOrDie();
+    ASSERT_TRUE(read_array->Equals(*array)) << "actual:\n"
+                                            << read_array->ToString() << "\nexpected:\n"
+                                            << array->ToString();
+    ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader));
+  }
+
   std::shared_ptr<FileIO> file_io_;
   std::string temp_avro_file_;
   bool skip_datum_{true};
@@ -806,6 +882,135 @@ TEST_P(AvroWriterTest, WritePrimitiveTypes) {
   VerifyWrittenData(test_data);
 }
 
+TEST_P(AvroWriterTest, WriteUuidType) {
+  auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{
+      SchemaField::MakeRequired(1, "uuid_col", iceberg::uuid())});
+
+  auto uuid_array = MakeUuidArray({&kUuidBytes1, &kUuidBytes2});
+  auto array =
+      ::arrow::StructArray::Make(
+          {uuid_array},
+          {::arrow::field("uuid_col", ::arrow::extension::uuid(), /*nullable=*/false)})
+          .ValueOrDie();
+
+  ArrowArray arrow_array;
+  ASSERT_TRUE(::arrow::ExportArray(*array, &arrow_array).ok());
+
+  WriterProperties writer_properties;
+  writer_properties.Set(WriterProperties::kAvroSkipDatum, skip_datum_);
+
+  ICEBERG_UNWRAP_OR_FAIL(
+      writer_, WriterFactoryRegistry::Open(FileFormatType::kAvro,
+                                           {.path = temp_avro_file_,
+                                            .schema = schema,
+                                            .io = file_io_,
+                                            .properties = std::move(writer_properties)}));
+  ASSERT_THAT(writer_->Write(&arrow_array), IsOk());
+  ASSERT_THAT(writer_->Close(), IsOk());
+
+  auto avro_schema = PhysicalAvroSchema();
+  auto root = avro_schema.root();
+  ASSERT_EQ(root->type(), ::avro::AVRO_RECORD);
+  ASSERT_EQ(root->leaves(), 1);
+  auto uuid_node = root->leafAt(0);
+  EXPECT_EQ(uuid_node->type(), ::avro::AVRO_FIXED);
+  EXPECT_EQ(uuid_node->logicalType().type(), ::avro::LogicalType::UUID);
+  EXPECT_EQ(uuid_node->fixedSize(), Uuid::kLength);
+
+  ICEBERG_UNWRAP_OR_FAIL(auto written_length, writer_->length());
+  ICEBERG_UNWRAP_OR_FAIL(
+      auto reader,
+      ReaderFactoryRegistry::Open(FileFormatType::kAvro, {.path = temp_avro_file_,
+                                                          .length = written_length,
+                                                          .io = file_io_,
+                                                          .projection = schema}));
+  ICEBERG_UNWRAP_OR_FAIL(auto data, reader->Next());
+  ASSERT_TRUE(data.has_value());
+
+  ICEBERG_UNWRAP_OR_FAIL(auto arrow_c_schema, reader->Schema());
+  auto read_array = ::arrow::ImportArray(&data.value(), &arrow_c_schema).ValueOrDie();
+  ASSERT_TRUE(read_array->Equals(*array)) << "actual:\n"
+                                          << read_array->ToString() << "\nexpected:\n"
+                                          << array->ToString();
+  ASSERT_NO_FATAL_FAILURE(VerifyExhausted(*reader));
+}
+
+TEST_P(AvroWriterTest, WriteUuidListType) {
+  auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{
+      SchemaField::MakeRequired(1, "uuid_list",
+                                std::make_shared<ListType>(SchemaField::MakeRequired(
+                                    2, ListType::kElementName, iceberg::uuid())))});
+
+  auto list_values = MakeUuidArray({&kUuidBytes1, &kUuidBytes2, &kUuidBytes1});
+  auto list_offsets = MakeInt32Array({0, 2, 3});
+  auto list_type = ::arrow::list(::arrow::field(std::string(ListType::kElementName),
+                                                ::arrow::extension::uuid(),
+                                                /*nullable=*/false));
+  auto list_array =
+      ::arrow::ListArray::FromArrays(list_type, *list_offsets, *list_values).ValueOrDie();
+
+  auto array =
+      ::arrow::StructArray::Make(
+          {list_array}, {::arrow::field("uuid_list", list_type, /*nullable=*/false)})
+          .ValueOrDie();
+
+  ASSERT_NO_FATAL_FAILURE(WriteArrowArrayAndVerify(schema, array));
+}
+
+TEST_P(AvroWriterTest, WriteUuidMapType) {
+  auto schema = std::make_shared<iceberg::Schema>(
+      std::vector<SchemaField>{SchemaField::MakeRequired(
+          1, "uuid_map",
+          std::make_shared<MapType>(
+              SchemaField::MakeRequired(2, MapType::kKeyName, iceberg::string()),
+              SchemaField::MakeRequired(3, MapType::kValueName, iceberg::uuid())))});
+
+  auto map_offsets = MakeInt32Array({0, 2, 3});
+  auto map_keys = MakeStringArray({"first", "second", "only"});
+  auto map_items = MakeUuidArray({&kUuidBytes1, &kUuidBytes2, &kUuidBytes1});
+  auto map_type =
+      ::arrow::map(::arrow::utf8(), ::arrow::field(std::string(MapType::kValueName),
+                                                   ::arrow::extension::uuid(),
+                                                   /*nullable=*/false));
+  auto map_array =
+      ::arrow::MapArray::FromArrays(map_type, map_offsets, map_keys, map_items)
+          .ValueOrDie();
+
+  auto array =
+      ::arrow::StructArray::Make(
+          {map_array}, {::arrow::field("uuid_map", map_type, /*nullable=*/false)})
+          .ValueOrDie();
+
+  ASSERT_NO_FATAL_FAILURE(WriteArrowArrayAndVerify(schema, array));
+}
+
+TEST_P(AvroWriterTest, WriteUuidMapKeyType) {
+  auto schema = std::make_shared<iceberg::Schema>(
+      std::vector<SchemaField>{SchemaField::MakeRequired(
+          1, "uuid_key_map",
+          std::make_shared<MapType>(
+              SchemaField::MakeRequired(2, MapType::kKeyName, iceberg::uuid()),
+              SchemaField::MakeRequired(3, MapType::kValueName, iceberg::string())))});
+
+  auto map_offsets = MakeInt32Array({0, 2, 3});
+  auto map_keys = MakeUuidArray({&kUuidBytes1, &kUuidBytes2, &kUuidBytes1});
+  auto map_items = MakeStringArray({"first", "second", "only"});
+  auto map_type =
+      ::arrow::map(::arrow::extension::uuid(),
+                   ::arrow::field(std::string(MapType::kValueName), ::arrow::utf8(),
+                                  /*nullable=*/false));
+  auto map_array =
+      ::arrow::MapArray::FromArrays(map_type, map_offsets, map_keys, map_items)
+          .ValueOrDie();
+
+  auto array =
+      ::arrow::StructArray::Make(
+          {map_array}, {::arrow::field("uuid_key_map", map_type, /*nullable=*/false)})
+          .ValueOrDie();
+
+  ASSERT_NO_FATAL_FAILURE(WriteArrowArrayAndVerify(schema, array));
+}
+
 TEST_P(AvroWriterTest, WriteTemporalTypes) {
   auto schema = std::make_shared<iceberg::Schema>(std::vector<SchemaField>{
       SchemaField::MakeRequired(1, "date_col", std::make_shared<DateType>()),
diff --git a/src/iceberg/test/parquet_test.cc b/src/iceberg/test/parquet_test.cc
index ee1cbc93..88c33760 100644
--- a/src/iceberg/test/parquet_test.cc
+++ b/src/iceberg/test/parquet_test.cc
@@ -17,13 +17,16 @@
  * under the License.
  */
 
+#include <array>
 #include <optional>
 #include <string>
 #include <utility>
 #include <vector>
 
 #include <arrow/array.h>
+#include <arrow/array/builder_binary.h>
 #include <arrow/c/bridge.h>
+#include <arrow/extension/uuid.h>
 #include <arrow/filesystem/filesystem.h>
 #include <arrow/json/from_string.h>
 #include <arrow/record_batch.h>
@@ -52,6 +55,7 @@
 #include "iceberg/type.h"
 #include "iceberg/util/checked_cast.h"
 #include "iceberg/util/macros.h"
+#include "iceberg/util/uuid.h"
 
 namespace iceberg::parquet {
 
@@ -150,6 +154,13 @@ std::optional<ParquetCodec> FirstUnavailableParquetCodec() {
   return std::nullopt;
 }
 
+constexpr std::array<uint8_t, Uuid::kLength> kUuidBytes1 = {
+    0x12, 0x3e, 0x45, 0x67, 0xe8, 0x9b, 0x12, 0xd3,
+    0xa4, 0x56, 0x42, 0x66, 0x14, 0x17, 0x40, 0x00};
+constexpr std::array<uint8_t, Uuid::kLength> kUuidBytes2 = {
+    0xf7, 0x9c, 0x3e, 0x09, 0x67, 0x7c, 0x4b, 0xbd,
+    0xa4, 0x79, 0x3f, 0x34, 0x9c, 0xb7, 0x85, 0xe7};
+
 }  // namespace
 
 class ParquetReaderTest : public TempFileTestBase {
@@ -771,4 +782,29 @@ TEST_F(ParquetReadWrite, SimpleTypeRoundTrip) {
   ASSERT_TRUE(out->Equals(*array));
 }
 
+TEST_F(ParquetReadWrite, UuidRoundTrip) {
+  auto schema = std::make_shared<Schema>(
+      std::vector<SchemaField>{SchemaField::MakeRequired(1, "uuid_col", uuid())});
+
+  ::arrow::FixedSizeBinaryBuilder uuid_storage_builder(
+      ::arrow::fixed_size_binary(Uuid::kLength));
+  ASSERT_TRUE(uuid_storage_builder.Append(kUuidBytes1.data()).ok());
+  ASSERT_TRUE(uuid_storage_builder.Append(kUuidBytes2.data()).ok());
+  auto uuid_storage = uuid_storage_builder.Finish().ValueOrDie();
+  auto uuid_array =
+      ::arrow::ExtensionType::WrapArray(::arrow::extension::uuid(), uuid_storage);
+  auto array =
+      ::arrow::StructArray::Make(
+          {uuid_array},
+          {::arrow::field("uuid_col", ::arrow::extension::uuid(), /*nullable=*/false)})
+          .ValueOrDie();
+
+  std::shared_ptr<::arrow::Array> out;
+  DoRoundtrip(array, schema, out);
+
+  ASSERT_TRUE(out->Equals(*array)) << "actual:\n"
+                                   << out->ToString() << "\nexpected:\n"
+                                   << array->ToString();
+}
+
 }  // namespace iceberg::parquet
diff --git a/src/iceberg/test/struct_like_test.cc b/src/iceberg/test/struct_like_test.cc
index 97df61b8..ad19d6a0 100644
--- a/src/iceberg/test/struct_like_test.cc
+++ b/src/iceberg/test/struct_like_test.cc
@@ -17,6 +17,11 @@
  * under the License.
  */
 
+#include "iceberg/row/struct_like.h"
+
+#include <array>
+#include <utility>
+
 #include <arrow/c/bridge.h>
 #include <arrow/json/from_string.h>
 #include <arrow/type.h>
@@ -24,6 +29,7 @@
 #include <arrow/util/decimal.h>
 
 #include "iceberg/arrow_c_data_guard_internal.h"
+#include "iceberg/expression/literal.h"
 #include "iceberg/manifest/manifest_list.h"
 #include "iceberg/manifest/manifest_reader_internal.h"
 #include "iceberg/row/arrow_array_wrapper.h"
@@ -32,6 +38,7 @@
 #include "iceberg/schema_internal.h"
 #include "iceberg/test/matchers.h"
 #include "iceberg/type.h"
+#include "iceberg/util/uuid.h"
 
 namespace iceberg {
 
@@ -59,6 +66,27 @@ namespace iceberg {
     ASSERT_TRUE(std::holds_alternative<std::monostate>(scalar)); \
   } while (0)
 
+namespace {
+
+class SingleFieldStructLike : public StructLike {
+ public:
+  explicit SingleFieldStructLike(Scalar value) : value_(std::move(value)) {}
+
+  Result<Scalar> GetField(size_t pos) const override {
+    if (pos != 0) {
+      return InvalidArgument("Field index {} out of range", pos);
+    }
+    return value_;
+  }
+
+  size_t num_fields() const override { return 1; }
+
+ private:
+  Scalar value_;
+};
+
+}  // namespace
+
 TEST(ManifestFileStructLike, BasicFields) {
   ManifestFile manifest_file{
       .manifest_path = "/path/to/manifest.avro",
@@ -99,6 +127,49 @@ TEST(ManifestFileStructLike, BasicFields) {
   EXPECT_THAT(struct_like.GetField(100), IsError(ErrorKind::kInvalidArgument));
 }
 
+TEST(LiteralToScalarTest, Uuid) {
+  ICEBERG_UNWRAP_OR_FAIL(auto uuid,
+                         Uuid::FromString("123e4567-e89b-12d3-a456-426614174000"));
+  auto literal = Literal::UUID(uuid);
+
+  ICEBERG_UNWRAP_OR_FAIL(auto scalar, LiteralToScalar(literal));
+  ASSERT_TRUE(std::holds_alternative<std::string_view>(scalar));
+
+  auto value = std::get<std::string_view>(scalar);
+  ASSERT_EQ(value.size(), Uuid::kLength);
+
+  const auto& expected_bytes = uuid.bytes();
+  for (size_t i = 0; i < expected_bytes.size(); ++i) {
+    EXPECT_EQ(static_cast<uint8_t>(static_cast<unsigned char>(value[i])),
+              expected_bytes[i]);
+  }
+}
+
+TEST(StructLikeAccessorTest, GetLiteralUuid) {
+  ICEBERG_UNWRAP_OR_FAIL(auto uuid,
+                         Uuid::FromString("123e4567-e89b-12d3-a456-426614174000"));
+  const auto& bytes = uuid.bytes();
+  std::string_view uuid_data(reinterpret_cast<const char*>(bytes.data()), bytes.size());
+  SingleFieldStructLike row(Scalar{uuid_data});
+  std::array<size_t, 1> path = {0};
+  StructLikeAccessor accessor(iceberg::uuid(), path);
+
+  ICEBERG_UNWRAP_OR_FAIL(auto literal, accessor.GetLiteral(row));
+  EXPECT_EQ(literal.type()->type_id(), TypeId::kUuid);
+  ASSERT_TRUE(std::holds_alternative<Uuid>(literal.value()));
+  EXPECT_EQ(std::get<Uuid>(literal.value()), uuid);
+}
+
+TEST(StructLikeAccessorTest, GetLiteralUuidRejectsWrongLength) {
+  SingleFieldStructLike row(Scalar{std::string_view("not-a-uuid")});
+  std::array<size_t, 1> path = {0};
+  StructLikeAccessor accessor(iceberg::uuid(), path);
+
+  auto result = accessor.GetLiteral(row);
+  EXPECT_THAT(result, IsError(ErrorKind::kInvalidArgument));
+  EXPECT_THAT(result, HasErrorMessage("UUID byte array must be exactly 16 bytes"));
+}
+
 TEST(ManifestFileStructLike, OptionalFields) {
   ManifestFile manifest_file{.manifest_path = "/path/to/manifest2.avro",
                              .manifest_length = 54321,