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]