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

jinyewu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-graphar.git


The following commit(s) were added to refs/heads/main by this push:
     new 7bad8ec5 feat(Rust): add `EdgeInfo` support (#837)
7bad8ec5 is described below

commit 7bad8ec50f79314b7b9db472ae6e699d27d450f6
Author: Jinye Wu <[email protected]>
AuthorDate: Thu Feb 5 20:02:02 2026 +0800

    feat(Rust): add `EdgeInfo` support (#837)
---
 rust/include/graphar_rs.h      |  41 ++-
 rust/src/error.rs              |  92 ++++++
 rust/src/ffi.rs                | 105 +++++-
 rust/src/graphar_rs.cc         |  70 +++-
 rust/src/info/adjacent_list.rs | 156 +++++++++
 rust/src/info/edge_info.rs     | 717 +++++++++++++++++++++++++++++++++++++++++
 rust/src/info/mod.rs           |   9 +-
 rust/src/info/version.rs       |   4 +-
 rust/src/info/vertex_info.rs   | 155 +++++----
 rust/src/lib.rs                |  13 +-
 rust/src/property.rs           | 144 +++++----
 11 files changed, 1362 insertions(+), 144 deletions(-)

diff --git a/rust/include/graphar_rs.h b/rust/include/graphar_rs.h
index d7f313fb..42cd149b 100644
--- a/rust/include/graphar_rs.h
+++ b/rust/include/graphar_rs.h
@@ -19,6 +19,7 @@
 
 #pragma once
 
+#include <cstddef>
 #include <cstdint>
 #include <memory>
 #include <string>
@@ -32,13 +33,15 @@
 
 namespace graphar {
 using SharedPropertyGroup = std::shared_ptr<PropertyGroup>;
+using SharedAdjacentList = std::shared_ptr<AdjacentList>;
 using ConstInfoVersion = const InfoVersion;
-}
+} // namespace graphar
 
 namespace graphar_rs {
 rust::String to_type_name(const graphar::DataType &type);
 
-std::shared_ptr<graphar::ConstInfoVersion> new_const_info_version(int32_t 
version);
+std::shared_ptr<graphar::ConstInfoVersion>
+new_const_info_version(int32_t version);
 
 std::unique_ptr<graphar::Property>
 new_property(const std::string &name, std::shared_ptr<graphar::DataType> type,
@@ -72,13 +75,31 @@ std::unique_ptr<std::vector<graphar::SharedPropertyGroup>>
 property_group_vec_clone(
     const std::vector<graphar::SharedPropertyGroup> &property_groups);
 
-std::shared_ptr<graphar::VertexInfo>
-create_vertex_info(const std::string &type, graphar::IdType chunk_size,
-                   const std::vector<graphar::SharedPropertyGroup> 
&property_groups,
-                   const rust::Vec<rust::String> &labels,
-                   const std::string &prefix,
-                   std::shared_ptr<graphar::ConstInfoVersion> version);
+std::shared_ptr<graphar::VertexInfo> create_vertex_info(
+    const std::string &type, graphar::IdType chunk_size,
+    const std::vector<graphar::SharedPropertyGroup> &property_groups,
+    const rust::Vec<rust::String> &labels, const std::string &prefix,
+    std::shared_ptr<graphar::ConstInfoVersion> version);
 
-void vertex_info_save(const graphar::VertexInfo &vertex_info, const 
std::string &path);
-std::unique_ptr<std::string> vertex_info_dump(const graphar::VertexInfo 
&vertex_info);
+std::shared_ptr<graphar::EdgeInfo> create_edge_info(
+    const std::string &src_type, const std::string &edge_type,
+    const std::string &dst_type, graphar::IdType chunk_size,
+    graphar::IdType src_chunk_size, graphar::IdType dst_chunk_size,
+    bool directed, const graphar::AdjacentListVector &adjacent_lists,
+    const std::vector<graphar::SharedPropertyGroup> &property_groups,
+    const std::string &prefix,
+    std::shared_ptr<graphar::ConstInfoVersion> version);
+
+void vertex_info_save(const graphar::VertexInfo &vertex_info,
+                      const std::string &path);
+std::unique_ptr<std::string>
+vertex_info_dump(const graphar::VertexInfo &vertex_info);
+
+std::unique_ptr<graphar::AdjacentListVector> new_adjacent_list_vec();
+void push_adjacent_list(graphar::AdjacentListVector &v,
+                        std::shared_ptr<graphar::AdjacentList> adjacent_list);
+
+void edge_info_save(const graphar::EdgeInfo &edge_info,
+                    const std::string &path);
+std::unique_ptr<std::string> edge_info_dump(const graphar::EdgeInfo 
&edge_info);
 } // namespace graphar_rs
diff --git a/rust/src/error.rs b/rust/src/error.rs
new file mode 100644
index 00000000..fcc873ce
--- /dev/null
+++ b/rust/src/error.rs
@@ -0,0 +1,92 @@
+// 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.
+
+//! Error types for this crate.
+
+use std::fmt;
+use std::path::PathBuf;
+
+/// A result type for this crate.
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// An error returned by this crate.
+#[derive(Debug)]
+pub enum Error {
+    /// An exception raised from the C++ GraphAr implementation and propagated 
via `cxx`.
+    Cxx(cxx::Exception),
+    /// A filesystem path is not valid UTF-8.
+    NonUtf8Path(PathBuf),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Cxx(e) => write!(f, "C++ exception: {e}"),
+            Self::NonUtf8Path(path) => write!(f, "path is not valid UTF-8: 
{}", path.display()),
+        }
+    }
+}
+
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            Self::Cxx(e) => Some(e),
+            Self::NonUtf8Path(_) => None,
+        }
+    }
+}
+
+impl From<cxx::Exception> for Error {
+    fn from(value: cxx::Exception) -> Self {
+        Self::Cxx(value)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::info::VertexInfo;
+    use crate::property::PropertyGroupVector;
+    use std::error::Error as StdError;
+
+    #[test]
+    fn test_non_utf8_path_display_and_source() {
+        let err = 
Error::NonUtf8Path(std::path::PathBuf::from("not_utf8_checked_here"));
+        let msg = err.to_string();
+        assert!(msg.contains("path is not valid UTF-8"), "msg={msg:?}");
+        assert!(StdError::source(&err).is_none());
+    }
+
+    #[test]
+    fn test_cxx_error_display_source_and_from() {
+        let groups = PropertyGroupVector::new();
+        let err = match VertexInfo::try_new("", 1, groups, vec![], "", None) {
+            Ok(_) => panic!("expected error"),
+            Err(e) => e,
+        };
+
+        let cxx_exc = match err {
+            Error::Cxx(e) => e,
+            other => panic!("expected Error::Cxx, got {other:?}"),
+        };
+
+        let err = Error::from(cxx_exc);
+        let msg = err.to_string();
+        assert!(msg.contains("C++ exception:"), "msg={msg:?}");
+        assert!(StdError::source(&err).is_some());
+    }
+}
diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs
index d6b1e0a1..66a9c712 100644
--- a/rust/src/ffi.rs
+++ b/rust/src/ffi.rs
@@ -17,7 +17,6 @@
 
 use cxx::{ExternType, SharedPtr};
 
-/// A shared pointer wrapper for `graphar::PropertyGroup`.
 #[repr(transparent)]
 #[derive(Clone)]
 pub struct SharedPropertyGroup(pub(crate) SharedPtr<graphar::PropertyGroup>);
@@ -27,6 +26,15 @@ unsafe impl ExternType for SharedPropertyGroup {
     type Kind = cxx::kind::Opaque;
 }
 
+#[repr(transparent)]
+#[derive(Clone)]
+pub struct SharedAdjacentList(pub(crate) SharedPtr<graphar::AdjacentList>);
+
+unsafe impl ExternType for SharedAdjacentList {
+    type Id = cxx::type_id!("graphar::SharedAdjacentList");
+    type Kind = cxx::kind::Opaque;
+}
+
 #[cxx::bridge(namespace = "graphar")]
 pub(crate) mod graphar {
     extern "C++" {
@@ -107,10 +115,31 @@ pub(crate) mod graphar {
         Set = 2,
     }
 
+    /// Adjacency list type.
+    ///
+    /// This corresponds to GraphAr's `graphar::AdjListType` bit flags.
+    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+    #[repr(i32)]
+    enum AdjListType {
+        /// Unordered adjacency list by source vertex.
+        #[cxx_name = "unordered_by_source"]
+        UnorderedBySource = 0b0000_0001,
+        /// Unordered adjacency list by destination vertex.
+        #[cxx_name = "unordered_by_dest"]
+        UnorderedByDest = 0b0000_0010,
+        /// Ordered adjacency list by source vertex.
+        #[cxx_name = "ordered_by_source"]
+        OrderedBySource = 0b0000_0100,
+        /// Ordered adjacency list by destination vertex.
+        #[cxx_name = "ordered_by_dest"]
+        OrderedByDest = 0b0000_1000,
+    }
+
     unsafe extern "C++" {
         type FileType;
         type Type;
         type Cardinality;
+        type AdjListType;
     }
 
     // `DataType`
@@ -252,8 +281,82 @@ pub(crate) mod graphar {
         fn vertex_info_dump(vertex_info: &VertexInfo) -> 
Result<UniquePtr<CxxString>>;
     }
 
+    // `AdjacentList`
+    unsafe extern "C++" {
+        type AdjacentList;
+
+        fn GetType(&self) -> AdjListType;
+        fn GetFileType(&self) -> FileType;
+        fn GetPrefix(&self) -> &CxxString;
+
+        fn CreateAdjacentList(
+            type_: AdjListType,
+            file_type: FileType,
+            path_prefix: &CxxString,
+        ) -> SharedPtr<AdjacentList>;
+    }
+
+    // `AdjacentListVector`
+    unsafe extern "C++" {
+        #[namespace = "graphar_rs"]
+        fn new_adjacent_list_vec() -> UniquePtr<CxxVector<SharedAdjacentList>>;
+        #[namespace = "graphar_rs"]
+        fn push_adjacent_list(
+            vec: Pin<&mut CxxVector<SharedAdjacentList>>,
+            adjacent_list: SharedPtr<AdjacentList>,
+        );
+    }
+
+    // `EdgeInfo`
+    unsafe extern "C++" {
+        type EdgeInfo;
+
+        fn GetSrcType(&self) -> &CxxString;
+        fn GetEdgeType(&self) -> &CxxString;
+        fn GetDstType(&self) -> &CxxString;
+        fn GetChunkSize(&self) -> i64;
+        fn GetSrcChunkSize(&self) -> i64;
+        fn GetDstChunkSize(&self) -> i64;
+        fn GetPrefix(&self) -> &CxxString;
+        fn IsDirected(&self) -> bool;
+        fn version(&self) -> &SharedPtr<ConstInfoVersion>;
+        fn HasAdjacentListType(&self, adj_list_type: AdjListType) -> bool;
+        fn GetAdjacentList(&self, adj_list_type: AdjListType) -> 
SharedPtr<AdjacentList>;
+
+        fn PropertyGroupNum(&self) -> i32;
+        fn GetPropertyGroups(&self) -> &CxxVector<SharedPropertyGroup>;
+        fn GetPropertyGroup(&self, property: &CxxString) -> 
SharedPtr<PropertyGroup>;
+        fn GetPropertyGroupByIndex(&self, index: i32) -> 
SharedPtr<PropertyGroup>;
+
+        #[namespace = "graphar_rs"]
+        #[allow(clippy::too_many_arguments)]
+        fn create_edge_info(
+            src_type: &CxxString,
+            edge_type: &CxxString,
+            dst_type: &CxxString,
+            chunk_size: i64,
+            src_chunk_size: i64,
+            dst_chunk_size: i64,
+            directed: bool,
+            adjacent_lists: &CxxVector<SharedAdjacentList>,
+            property_groups: &CxxVector<SharedPropertyGroup>,
+            path_prefix: &CxxString,
+            version: SharedPtr<ConstInfoVersion>,
+        ) -> Result<SharedPtr<EdgeInfo>>;
+
+        #[namespace = "graphar_rs"]
+        fn edge_info_save(edge_info: &EdgeInfo, path: &CxxString) -> 
Result<()>;
+        #[namespace = "graphar_rs"]
+        fn edge_info_dump(edge_info: &EdgeInfo) -> 
Result<UniquePtr<CxxString>>;
+    }
+
     unsafe extern "C++" {
         type SharedPropertyGroup = crate::ffi::SharedPropertyGroup;
     }
     impl CxxVector<SharedPropertyGroup> {}
+
+    unsafe extern "C++" {
+        type SharedAdjacentList = crate::ffi::SharedAdjacentList;
+    }
+    impl CxxVector<SharedAdjacentList> {}
 }
diff --git a/rust/src/graphar_rs.cc b/rust/src/graphar_rs.cc
index d806ef99..ea979122 100644
--- a/rust/src/graphar_rs.cc
+++ b/rust/src/graphar_rs.cc
@@ -110,14 +110,54 @@ std::shared_ptr<graphar::VertexInfo> create_vertex_info(
     label_vec.emplace_back(std::string(labels[i]));
   }
 
-  auto vertex_info = graphar::CreateVertexInfo(type, chunk_size, 
property_groups,
-                                               label_vec, prefix, 
std::move(version));
+  auto vertex_info = graphar::CreateVertexInfo(
+      type, chunk_size, property_groups, label_vec, prefix, 
std::move(version));
   if (vertex_info == nullptr) {
     throw std::runtime_error("CreateVertexInfo: returned nullptr");
   }
   return vertex_info;
 }
 
+std::shared_ptr<graphar::EdgeInfo> create_edge_info(
+    const std::string &src_type, const std::string &edge_type,
+    const std::string &dst_type, graphar::IdType chunk_size,
+    graphar::IdType src_chunk_size, graphar::IdType dst_chunk_size,
+    bool directed, const graphar::AdjacentListVector &adjacent_lists,
+    const std::vector<graphar::SharedPropertyGroup> &property_groups,
+    const std::string &prefix,
+    std::shared_ptr<graphar::ConstInfoVersion> version) {
+  if (src_type.empty()) {
+    throw std::runtime_error("CreateEdgeInfo: src_type must not be empty");
+  }
+  if (edge_type.empty()) {
+    throw std::runtime_error("CreateEdgeInfo: edge_type must not be empty");
+  }
+  if (dst_type.empty()) {
+    throw std::runtime_error("CreateEdgeInfo: dst_type must not be empty");
+  }
+  if (chunk_size <= 0) {
+    throw std::runtime_error("CreateEdgeInfo: chunk_size must be > 0");
+  }
+  if (src_chunk_size <= 0) {
+    throw std::runtime_error("CreateEdgeInfo: src_chunk_size must be > 0");
+  }
+  if (dst_chunk_size <= 0) {
+    throw std::runtime_error("CreateEdgeInfo: dst_chunk_size must be > 0");
+  }
+  if (adjacent_lists.empty()) {
+    throw std::runtime_error(
+        "CreateEdgeInfo: adjacent_lists must not be empty");
+  }
+
+  auto edge_info = graphar::CreateEdgeInfo(
+      src_type, edge_type, dst_type, chunk_size, src_chunk_size, 
dst_chunk_size,
+      directed, adjacent_lists, property_groups, prefix, std::move(version));
+  if (edge_info == nullptr) {
+    throw std::runtime_error("CreateEdgeInfo: returned nullptr");
+  }
+  return edge_info;
+}
+
 void vertex_info_save(const graphar::VertexInfo &vertex_info,
                       const std::string &path) {
   auto status = vertex_info.Save(path);
@@ -134,4 +174,30 @@ vertex_info_dump(const graphar::VertexInfo &vertex_info) {
   }
   return std::make_unique<std::string>(std::move(dumped).value());
 }
+
+std::unique_ptr<graphar::AdjacentListVector> new_adjacent_list_vec() {
+  return std::make_unique<graphar::AdjacentListVector>();
+}
+
+void push_adjacent_list(graphar::AdjacentListVector &v,
+                        std::shared_ptr<graphar::AdjacentList> adjacent_list) {
+  v.emplace_back(std::move(adjacent_list));
+}
+
+void edge_info_save(const graphar::EdgeInfo &edge_info,
+                    const std::string &path) {
+  auto status = edge_info.Save(path);
+  if (!status.ok()) {
+    throw std::runtime_error(status.message());
+  }
+}
+
+std::unique_ptr<std::string>
+edge_info_dump(const graphar::EdgeInfo &edge_info) {
+  auto r = edge_info.Dump();
+  if (!r) {
+    throw std::runtime_error(r.error().message());
+  }
+  return std::make_unique<std::string>(std::move(r).value());
+}
 } // namespace graphar_rs
diff --git a/rust/src/info/adjacent_list.rs b/rust/src/info/adjacent_list.rs
new file mode 100644
index 00000000..8ef28e99
--- /dev/null
+++ b/rust/src/info/adjacent_list.rs
@@ -0,0 +1,156 @@
+// 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.
+
+//! GraphAr adjacent list metadata.
+
+use std::ops::Deref;
+
+use cxx::{CxxVector, SharedPtr, UniquePtr, let_cxx_string};
+
+use crate::ffi;
+use crate::info::AdjListType;
+use crate::types::FileType;
+
+/// An adjacency list definition in GraphAr metadata.
+pub type AdjacentList = ffi::SharedAdjacentList;
+
+impl AdjacentList {
+    /// Construct a `AdjacentList` from a raw C++ shared pointer.
+    pub(crate) fn from_inner(inner: SharedPtr<ffi::graphar::AdjacentList>) -> 
Self {
+        Self(inner)
+    }
+
+    /// Create a new adjacency list definition.
+    ///
+    /// If `path_prefix` is `None`, GraphAr will use a default prefix derived
+    /// from `ty` (e.g. `ordered_by_source/`).
+    pub fn new<P: AsRef<str>>(
+        ty: AdjListType,
+        file_type: FileType,
+        path_prefix: Option<P>,
+    ) -> Self {
+        let prefix = path_prefix.as_ref().map(|p| p.as_ref()).unwrap_or("");
+        let_cxx_string!(prefix = prefix);
+        let inner = ffi::graphar::CreateAdjacentList(ty, file_type, &prefix);
+        Self(inner)
+    }
+
+    /// Returns the adjacency list type.
+    pub fn ty(&self) -> AdjListType {
+        self.0.GetType()
+    }
+
+    /// Returns the chunk file type for this adjacency list.
+    pub fn file_type(&self) -> FileType {
+        self.0.GetFileType()
+    }
+
+    /// Returns the logical prefix for this adjacency list.
+    pub fn prefix(&self) -> String {
+        self.0.GetPrefix().to_string()
+    }
+}
+
+/// A vector of adjacency lists.
+///
+/// This is a wrapper around a C++ `graphar::AdjacentListVector`.
+pub struct AdjacentListVector(UniquePtr<CxxVector<AdjacentList>>);
+
+impl Default for AdjacentListVector {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Deref for AdjacentListVector {
+    type Target = CxxVector<AdjacentList>;
+
+    fn deref(&self) -> &Self::Target {
+        self.as_ref()
+    }
+}
+
+impl AdjacentListVector {
+    /// Create an empty adjacency list vector.
+    pub fn new() -> Self {
+        Self(ffi::graphar::new_adjacent_list_vec())
+    }
+
+    /// Push an adjacency list into the vector.
+    pub fn push(&mut self, adjacent_list: AdjacentList) {
+        ffi::graphar::push_adjacent_list(self.0.pin_mut(), adjacent_list.0);
+    }
+
+    /// Borrow the underlying C++ adjacency list vector.
+    pub(crate) fn as_ref(&self) -> &CxxVector<AdjacentList> {
+        self.0
+            .as_ref()
+            .expect("adjacent list vector should be valid")
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::types::FileType;
+
+    #[test]
+    fn test_adjacent_list_roundtrip_getters() {
+        let list = AdjacentList::new(AdjListType::OrderedBySource, 
FileType::Csv, Some("adj/"));
+        assert_eq!(list.ty(), AdjListType::OrderedBySource);
+        assert_eq!(list.file_type(), FileType::Csv);
+        assert_eq!(list.prefix(), "adj/");
+    }
+
+    #[test]
+    fn test_adjacent_list_default_prefix() {
+        let list = AdjacentList::new(
+            AdjListType::OrderedBySource,
+            FileType::Parquet,
+            None::<&str>,
+        );
+        assert_eq!(list.prefix(), "ordered_by_source/");
+    }
+
+    #[test]
+    fn test_adjacent_list_vector_push_and_borrow() {
+        let mut v = AdjacentListVector::new();
+        let _ = v.as_ref();
+        assert!(v.is_empty());
+
+        v.push(AdjacentList::new(
+            AdjListType::UnorderedBySource,
+            FileType::Parquet,
+            Some("u/"),
+        ));
+        let _ = v.as_ref();
+        assert_eq!(v.len(), 1);
+    }
+
+    #[test]
+    fn test_adjacent_list_vector_default() {
+        let mut v = AdjacentListVector::default();
+        assert_eq!(v.len(), 0);
+        v.push(AdjacentList::new(
+            AdjListType::OrderedByDest,
+            FileType::Parquet,
+            None::<&str>,
+        ));
+        assert_eq!(v.len(), 1);
+        let _ = v.as_ref();
+    }
+}
diff --git a/rust/src/info/edge_info.rs b/rust/src/info/edge_info.rs
new file mode 100644
index 00000000..1862da25
--- /dev/null
+++ b/rust/src/info/edge_info.rs
@@ -0,0 +1,717 @@
+// 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.
+
+//! GraphAr edge metadata.
+
+use std::path::Path;
+
+use cxx::{CxxVector, SharedPtr, UniquePtr, let_cxx_string};
+
+use crate::ffi;
+use crate::info::{AdjListType, AdjacentList, AdjacentListVector, InfoVersion};
+use crate::property::{PropertyGroup, PropertyGroupVector};
+
+/// An edge definition in the GraphAr metadata.
+#[derive(Clone)]
+pub struct EdgeInfo(pub(crate) SharedPtr<ffi::graphar::EdgeInfo>);
+
+impl EdgeInfo {
+    /// Create a builder for [`EdgeInfo`].
+    ///
+    /// This is the preferred API when constructing `EdgeInfo` in Rust, since
+    /// the raw constructor has many parameters.
+    pub fn builder(
+        src_type: impl Into<String>,
+        edge_type: impl Into<String>,
+        dst_type: impl Into<String>,
+        chunk_size: i64,
+        src_chunk_size: i64,
+        dst_chunk_size: i64,
+    ) -> EdgeInfoBuilder {
+        EdgeInfoBuilder::new(
+            src_type,
+            edge_type,
+            dst_type,
+            chunk_size,
+            src_chunk_size,
+            dst_chunk_size,
+        )
+    }
+
+    /// Create a new edge definition.
+    ///
+    /// The `prefix` is a logical prefix string used by GraphAr (it is not a 
filesystem path).
+    ///
+    /// Panics if GraphAr rejects the inputs (including, but not limited to,
+    /// empty type names, non-positive chunk sizes, or empty adjacency list
+    /// vector). Prefer [`EdgeInfo::try_new`] if you want to handle errors.
+    #[allow(clippy::too_many_arguments)]
+    pub fn new<P: AsRef<str>>(
+        src_type: &str,
+        edge_type: &str,
+        dst_type: &str,
+        chunk_size: i64,
+        src_chunk_size: i64,
+        dst_chunk_size: i64,
+        directed: bool,
+        adjacent_lists: AdjacentListVector,
+        property_groups: PropertyGroupVector,
+        prefix: P,
+        version: Option<InfoVersion>,
+    ) -> Self {
+        Self::try_new(
+            src_type,
+            edge_type,
+            dst_type,
+            chunk_size,
+            src_chunk_size,
+            dst_chunk_size,
+            directed,
+            adjacent_lists,
+            property_groups,
+            prefix,
+            version,
+        )
+        .unwrap()
+    }
+
+    /// Try to create a new edge definition.
+    ///
+    /// This returns an error if any required field is invalid (e.g. empty type
+    /// names, non-positive chunk sizes, or empty adjacency list vector), or if
+    /// the upstream GraphAr implementation rejects the inputs.
+    #[allow(clippy::too_many_arguments)]
+    pub fn try_new<P: AsRef<str>>(
+        src_type: &str,
+        edge_type: &str,
+        dst_type: &str,
+        chunk_size: i64,
+        src_chunk_size: i64,
+        dst_chunk_size: i64,
+        directed: bool,
+        adjacent_lists: AdjacentListVector,
+        property_groups: PropertyGroupVector,
+        prefix: P,
+        version: Option<InfoVersion>,
+    ) -> crate::Result<Self> {
+        let_cxx_string!(src = src_type);
+        let_cxx_string!(edge = edge_type);
+        let_cxx_string!(dst = dst_type);
+        let_cxx_string!(prefix = prefix.as_ref());
+
+        let prop_groups = property_groups.as_ref();
+        let version = version.map(|v| v.0).unwrap_or_else(SharedPtr::null);
+
+        let inner = ffi::graphar::create_edge_info(
+            &src,
+            &edge,
+            &dst,
+            chunk_size,
+            src_chunk_size,
+            dst_chunk_size,
+            directed,
+            adjacent_lists.as_ref(),
+            prop_groups,
+            &prefix,
+            version,
+        )?;
+        Ok(Self(inner))
+    }
+
+    /// Return the source vertex type.
+    pub fn src_type(&self) -> String {
+        self.0.GetSrcType().to_string()
+    }
+
+    /// Return the edge type.
+    pub fn edge_type(&self) -> String {
+        self.0.GetEdgeType().to_string()
+    }
+
+    /// Return the destination vertex type.
+    pub fn dst_type(&self) -> String {
+        self.0.GetDstType().to_string()
+    }
+
+    /// Return the global chunk size.
+    pub fn chunk_size(&self) -> i64 {
+        self.0.GetChunkSize()
+    }
+
+    /// Return the chunk size for the source vertex type.
+    pub fn src_chunk_size(&self) -> i64 {
+        self.0.GetSrcChunkSize()
+    }
+
+    /// Return the chunk size for the destination vertex type.
+    pub fn dst_chunk_size(&self) -> i64 {
+        self.0.GetDstChunkSize()
+    }
+
+    /// Return the logical prefix.
+    pub fn prefix(&self) -> String {
+        self.0.GetPrefix().to_string()
+    }
+
+    /// Return whether this edge is directed.
+    pub fn is_directed(&self) -> bool {
+        self.0.IsDirected()
+    }
+
+    /// Return the optional info version.
+    pub fn version(&self) -> Option<InfoVersion> {
+        let sp = self.0.version();
+        if sp.is_null() {
+            None
+        } else {
+            Some(InfoVersion(sp.clone()))
+        }
+    }
+
+    /// Return whether this edge has the given adjacency list type.
+    pub fn has_adjacent_list_type(&self, adj_list_type: AdjListType) -> bool {
+        self.0.HasAdjacentListType(adj_list_type)
+    }
+
+    /// Return the adjacency list definition of the given type.
+    ///
+    /// Returns `None` if the adjacency list type is not found.
+    pub fn adjacent_list(&self, adj_list_type: AdjListType) -> 
Option<AdjacentList> {
+        let sp = self.0.GetAdjacentList(adj_list_type);
+        if sp.is_null() {
+            None
+        } else {
+            Some(AdjacentList::from_inner(sp))
+        }
+    }
+
+    /// Return the number of property groups.
+    ///
+    /// TODO: upstream C++ uses `int` for this return type; prefer fixed-width.
+    pub fn property_group_num(&self) -> i32 {
+        self.0.PropertyGroupNum()
+    }
+
+    /// Return property groups.
+    ///
+    /// This is an advanced API that exposes `cxx` types and ties the returned
+    /// reference to the lifetime of `self`.
+    pub fn property_groups_cxx(&self) -> &CxxVector<PropertyGroup> {
+        self.0.GetPropertyGroups()
+    }
+
+    /// Return property groups.
+    ///
+    /// This allocates a new `Vec`. Prefer [`EdgeInfo::property_groups_iter`]
+    /// if you only need to iterate.
+    pub fn property_groups(&self) -> Vec<PropertyGroup> {
+        self.property_groups_iter().collect()
+    }
+
+    /// Iterate over property groups without allocating a `Vec`.
+    pub fn property_groups_iter(&self) -> impl Iterator<Item = PropertyGroup> 
+ '_ {
+        self.0.GetPropertyGroups().iter().cloned()
+    }
+
+    /// Return the property group containing the given property name.
+    ///
+    /// Returns `None` if the property is not found.
+    pub fn property_group<S: AsRef<str>>(&self, property_name: S) -> 
Option<PropertyGroup> {
+        let_cxx_string!(name = property_name.as_ref());
+        let sp = self.0.GetPropertyGroup(&name);
+        if sp.is_null() {
+            None
+        } else {
+            Some(PropertyGroup::from_inner(sp))
+        }
+    }
+
+    /// Return the property group at the given index.
+    ///
+    /// Returns `None` if the index is out of range.
+    ///
+    /// TODO: upstream C++ uses `int` for this parameter; prefer fixed-width.
+    pub fn property_group_by_index(&self, index: i32) -> Option<PropertyGroup> 
{
+        let sp = self.0.GetPropertyGroupByIndex(index);
+        if sp.is_null() {
+            None
+        } else {
+            Some(PropertyGroup::from_inner(sp))
+        }
+    }
+
+    /// Save this edge definition as a YAML file.
+    ///
+    /// Note: `path` must be valid UTF-8. On Unix, paths can contain arbitrary
+    /// bytes; such non-UTF8 paths return [`crate::Error::NonUtf8Path`].
+    pub fn save<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
+        let path_str = crate::path_to_utf8_str(path.as_ref())?;
+        let_cxx_string!(p = path_str);
+        ffi::graphar::edge_info_save(&self.0, &p)?;
+        Ok(())
+    }
+
+    /// Dump this edge definition to a YAML string.
+    pub fn dump(&self) -> crate::Result<String> {
+        let dumped: UniquePtr<cxx::CxxString> = 
ffi::graphar::edge_info_dump(&self.0)?;
+        let dumped = dumped.as_ref().expect("edge info dump should be valid");
+        Ok(dumped.to_string())
+    }
+}
+
+/// A builder for constructing an [`EdgeInfo`].
+///
+/// This builder is intended to reduce the argument noise of [`EdgeInfo::new`],
+/// while keeping the resulting `EdgeInfo` construction explicit and readable.
+///
+/// Note: You must supply at least one adjacent list (via
+/// [`EdgeInfoBuilder::push_adjacent_list`] or 
[`EdgeInfoBuilder::adjacent_lists`])
+/// before calling [`EdgeInfoBuilder::build`] / [`EdgeInfoBuilder::try_build`].
+/// If the adjacent list vector is empty, GraphAr will reject the inputs:
+/// `try_build` returns an error and `build` panics.
+///
+/// Defaults:
+/// - `directed = false`
+/// - `adjacent_lists = []` (must be non-empty before `build` / `try_build`)
+/// - `property_groups = []`
+/// - `prefix = ""` (GraphAr may set a default prefix based on type names)
+/// - `version = None`
+pub struct EdgeInfoBuilder {
+    src_type: String,
+    edge_type: String,
+    dst_type: String,
+    chunk_size: i64,
+    src_chunk_size: i64,
+    dst_chunk_size: i64,
+    directed: bool,
+    adjacent_lists: AdjacentListVector,
+    property_groups: PropertyGroupVector,
+    prefix: String,
+    version: Option<InfoVersion>,
+}
+
+impl EdgeInfoBuilder {
+    /// Create a new builder with required fields.
+    pub fn new(
+        src_type: impl Into<String>,
+        edge_type: impl Into<String>,
+        dst_type: impl Into<String>,
+        chunk_size: i64,
+        src_chunk_size: i64,
+        dst_chunk_size: i64,
+    ) -> Self {
+        Self {
+            src_type: src_type.into(),
+            edge_type: edge_type.into(),
+            dst_type: dst_type.into(),
+            chunk_size,
+            src_chunk_size,
+            dst_chunk_size,
+            directed: false,
+            adjacent_lists: AdjacentListVector::new(),
+            property_groups: PropertyGroupVector::new(),
+            prefix: String::new(),
+            version: None,
+        }
+    }
+
+    /// Set whether this edge is directed.
+    pub fn directed(mut self, directed: bool) -> Self {
+        self.directed = directed;
+        self
+    }
+
+    /// Set the logical prefix.
+    ///
+    /// This is a logical prefix string used by GraphAr (it is not a 
filesystem path).
+    pub fn prefix<P: AsRef<str>>(mut self, prefix: P) -> Self {
+        self.prefix = prefix.as_ref().to_owned();
+        self
+    }
+
+    /// Set the info format version.
+    pub fn version(mut self, version: InfoVersion) -> Self {
+        self.version = Some(version);
+        self
+    }
+
+    /// Set the optional info format version.
+    pub fn version_opt(mut self, version: Option<InfoVersion>) -> Self {
+        self.version = version;
+        self
+    }
+
+    /// Push an adjacent list definition.
+    pub fn push_adjacent_list(mut self, adjacent_list: AdjacentList) -> Self {
+        self.adjacent_lists.push(adjacent_list);
+        self
+    }
+
+    /// Replace adjacent lists with the given vector.
+    pub fn adjacent_lists(mut self, adjacent_lists: AdjacentListVector) -> 
Self {
+        self.adjacent_lists = adjacent_lists;
+        self
+    }
+
+    /// Push a property group definition.
+    pub fn push_property_group(mut self, property_group: PropertyGroup) -> 
Self {
+        self.property_groups.push(property_group);
+        self
+    }
+
+    /// Replace property groups with the given vector.
+    pub fn property_groups(mut self, property_groups: PropertyGroupVector) -> 
Self {
+        self.property_groups = property_groups;
+        self
+    }
+
+    /// Build an [`EdgeInfo`].
+    ///
+    /// Panics if GraphAr rejects the builder inputs. Prefer
+    /// [`EdgeInfoBuilder::try_build`] if you want to handle errors.
+    pub fn build(self) -> EdgeInfo {
+        self.try_build().unwrap()
+    }
+
+    /// Try to build an [`EdgeInfo`].
+    pub fn try_build(self) -> crate::Result<EdgeInfo> {
+        let Self {
+            src_type,
+            edge_type,
+            dst_type,
+            chunk_size,
+            src_chunk_size,
+            dst_chunk_size,
+            directed,
+            adjacent_lists,
+            property_groups,
+            prefix,
+            version,
+        } = self;
+
+        EdgeInfo::try_new(
+            &src_type,
+            &edge_type,
+            &dst_type,
+            chunk_size,
+            src_chunk_size,
+            dst_chunk_size,
+            directed,
+            adjacent_lists,
+            property_groups,
+            prefix.as_str(),
+            version,
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::property::{PropertyBuilder, PropertyGroup, PropertyGroupVector, 
PropertyVec};
+    use crate::types::{DataType, FileType};
+    use tempfile::tempdir;
+
+    fn make_adjacent_lists() -> AdjacentListVector {
+        let mut lists = AdjacentListVector::new();
+        lists.push(AdjacentList::new(
+            AdjListType::UnorderedBySource,
+            FileType::Parquet,
+            Some("adj/"),
+        ));
+        lists
+    }
+
+    fn make_property_groups() -> PropertyGroupVector {
+        let mut props = PropertyVec::new();
+        props.emplace(PropertyBuilder::new("id", 
DataType::int64()).primary_key(true));
+        props.emplace(PropertyBuilder::new("weight", DataType::float64()));
+
+        let pg = PropertyGroup::new(props, FileType::Parquet, "props/");
+        let mut groups = PropertyGroupVector::new();
+        groups.push(pg);
+        groups
+    }
+
+    #[test]
+    fn test_edge_info_clone_and_lookup_inputs() {
+        let edge_info = EdgeInfoBuilder::new("person", "knows", "person", 
1024, 100, 100)
+            .adjacent_lists(make_adjacent_lists())
+            .property_groups(make_property_groups())
+            .prefix("edge/person_knows_person/")
+            .build();
+
+        let cloned = edge_info.clone();
+        assert_eq!(cloned.src_type(), "person");
+        assert_eq!(cloned.edge_type(), "knows");
+        assert_eq!(cloned.dst_type(), "person");
+        assert_eq!(cloned.prefix(), "edge/person_knows_person/");
+
+        assert!(cloned.property_group(String::from("id")).is_some());
+        assert!(cloned.property_group(String::from("missing")).is_none());
+    }
+
+    #[test]
+    fn test_edge_info_builder_basic() {
+        let edge_info = EdgeInfoBuilder::new("person", "knows", "person", 
1024, 100, 100)
+            .directed(true)
+            .prefix("knows/")
+            .version(InfoVersion::new(1).unwrap())
+            .adjacent_lists(make_adjacent_lists())
+            .property_groups(make_property_groups())
+            .build();
+
+        assert_eq!(edge_info.src_type(), "person");
+        assert_eq!(edge_info.edge_type(), "knows");
+        assert_eq!(edge_info.dst_type(), "person");
+        assert_eq!(edge_info.chunk_size(), 1024);
+        assert_eq!(edge_info.src_chunk_size(), 100);
+        assert_eq!(edge_info.dst_chunk_size(), 100);
+        assert!(edge_info.is_directed());
+        assert_eq!(edge_info.prefix(), "knows/");
+        assert!(edge_info.version().is_some());
+        
assert!(edge_info.has_adjacent_list_type(AdjListType::UnorderedBySource));
+        assert!(
+            edge_info
+                .adjacent_list(AdjListType::UnorderedBySource)
+                .is_some()
+        );
+        assert!(
+            edge_info
+                .adjacent_list(AdjListType::OrderedBySource)
+                .is_none()
+        );
+        assert_eq!(edge_info.property_group_num(), 1);
+        assert!(edge_info.property_group("id").is_some());
+        assert!(edge_info.property_group("missing").is_none());
+        assert!(edge_info.property_group_by_index(0).is_some());
+        assert!(edge_info.property_group_by_index(1).is_none());
+        assert!(edge_info.property_group_by_index(-1).is_none());
+    }
+
+    #[test]
+    fn test_edge_info_builder_aliases_and_push_helpers() {
+        let version = InfoVersion::new(1).unwrap();
+        let pg_vec = make_property_groups();
+        let pg = pg_vec.get(0).unwrap().clone();
+
+        let mut adj = AdjacentListVector::new();
+        adj.push(AdjacentList::new(
+            AdjListType::OrderedBySource,
+            FileType::Parquet,
+            None::<&str>,
+        ));
+        let list = AdjacentList::new(
+            AdjListType::UnorderedBySource,
+            FileType::Parquet,
+            Some("u/"),
+        );
+
+        let edge_info = EdgeInfo::builder("person", "knows", "person", 1024, 
100, 100)
+            .directed(false)
+            .prefix("edge/person_knows_person/")
+            .version_opt(Some(version))
+            .push_adjacent_list(list)
+            .adjacent_lists(adj)
+            .push_property_group(pg)
+            .property_groups(pg_vec)
+            .build();
+
+        assert_eq!(edge_info.prefix(), "edge/person_knows_person/");
+
+        // Covered API that exposes cxx types.
+        let groups_cxx = edge_info.property_groups_cxx();
+        assert_eq!(groups_cxx.len(), 1);
+        assert!(groups_cxx.get(0).unwrap().has_property("id"));
+    }
+
+    #[test]
+    fn test_edge_info_builder_try_build_error_paths() {
+        // adjacent_lists cannot be empty
+        let err = EdgeInfoBuilder::new("person", "knows", "person", 1024, 100, 
100)
+            .property_groups(make_property_groups())
+            .try_build()
+            .err()
+            .unwrap();
+        let msg = err.to_string();
+        assert!(
+            msg.contains("CreateEdgeInfo") && msg.contains("adjacent_lists 
must not be empty"),
+            "unexpected error message: {msg:?}"
+        );
+    }
+
+    #[test]
+    fn test_edge_info_property_groups_roundtrip() {
+        let edge_info = EdgeInfoBuilder::new("person", "knows", "person", 
1024, 100, 100)
+            .adjacent_lists(make_adjacent_lists())
+            .property_groups(make_property_groups())
+            .build();
+
+        assert!(edge_info.version().is_none());
+
+        let groups = edge_info.property_groups();
+        assert_eq!(groups.len(), 1);
+        assert!(groups[0].has_property("id"));
+        assert!(groups[0].has_property("weight"));
+    }
+
+    #[test]
+    fn test_edge_info_dump_and_save() {
+        let edge_info = EdgeInfoBuilder::new("person", "knows", "person", 
1024, 100, 100)
+            .directed(true)
+            .adjacent_lists(make_adjacent_lists())
+            .property_groups(make_property_groups())
+            .prefix("edge/person_knows_person/")
+            .build();
+
+        let dumped = edge_info.dump().unwrap();
+        assert!(!dumped.trim().is_empty(), "dumped={dumped:?}");
+        assert!(dumped.contains("person"), "dumped={dumped:?}");
+        assert!(dumped.contains("knows"), "dumped={dumped:?}");
+        assert!(
+            dumped.contains("edge/person_knows_person/"),
+            "dumped={dumped:?}"
+        );
+
+        let dir = tempdir().unwrap();
+        let path = dir.path().join("edge_info.yaml");
+        edge_info.save(&path).unwrap();
+
+        let metadata = std::fs::metadata(&path).unwrap();
+        assert!(metadata.is_file());
+        assert!(metadata.len() > 0);
+
+        let saved = std::fs::read_to_string(&path).unwrap();
+        assert!(!saved.trim().is_empty(), "saved={saved:?}");
+        assert!(saved.contains("person"), "saved={saved:?}");
+        assert!(saved.contains("knows"), "saved={saved:?}");
+    }
+
+    #[cfg(unix)]
+    #[test]
+    fn test_edge_info_save_non_utf8_path() {
+        use std::os::unix::ffi::OsStringExt;
+
+        let edge_info = EdgeInfoBuilder::new("person", "knows", "person", 
1024, 100, 100)
+            .adjacent_lists(make_adjacent_lists())
+            .property_groups(make_property_groups())
+            .build();
+
+        let dir = tempdir().unwrap();
+        let mut path = dir.path().to_path_buf();
+        path.push(std::ffi::OsString::from_vec(
+            b"edge_info_\xFF.yaml".to_vec(),
+        ));
+
+        let err = edge_info.save(&path).err().unwrap();
+        assert!(
+            matches!(err, crate::Error::NonUtf8Path(_)),
+            "unexpected error: {err:?}"
+        );
+        assert!(std::fs::metadata(&path).is_err());
+    }
+
+    #[test]
+    fn test_edge_info_try_new_error_paths() {
+        let version = InfoVersion::new(1).unwrap();
+
+        // src_type cannot be empty
+        let msg = EdgeInfo::try_new(
+            "",
+            "knows",
+            "person",
+            1,
+            1,
+            1,
+            true,
+            make_adjacent_lists(),
+            make_property_groups(),
+            "",
+            Some(version.clone()),
+        )
+        .err()
+        .unwrap()
+        .to_string();
+        assert!(
+            msg.contains("CreateEdgeInfo") && msg.contains("src_type must not 
be empty"),
+            "unexpected error message: {msg:?}"
+        );
+
+        // `chunk_size` cannot be less than 1
+        let msg = EdgeInfo::try_new(
+            "person",
+            "knows",
+            "person",
+            0,
+            1,
+            1,
+            true,
+            make_adjacent_lists(),
+            make_property_groups(),
+            "",
+            Some(version.clone()),
+        )
+        .err()
+        .unwrap()
+        .to_string();
+        assert!(
+            msg.contains("CreateEdgeInfo") && msg.contains("chunk_size must be 
> 0"),
+            "unexpected error message: {msg:?}"
+        );
+
+        // adjacent_lists cannot be empty
+        let msg = EdgeInfo::try_new(
+            "person",
+            "knows",
+            "person",
+            1,
+            1,
+            1,
+            true,
+            AdjacentListVector::new(),
+            make_property_groups(),
+            "",
+            Some(version),
+        )
+        .err()
+        .unwrap()
+        .to_string();
+        assert!(
+            msg.contains("CreateEdgeInfo") && msg.contains("adjacent_lists 
must not be empty"),
+            "unexpected error message: {msg:?}"
+        );
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_edge_info_new_panics_on_invalid_args() {
+        let version = InfoVersion::new(1).unwrap();
+        let _ = EdgeInfo::new(
+            "",
+            "knows",
+            "person",
+            1,
+            1,
+            1,
+            true,
+            make_adjacent_lists(),
+            make_property_groups(),
+            "",
+            Some(version),
+        );
+    }
+}
diff --git a/rust/src/info/mod.rs b/rust/src/info/mod.rs
index e46a62a7..3e032f9e 100644
--- a/rust/src/info/mod.rs
+++ b/rust/src/info/mod.rs
@@ -15,10 +15,17 @@
 // specific language governing permissions and limitations
 // under the License.
 
-//! Graph metadata bindings.
+//! GraphAr metadata information types.
 
+mod adjacent_list;
+mod edge_info;
 mod version;
 mod vertex_info;
 
+/// Re-export of the C++ `graphar::AdjListType`.
+pub use crate::ffi::graphar::AdjListType;
+
+pub use adjacent_list::{AdjacentList, AdjacentListVector};
+pub use edge_info::{EdgeInfo, EdgeInfoBuilder};
 pub use version::InfoVersion;
 pub use vertex_info::{VertexInfo, VertexInfoBuilder};
diff --git a/rust/src/info/version.rs b/rust/src/info/version.rs
index b10b30f5..050632f2 100644
--- a/rust/src/info/version.rs
+++ b/rust/src/info/version.rs
@@ -20,7 +20,7 @@
 use crate::ffi;
 use cxx::SharedPtr;
 
-/// A GraphAr `InfoVersion` value.
+/// A GraphAr info version.
 ///
 /// This is a thin wrapper around `std::shared_ptr<const 
graphar::InfoVersion>`.
 #[derive(Clone)]
@@ -30,7 +30,7 @@ impl InfoVersion {
     /// Create a new `InfoVersion` by version number.
     ///
     /// TODO: upstream C++ constructor takes `int`; prefer fixed-width integer 
types.
-    pub fn new(version: i32) -> Result<Self, cxx::Exception> {
+    pub fn new(version: i32) -> crate::Result<Self> {
         Ok(Self(ffi::graphar::new_const_info_version(version)?))
     }
 }
diff --git a/rust/src/info/vertex_info.rs b/rust/src/info/vertex_info.rs
index 11d9a714..0acd1b53 100644
--- a/rust/src/info/vertex_info.rs
+++ b/rust/src/info/vertex_info.rs
@@ -18,9 +18,8 @@
 //! Vertex metadata bindings.
 
 use super::version::InfoVersion;
-use crate::{cxx_string_to_string, ffi, property::PropertyGroup, 
property::PropertyGroupVector};
+use crate::{ffi, property::PropertyGroup, property::PropertyGroupVector};
 use cxx::{CxxVector, SharedPtr, UniquePtr, let_cxx_string};
-use std::borrow::Cow;
 use std::path::Path;
 
 /// GraphAr vertex metadata (`graphar::VertexInfo`).
@@ -32,12 +31,8 @@ impl VertexInfo {
     ///
     /// This is the preferred API when constructing `VertexInfo` in Rust, since
     /// the raw constructor has many parameters.
-    pub fn builder<S: Into<String>>(
-        r#type: S,
-        chunk_size: i64,
-        property_groups: PropertyGroupVector,
-    ) -> VertexInfoBuilder {
-        VertexInfoBuilder::new(r#type, chunk_size, property_groups)
+    pub fn builder(r#type: impl Into<String>, chunk_size: i64) -> 
VertexInfoBuilder {
+        VertexInfoBuilder::new(r#type, chunk_size)
     }
 
     /// Create a new `VertexInfo`.
@@ -48,8 +43,8 @@ impl VertexInfo {
     /// Panics if GraphAr rejects the inputs (including, but not limited to,
     /// `type` being empty or `chunk_size <= 0`). Prefer 
[`VertexInfo::try_new`]
     /// if you want to handle errors.
-    pub fn new<S: AsRef<[u8]>, P: AsRef<[u8]>>(
-        r#type: S,
+    pub fn new<T: AsRef<str>, P: AsRef<str>>(
+        r#type: T,
         chunk_size: i64,
         property_groups: PropertyGroupVector,
         labels: Vec<String>,
@@ -63,20 +58,18 @@ impl VertexInfo {
     ///
     /// This returns an error if `type` is empty, `chunk_size <= 0`, or if the
     /// upstream GraphAr implementation rejects the inputs.
-    pub fn try_new<S: AsRef<[u8]>, P: AsRef<[u8]>>(
-        r#type: S,
+    pub fn try_new<T: AsRef<str>, P: AsRef<str>>(
+        r#type: T,
         chunk_size: i64,
         property_groups: PropertyGroupVector,
         labels: Vec<String>,
         prefix: P,
         version: Option<InfoVersion>,
-    ) -> Result<Self, cxx::Exception> {
+    ) -> crate::Result<Self> {
         let_cxx_string!(ty = r#type.as_ref());
-        let_cxx_string!(prefix = prefix);
+        let_cxx_string!(prefix = prefix.as_ref());
 
-        let groups_ref = property_groups
-            .as_ref()
-            .expect("property group vec should be valid");
+        let groups_ref = property_groups.as_ref();
         let version = version.map(|v| v.0).unwrap_or_else(SharedPtr::null);
 
         Ok(Self(ffi::graphar::create_vertex_info(
@@ -86,7 +79,7 @@ impl VertexInfo {
 
     /// Return the vertex type name.
     pub fn type_name(&self) -> String {
-        cxx_string_to_string(self.0.GetType())
+        self.0.GetType().to_string()
     }
 
     /// Return the chunk size.
@@ -96,7 +89,7 @@ impl VertexInfo {
 
     /// Return the logical prefix.
     pub fn prefix(&self) -> String {
-        cxx_string_to_string(self.0.GetPrefix())
+        self.0.GetPrefix().to_string()
     }
 
     /// Return the optional format version.
@@ -114,7 +107,7 @@ impl VertexInfo {
         let labels = self.0.GetLabels();
         let mut out = Vec::with_capacity(labels.len());
         for label in labels {
-            out.push(cxx_string_to_string(label));
+            out.push(label.to_string());
         }
         out
     }
@@ -158,8 +151,8 @@ impl VertexInfo {
     /// Return the property group containing the given property.
     ///
     /// Returns `None` if the property is not found.
-    pub fn property_group<S: AsRef<[u8]>>(&self, property_name: S) -> 
Option<PropertyGroup> {
-        let_cxx_string!(name = property_name);
+    pub fn property_group<S: AsRef<str>>(&self, property_name: S) -> 
Option<PropertyGroup> {
+        let_cxx_string!(name = property_name.as_ref());
 
         let sp = self.0.GetPropertyGroup(&name);
         if sp.is_null() {
@@ -191,29 +184,19 @@ impl VertexInfo {
 
     /// Save this `VertexInfo` to the given path.
     ///
-    /// On Unix, this passes the raw `OsStr` bytes to C++ to avoid lossy UTF-8
-    /// conversion. On non-Unix platforms, this falls back to converting the
-    /// path to UTF-8 using [`Path::to_string_lossy`].
-    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), cxx::Exception> {
-        let path = path.as_ref();
-
-        #[cfg(unix)]
-        let path_bytes: Cow<[u8]> = {
-            use std::os::unix::ffi::OsStrExt;
-            Cow::Borrowed(path.as_os_str().as_bytes())
-        };
-
-        #[cfg(not(unix))]
-        let path_bytes: Cow<[u8]> = 
Cow::Owned(path.to_string_lossy().into_owned().into_bytes());
-
-        let_cxx_string!(p = path_bytes.as_ref());
+    /// Note: `path` must be valid UTF-8. On Unix, paths can contain arbitrary
+    /// bytes; such non-UTF8 paths return [`crate::Error::NonUtf8Path`].
+    pub fn save<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
+        let path_str = crate::path_to_utf8_str(path.as_ref())?;
+        let_cxx_string!(p = path_str);
         ffi::graphar::vertex_info_save(&self.0, &p)?;
         Ok(())
     }
 
     /// Dump this `VertexInfo` as YAML string.
-    pub fn dump(&self) -> Result<String, cxx::Exception> {
+    pub fn dump(&self) -> crate::Result<String> {
         let dumped: UniquePtr<cxx::CxxString> = 
ffi::graphar::vertex_info_dump(&self.0)?;
+        let dumped = dumped.as_ref().expect("vertex info dump should be 
valid");
         Ok(dumped.to_string())
     }
 }
@@ -221,6 +204,7 @@ impl VertexInfo {
 /// A builder for constructing a [`VertexInfo`].
 ///
 /// Defaults:
+/// - `property_groups = []`
 /// - `labels = []`
 /// - `prefix = ""` (GraphAr may set a default prefix based on type)
 /// - `version = None`
@@ -229,23 +213,19 @@ pub struct VertexInfoBuilder {
     chunk_size: i64,
     property_groups: PropertyGroupVector,
     labels: Vec<String>,
-    prefix: Vec<u8>,
+    prefix: String,
     version: Option<InfoVersion>,
 }
 
 impl VertexInfoBuilder {
     /// Create a new builder with required fields.
-    pub fn new<S: Into<String>>(
-        r#type: S,
-        chunk_size: i64,
-        property_groups: PropertyGroupVector,
-    ) -> Self {
+    pub fn new(r#type: impl Into<String>, chunk_size: i64) -> Self {
         Self {
             r#type: r#type.into(),
             chunk_size,
-            property_groups,
+            property_groups: PropertyGroupVector::new(),
             labels: Vec::new(),
-            prefix: Vec::new(),
+            prefix: String::new(),
             version: None,
         }
     }
@@ -273,8 +253,25 @@ impl VertexInfoBuilder {
     }
 
     /// Set the logical prefix.
-    pub fn prefix<P: AsRef<[u8]>>(mut self, prefix: P) -> Self {
-        self.prefix = prefix.as_ref().to_vec();
+    ///
+    /// This is a logical prefix string used by GraphAr (it is not a 
filesystem path).
+    pub fn prefix<P: AsRef<str>>(mut self, prefix: P) -> Self {
+        self.prefix = prefix.as_ref().to_owned();
+        self
+    }
+
+    /// Push a property group definition.
+    ///
+    /// This is a convenience helper when you want to build up property groups
+    /// incrementally.
+    pub fn push_property_group(mut self, property_group: PropertyGroup) -> 
Self {
+        self.property_groups.push(property_group);
+        self
+    }
+
+    /// Replace property groups with the given vector.
+    pub fn property_groups(mut self, property_groups: PropertyGroupVector) -> 
Self {
+        self.property_groups = property_groups;
         self
     }
 
@@ -299,7 +296,7 @@ impl VertexInfoBuilder {
     }
 
     /// Try to build a [`VertexInfo`].
-    pub fn try_build(self) -> Result<VertexInfo, cxx::Exception> {
+    pub fn try_build(self) -> crate::Result<VertexInfo> {
         let Self {
             r#type,
             chunk_size,
@@ -335,6 +332,12 @@ mod tests {
         groups
     }
 
+    fn make_property_group(prefix: &str, id_name: &str) -> PropertyGroup {
+        let mut props = PropertyVec::new();
+        props.emplace(PropertyBuilder::new(id_name, 
DataType::int64()).primary_key(true));
+        PropertyGroup::new(props, FileType::Parquet, prefix)
+    }
+
     #[test]
     fn test_vertex_info_try_new_error_paths() {
         let groups = PropertyGroupVector::new();
@@ -374,7 +377,8 @@ mod tests {
         let groups = make_property_groups();
 
         // Create `VertexInfo` using builder API.
-        let vertex_info = VertexInfo::builder("person", 1024, groups)
+        let vertex_info = VertexInfo::builder("person", 1024)
+            .property_groups(groups)
             .labels(vec!["l0".to_string()])
             .labels_from_iter(["l1", "l2"])
             .push_label("l3")
@@ -389,6 +393,10 @@ mod tests {
         assert!(vertex_info.version().is_some());
 
         let labels = vertex_info.labels();
+        assert!(
+            !labels.iter().any(|l| l == "l0"),
+            "expected labels() to reflect the latest setter (labels_from_iter)"
+        );
         assert_eq!(
             labels,
             vec!["l1".to_string(), "l2".to_string(), "l3".to_string()]
@@ -414,11 +422,35 @@ mod tests {
         assert_eq!(groups_iter.len(), 1);
     }
 
+    #[test]
+    fn test_vertex_info_builder_property_group_mutators_and_lookup_inputs() {
+        let pg1 = make_property_group("pg1/", "id1");
+        let pg2 = make_property_group("pg2/", "id2");
+
+        let vertex_info = VertexInfo::builder("person", 1024)
+            .push_property_group(pg1.clone())
+            .push_property_group(pg2.clone())
+            .build();
+
+        assert_eq!(vertex_info.property_group_num(), 2);
+        assert!(vertex_info.property_group("id1").is_some());
+        assert!(vertex_info.property_group(String::from("id2")).is_some());
+
+        // Replace property groups with an empty vector.
+        let vertex_info = VertexInfo::builder("person", 1024)
+            .property_groups(make_property_groups())
+            .property_groups(PropertyGroupVector::new())
+            .build();
+        assert_eq!(vertex_info.property_group_num(), 0);
+        assert!(vertex_info.property_group("id").is_none());
+    }
+
     #[test]
     fn test_vertex_info_property_group_lookups() {
         let groups = make_property_groups();
 
-        let vertex_info = VertexInfo::builder("person", 1024, groups)
+        let vertex_info = VertexInfo::builder("person", 1024)
+            .property_groups(groups)
             .prefix("person/")
             .version_opt(None)
             .build();
@@ -439,7 +471,8 @@ mod tests {
     fn test_vertex_info_dump_and_save() {
         let groups = make_property_groups();
 
-        let vertex_info = VertexInfo::builder("person", 1024, groups)
+        let vertex_info = VertexInfo::builder("person", 1024)
+            .property_groups(groups)
             .labels_from_iter(["l1"])
             .prefix("person/")
             .build();
@@ -473,7 +506,8 @@ mod tests {
         use std::os::unix::ffi::OsStringExt;
 
         let groups = make_property_groups();
-        let vertex_info = VertexInfo::builder("person", 1024, groups)
+        let vertex_info = VertexInfo::builder("person", 1024)
+            .property_groups(groups)
             .labels_from_iter(["l1"])
             .prefix("person/")
             .build();
@@ -485,12 +519,11 @@ mod tests {
             b"vertex_info_\xFF_non_utf8.yaml".to_vec(),
         ));
 
-        std::fs::File::create(&path).unwrap();
-        std::fs::remove_file(&path).unwrap();
-
-        vertex_info.save(&path).unwrap();
-        let metadata = std::fs::metadata(&path).unwrap();
-        assert!(metadata.is_file());
-        assert!(metadata.len() > 0);
+        let err = vertex_info.save(&path).err().unwrap();
+        assert!(
+            matches!(err, crate::Error::NonUtf8Path(_)),
+            "unexpected error: {err:?}"
+        );
+        assert!(std::fs::metadata(&path).is_err());
     }
 }
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 2c2d21d3..9a44a40a 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -19,17 +19,22 @@
 
 #![deny(missing_docs)]
 
-use cxx::CxxString;
+use std::path::Path;
 
 mod ffi;
 
-/// GraphAr metadata.
+/// Error types for this crate.
+pub mod error;
+/// GraphAr metadata information types.
 pub mod info;
 /// GraphAr property.
 pub mod property;
 /// GraphAr logical data types.
 pub mod types;
 
-fn cxx_string_to_string(value: &CxxString) -> String {
-    String::from_utf8_lossy(value.as_bytes()).into_owned()
+pub use error::{Error, Result};
+
+fn path_to_utf8_str(path: &Path) -> Result<&str> {
+    path.to_str()
+        .ok_or_else(|| Error::NonUtf8Path(path.to_owned()))
 }
diff --git a/rust/src/property.rs b/rust/src/property.rs
index c33f8d86..ef13fd52 100644
--- a/rust/src/property.rs
+++ b/rust/src/property.rs
@@ -17,7 +17,8 @@
 
 //! Rust bindings for GraphAr property types.
 
-use std::ops::{Deref, DerefMut};
+use std::ops::Deref;
+use std::pin::Pin;
 
 use crate::ffi;
 use crate::types::{Cardinality, DataType, FileType};
@@ -26,9 +27,13 @@ use cxx::{CxxVector, SharedPtr, UniquePtr, let_cxx_string};
 /// A property definition in the GraphAr schema.
 ///
 /// This is a thin wrapper around the C++ `graphar::Property`.
-pub struct Property(pub(crate) UniquePtr<ffi::graphar::Property>);
+pub struct Property(UniquePtr<ffi::graphar::Property>);
 
 impl Property {
+    pub(crate) fn as_ref(&self) -> &ffi::graphar::Property {
+        self.0.as_ref().expect("property should be valid")
+    }
+
     /// Create a new property definition.
     ///
     /// Note: In upstream GraphAr C++ (`graphar::Property` constructor), a 
primary key property is
@@ -55,18 +60,18 @@ impl Property {
 
     /// Return the property name.
     pub fn name(&self) -> String {
-        crate::cxx_string_to_string(ffi::graphar::property_get_name(&self.0))
+        ffi::graphar::property_get_name(self.as_ref()).to_string()
     }
 
     /// Return the property data type.
     pub fn data_type(&self) -> DataType {
-        let ty = ffi::graphar::property_get_type(&self.0);
+        let ty = ffi::graphar::property_get_type(self.as_ref());
         DataType(ty.clone())
     }
 
     /// Return whether this property is a primary key.
     pub fn is_primary(&self) -> bool {
-        ffi::graphar::property_is_primary(&self.0)
+        ffi::graphar::property_is_primary(self.as_ref())
     }
 
     /// Return whether this property is nullable.
@@ -74,12 +79,12 @@ impl Property {
     /// Note: In upstream GraphAr C++ (`graphar::Property` constructor), a 
primary key property is
     /// always treated as non-nullable, so this returns `false` when 
`is_primary()` is `true`.
     pub fn is_nullable(&self) -> bool {
-        ffi::graphar::property_is_nullable(&self.0)
+        ffi::graphar::property_is_nullable(self.as_ref())
     }
 
     /// Return the cardinality of this property.
     pub fn cardinality(&self) -> Cardinality {
-        ffi::graphar::property_get_cardinality(&self.0)
+        ffi::graphar::property_get_cardinality(self.as_ref())
     }
 }
 
@@ -164,22 +169,15 @@ impl Default for PropertyVec {
 
 impl Clone for PropertyVec {
     fn clone(&self) -> Self {
-        let src = self.0.as_ref().expect("properties vec should be valid");
-        Self(ffi::graphar::property_vec_clone(src))
+        Self(ffi::graphar::property_vec_clone(self.as_ref()))
     }
 }
 
 impl Deref for PropertyVec {
-    type Target = UniquePtr<CxxVector<ffi::graphar::Property>>;
+    type Target = CxxVector<ffi::graphar::Property>;
 
     fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl DerefMut for PropertyVec {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.0
+        self.as_ref()
     }
 }
 
@@ -191,7 +189,7 @@ impl PropertyVec {
 
     /// Push a property into the vector.
     pub fn push(&mut self, property: Property) {
-        ffi::graphar::property_vec_push_property(self.0.pin_mut(), property.0);
+        ffi::graphar::property_vec_push_property(self.pin_mut(), property.0);
     }
 
     /// Construct and append a property directly in the underlying C++ vector.
@@ -209,7 +207,7 @@ impl PropertyVec {
 
         let_cxx_string!(name = name.as_ref());
         ffi::graphar::property_vec_emplace_property(
-            self.0.pin_mut(),
+            self.pin_mut(),
             &name,
             data_type.0,
             is_primary,
@@ -217,12 +215,28 @@ impl PropertyVec {
             cardinality,
         );
     }
+
+    pub(crate) fn as_ref(&self) -> &CxxVector<ffi::graphar::Property> {
+        self.0.as_ref().expect("properties vec should be valid")
+    }
+
+    /// Borrow the underlying C++ vector mutably.
+    ///
+    /// Mutating APIs on `cxx::CxxVector` require a pinned mutable reference.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the underlying C++ vector pointer is null.
+    pub fn pin_mut(&mut self) -> Pin<&mut CxxVector<ffi::graphar::Property>> {
+        self.0.as_mut().expect("properties vec should be valid")
+    }
 }
 
 /// A group of properties stored in the same file(s).
 pub type PropertyGroup = ffi::SharedPropertyGroup;
 
 impl PropertyGroup {
+    /// Construct a `PropertyGroup` from a raw C++ shared pointer.
     pub(crate) fn from_inner(inner: SharedPtr<ffi::graphar::PropertyGroup>) -> 
Self {
         Self(inner)
     }
@@ -231,13 +245,9 @@ impl PropertyGroup {
     ///
     /// The `prefix` is a logical prefix string used by GraphAr (it is not a
     /// filesystem path).
-    pub fn new<S: AsRef<[u8]>>(properties: PropertyVec, file_type: FileType, 
prefix: S) -> Self {
-        let_cxx_string!(prefix = prefix);
-        let props = properties
-            .0
-            .as_ref()
-            .expect("properties vec should be valid");
-        let inner = ffi::graphar::CreatePropertyGroup(props, file_type, 
&prefix);
+    pub fn new<S: AsRef<str>>(properties: PropertyVec, file_type: FileType, 
prefix: S) -> Self {
+        let_cxx_string!(prefix = prefix.as_ref());
+        let inner = ffi::graphar::CreatePropertyGroup(properties.as_ref(), 
file_type, &prefix);
         Self(inner)
     }
 
@@ -271,22 +281,15 @@ impl Default for PropertyGroupVector {
 
 impl Clone for PropertyGroupVector {
     fn clone(&self) -> Self {
-        let src = self.0.as_ref().expect("property group vec should be valid");
-        Self(ffi::graphar::property_group_vec_clone(src))
+        Self(ffi::graphar::property_group_vec_clone(self.as_ref()))
     }
 }
 
 impl Deref for PropertyGroupVector {
-    type Target = UniquePtr<CxxVector<PropertyGroup>>;
+    type Target = CxxVector<PropertyGroup>;
 
     fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl DerefMut for PropertyGroupVector {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.0
+        self.as_ref()
     }
 }
 
@@ -298,7 +301,22 @@ impl PropertyGroupVector {
 
     /// Push a property group into the vector.
     pub fn push(&mut self, property_group: PropertyGroup) {
-        ffi::graphar::property_group_vec_push_property_group(self.0.pin_mut(), 
property_group.0);
+        ffi::graphar::property_group_vec_push_property_group(self.pin_mut(), 
property_group.0);
+    }
+
+    pub(crate) fn as_ref(&self) -> &CxxVector<PropertyGroup> {
+        self.0.as_ref().expect("property group vec should be valid")
+    }
+
+    /// Borrow the underlying C++ vector mutably.
+    ///
+    /// Mutating APIs on `cxx::CxxVector` require a pinned mutable reference.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the underlying C++ vector pointer is null.
+    pub fn pin_mut(&mut self) -> Pin<&mut CxxVector<PropertyGroup>> {
+        self.0.as_mut().expect("property group vec should be valid")
     }
 }
 
@@ -372,13 +390,13 @@ mod tests {
     #[test]
     fn test_property_vec_deref() {
         let mut vec = PropertyVec::default();
-        assert_eq!(vec.deref().len(), 0);
-        assert!(vec.deref().is_empty());
-        assert_eq!(vec.deref().capacity(), 0);
+        assert_eq!(vec.len(), 0);
+        assert_eq!(vec.as_ref().len(), 0);
+        assert!(vec.as_ref().is_empty());
+        assert_eq!(vec.as_ref().capacity(), 0);
 
-        // test `deref_mut`
         vec.pin_mut().reserve(2);
-        assert_eq!(vec.deref().capacity(), 2);
+        assert_eq!(vec.as_ref().capacity(), 2);
     }
 
     #[test]
@@ -406,11 +424,11 @@ mod tests {
     #[test]
     fn test_property_group_vector_default_and_deref() {
         let mut vec = PropertyGroupVector::default();
-        assert_eq!(vec.deref().len(), 0);
-        assert!(vec.deref().is_empty());
-        // test `deref_mut`
-        vec.deref_mut().pin_mut().reserve(2);
-        assert_eq!(vec.deref().capacity(), 2);
+        assert_eq!(vec.len(), 0);
+        assert_eq!(vec.as_ref().len(), 0);
+        assert!(vec.as_ref().is_empty());
+        vec.pin_mut().reserve(2);
+        assert_eq!(vec.as_ref().capacity(), 2);
     }
 
     #[test]
@@ -437,15 +455,15 @@ mod tests {
         let pg2 = PropertyGroup::new(props2, FileType::Parquet, "pg2/");
 
         let mut vec = PropertyGroupVector::default();
-        assert_eq!(vec.deref().len(), 0);
-        assert!(vec.deref().is_empty());
+        assert_eq!(vec.as_ref().len(), 0);
+        assert!(vec.as_ref().is_empty());
 
         vec.push(pg1);
-        assert_eq!(vec.deref().len(), 1);
-        assert!(!vec.deref().is_empty());
+        assert_eq!(vec.as_ref().len(), 1);
+        assert!(!vec.as_ref().is_empty());
 
         vec.push(pg2);
-        assert_eq!(vec.deref().len(), 2);
+        assert_eq!(vec.as_ref().len(), 2);
     }
 
     fn make_property_vec_for_clone() -> PropertyVec {
@@ -466,15 +484,15 @@ mod tests {
         let mut original = make_property_vec_for_clone();
         let cloned = original.clone();
 
-        assert_eq!(original.len(), 2);
-        assert_eq!(cloned.len(), 2);
+        assert_eq!(original.as_ref().len(), 2);
+        assert_eq!(cloned.as_ref().len(), 2);
 
         let id_prop = Property::new("id2", DataType::int64(), true, true, 
Cardinality::Single);
         original.push(id_prop);
-        assert_eq!(original.len(), 3);
+        assert_eq!(original.as_ref().len(), 3);
 
         // Mutating the original container should not affect the cloned one.
-        assert_eq!(cloned.len(), 2);
+        assert_eq!(cloned.as_ref().len(), 2);
 
         let pg = PropertyGroup::new(cloned, FileType::Parquet, "clone_check/");
         let mut names: Vec<_> = pg.properties().into_iter().map(|p| 
p.name()).collect();
@@ -497,21 +515,21 @@ mod tests {
         groups.push(pg2);
 
         let cloned = groups.clone();
-        assert_eq!(groups.len(), 2);
-        assert_eq!(cloned.len(), 2);
+        assert_eq!(groups.as_ref().len(), 2);
+        assert_eq!(cloned.as_ref().len(), 2);
 
-        assert!(cloned.get(0).unwrap().has_property("id1"));
-        assert!(cloned.get(1).unwrap().has_property("id2"));
+        assert!(cloned.as_ref().get(0).unwrap().has_property("id1"));
+        assert!(cloned.as_ref().get(1).unwrap().has_property("id2"));
 
         let mut props3 = PropertyVec::new();
         props3.emplace(PropertyBuilder::new("id3", 
DataType::int64()).primary_key(true));
         let pg3 = PropertyGroup::new(props3, FileType::Parquet, "pg3/");
         groups.push(pg3);
 
-        assert_eq!(groups.len(), 3);
-        assert_eq!(cloned.len(), 2);
+        assert_eq!(groups.as_ref().len(), 3);
+        assert_eq!(cloned.as_ref().len(), 2);
 
-        let cloned_props = cloned.get(0).unwrap().properties();
+        let cloned_props = cloned.as_ref().get(0).unwrap().properties();
         assert_eq!(cloned_props.len(), 1);
         assert_eq!(cloned_props[0].name(), "id1");
         assert_eq!(cloned_props[0].data_type().id(), Type::Int64);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to