blob: e8c55ed706702fea445f612aa3c572a03aeb28a8 [file]
// 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 graph-level metadata bindings.
use std::path::Path;
use cxx::{CxxVector, SharedPtr, UniquePtr, let_cxx_string};
use crate::ffi;
use super::{EdgeInfo, InfoVersion, VertexInfo};
/// Graph-level metadata (`graphar::GraphInfo`).
#[derive(Clone)]
pub struct GraphInfo(pub(crate) SharedPtr<ffi::graphar::GraphInfo>);
impl GraphInfo {
/// Create a builder for [`GraphInfo`].
pub fn builder(name: impl Into<String>) -> GraphInfoBuilder {
GraphInfoBuilder::new(name)
}
/// Create a new `GraphInfo`.
///
/// Panics if GraphAr rejects the inputs. Prefer [`GraphInfo::try_new`] if
/// you want to handle errors.
pub fn new<N: AsRef<str>, P: AsRef<str>>(
name: N,
vertex_infos: Vec<VertexInfo>,
edge_infos: Vec<EdgeInfo>,
labels: Vec<String>,
prefix: P,
version: Option<InfoVersion>,
) -> Self {
Self::try_new(name, vertex_infos, edge_infos, labels, prefix, version).unwrap()
}
/// Try to create a new `GraphInfo`.
pub fn try_new<N: AsRef<str>, P: AsRef<str>>(
name: N,
vertex_infos: Vec<VertexInfo>,
edge_infos: Vec<EdgeInfo>,
labels: Vec<String>,
prefix: P,
version: Option<InfoVersion>,
) -> crate::Result<Self> {
let_cxx_string!(name = name.as_ref());
let_cxx_string!(prefix = prefix.as_ref());
let mut vertex_info_vec = CxxVector::new();
vertex_info_vec.pin_mut().reserve(vertex_infos.len());
for vertex_info in vertex_infos {
ffi::graphar::vertex_info_vec_push_vertex_info(
vertex_info_vec.pin_mut(),
vertex_info.0,
);
}
let vertex_info_vec_ref = vertex_info_vec
.as_ref()
.expect("vertex info vector should be valid");
let mut edge_info_vec: UniquePtr<CxxVector<ffi::SharedEdgeInfo>> = CxxVector::new();
edge_info_vec.pin_mut().reserve(edge_infos.len());
for edge_info in edge_infos {
ffi::graphar::edge_info_vec_push_edge_info(edge_info_vec.pin_mut(), edge_info.0);
}
let edge_info_vec_ref = edge_info_vec
.as_ref()
.expect("edge info vector should be valid");
let version = version.map(|v| v.0).unwrap_or_else(SharedPtr::null);
let inner = ffi::graphar::create_graph_info(
&name,
vertex_info_vec_ref,
edge_info_vec_ref,
&labels,
&prefix,
version,
)?;
Ok(Self(inner))
}
/// Load graph metadata from 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 load<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
let path_str = crate::path_to_utf8_str(path.as_ref())?;
let_cxx_string!(p = path_str);
Ok(Self(ffi::graphar::load_graph_info(&p)?))
}
/// Return the graph name.
pub fn name(&self) -> String {
self.0.GetName().to_string()
}
/// Return graph labels.
pub fn labels(&self) -> Vec<String> {
let labels = self.0.GetLabels();
let mut out = Vec::with_capacity(labels.len());
for label in labels {
out.push(label.to_string());
}
out
}
/// Return the logical prefix.
pub fn prefix(&self) -> String {
self.0.GetPrefix().to_string()
}
/// 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 the vertex info with the given type.
///
/// Returns `None` if the type is not found.
pub fn vertex_info<S: AsRef<str>>(&self, r#type: S) -> Option<VertexInfo> {
let_cxx_string!(ty = r#type.as_ref());
let sp = self.0.GetVertexInfo(&ty);
if sp.is_null() {
None
} else {
Some(VertexInfo(sp))
}
}
/// Return the edge info with the given edge triplet.
///
/// Returns `None` if the edge triplet is not found.
pub fn edge_info<S1: AsRef<str>, S2: AsRef<str>, S3: AsRef<str>>(
&self,
src_type: S1,
edge_type: S2,
dst_type: S3,
) -> Option<EdgeInfo> {
let_cxx_string!(src = src_type.as_ref());
let_cxx_string!(edge = edge_type.as_ref());
let_cxx_string!(dst = dst_type.as_ref());
let sp = self.0.GetEdgeInfo(&src, &edge, &dst);
if sp.is_null() {
None
} else {
Some(EdgeInfo(sp))
}
}
/// Return the index of the vertex info with the given type.
///
/// Returns `None` if the type is not found.
pub fn vertex_info_index<S: AsRef<str>>(&self, r#type: S) -> Option<usize> {
let_cxx_string!(ty = r#type.as_ref());
ffi::graphar::graph_info_vertex_info_index(&self.0, &ty).into()
}
/// Return the index of the edge info with the given edge triplet.
///
/// Returns `None` if the edge triplet is not found.
pub fn edge_info_index<S1: AsRef<str>, S2: AsRef<str>, S3: AsRef<str>>(
&self,
src_type: S1,
edge_type: S2,
dst_type: S3,
) -> Option<usize> {
let_cxx_string!(src = src_type.as_ref());
let_cxx_string!(edge = edge_type.as_ref());
let_cxx_string!(dst = dst_type.as_ref());
ffi::graphar::graph_info_edge_info_index(&self.0, &src, &edge, &dst).into()
}
/// Return the number of vertex infos.
pub fn vertex_info_num(&self) -> usize {
self.0.VertexInfoNum()
}
/// Return the number of edge infos.
pub fn edge_info_num(&self) -> usize {
self.0.EdgeInfoNum()
}
/// Return vertex infos.
///
/// This allocates a new `Vec`. Prefer [`GraphInfo::vertex_infos_iter`]
/// if you only need to iterate.
pub fn vertex_infos(&self) -> Vec<VertexInfo> {
self.vertex_infos_iter().collect()
}
/// Iterate over vertex infos without allocating a `Vec`.
pub fn vertex_infos_iter(&self) -> impl Iterator<Item = VertexInfo> + '_ {
self.0
.GetVertexInfos()
.iter()
.map(|item| VertexInfo(item.0.clone()))
}
/// Return edge infos.
///
/// This allocates a new `Vec`. Prefer [`GraphInfo::edge_infos_iter`]
/// if you only need to iterate.
pub fn edge_infos(&self) -> Vec<EdgeInfo> {
self.edge_infos_iter().collect()
}
/// Iterate over edge infos without allocating a `Vec`.
pub fn edge_infos_iter(&self) -> impl Iterator<Item = EdgeInfo> + '_ {
self.0
.GetEdgeInfos()
.iter()
.map(|item| EdgeInfo(item.0.clone()))
}
/// Save this `GraphInfo` to the given path.
///
/// 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::graph_info_save(&self.0, &p)?;
Ok(())
}
/// Dump this `GraphInfo` as a YAML string.
pub fn dump(&self) -> crate::Result<String> {
let dumped = ffi::graphar::graph_info_dump(&self.0)?;
let dumped = dumped.as_ref().expect("graph info dump should be valid");
Ok(dumped.to_string())
}
}
/// A builder for constructing a [`GraphInfo`].
///
/// Defaults:
/// - `vertex_infos = []`
/// - `edge_infos = []`
/// - `labels = []`
/// - `prefix = "./"` (matches upstream `graphar::GraphInfo` default and keeps the graph info validated; empty prefix is considered invalid upstream)
/// - `version = None`
pub struct GraphInfoBuilder {
name: String,
vertex_infos: Vec<VertexInfo>,
edge_infos: Vec<EdgeInfo>,
labels: Vec<String>,
prefix: String,
version: Option<InfoVersion>,
}
impl GraphInfoBuilder {
/// Create a new builder with the required graph name.
///
/// The default `prefix` is `"./"` to match upstream GraphAr C++
/// `graphar::GraphInfo` behavior.
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
vertex_infos: Vec::new(),
edge_infos: Vec::new(),
labels: Vec::new(),
prefix: "./".to_string(),
version: None,
}
}
/// Push a single vertex info.
pub fn push_vertex_info(mut self, vertex_info: VertexInfo) -> Self {
self.vertex_infos.push(vertex_info);
self
}
/// Replace vertex infos with the given vector.
pub fn vertex_infos(mut self, vertex_infos: Vec<VertexInfo>) -> Self {
self.vertex_infos = vertex_infos;
self
}
/// Push a single edge info.
pub fn push_edge_info(mut self, edge_info: EdgeInfo) -> Self {
self.edge_infos.push(edge_info);
self
}
/// Replace edge infos with the given vector.
pub fn edge_infos(mut self, edge_infos: Vec<EdgeInfo>) -> Self {
self.edge_infos = edge_infos;
self
}
/// Set graph labels.
pub fn labels(mut self, labels: Vec<String>) -> Self {
self.labels = labels;
self
}
/// Set graph labels from a string iterator.
pub fn labels_from_iter<I, S>(mut self, labels: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.labels = labels.into_iter().map(|s| s.as_ref().to_string()).collect();
self
}
/// Push a single label.
pub fn push_label<S: Into<String>>(mut self, label: S) -> Self {
self.labels.push(label.into());
self
}
/// Set the logical prefix.
///
/// If unset, [`GraphInfoBuilder`] uses `"./"` by default.
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
}
/// Build a [`GraphInfo`].
///
/// Panics if GraphAr rejects the builder inputs. Prefer
/// [`GraphInfoBuilder::try_build`] if you want to handle errors.
pub fn build(self) -> GraphInfo {
self.try_build().unwrap()
}
/// Try to build a [`GraphInfo`].
pub fn try_build(self) -> crate::Result<GraphInfo> {
let Self {
name,
vertex_infos,
edge_infos,
labels,
prefix,
version,
} = self;
GraphInfo::try_new(name, vertex_infos, edge_infos, labels, prefix, version)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::info::{AdjListType, AdjacentList, AdjacentListVector};
use crate::property::{PropertyBuilder, PropertyGroup, PropertyGroupVector, PropertyVec};
use crate::types::{DataType, FileType};
use tempfile::tempdir;
fn make_vertex_info(r#type: &str) -> VertexInfo {
let mut props = PropertyVec::new();
props.emplace(PropertyBuilder::new("id", DataType::int64()).primary_key(true));
let group = PropertyGroup::new(props, FileType::Parquet, format!("{}_props/", r#type));
let mut groups = PropertyGroupVector::new();
groups.push(group);
VertexInfo::builder(r#type, 1024)
.property_groups(groups)
.labels_from_iter(["label"])
.prefix(format!("{}/", r#type))
.build()
}
fn make_edge_info(src_type: &str, edge_type: &str, dst_type: &str) -> EdgeInfo {
let mut adjacent_lists = AdjacentListVector::new();
adjacent_lists.push(AdjacentList::new(
AdjListType::OrderedBySource,
FileType::Parquet,
Some("ordered_by_source/"),
));
let mut props = PropertyVec::new();
props.emplace(PropertyBuilder::new("weight", DataType::float64()));
let mut groups = PropertyGroupVector::new();
groups.push(PropertyGroup::new(props, FileType::Parquet, "weight/"));
EdgeInfo::builder(src_type, edge_type, dst_type, 1024, 100, 100)
.directed(true)
.adjacent_lists(adjacent_lists)
.property_groups(groups)
.prefix(format!("{src_type}_{edge_type}_{dst_type}/"))
.build()
}
#[test]
fn test_graph_info_builder_roundtrip_getters_and_lookups() {
let person = make_vertex_info("person");
let software = make_vertex_info("software");
let knows = make_edge_info("person", "knows", "person");
let graph_info = GraphInfo::builder("my_graph")
.push_vertex_info(person)
.push_vertex_info(software)
.push_edge_info(knows)
.labels(vec!["l0".to_string()])
.labels_from_iter(["l1", "l2"])
.push_label("l3")
.prefix("graph/")
.version(InfoVersion::new(1).unwrap())
.build();
assert_eq!(graph_info.name(), "my_graph");
assert_eq!(graph_info.prefix(), "graph/");
assert!(graph_info.version().is_some());
assert_eq!(
graph_info.labels(),
vec!["l1".to_string(), "l2".to_string(), "l3".to_string()]
);
assert_eq!(graph_info.vertex_info_num(), 2);
assert_eq!(graph_info.edge_info_num(), 1);
assert_eq!(graph_info.vertex_info_index("person"), Some(0));
assert_eq!(graph_info.vertex_info_index("software"), Some(1));
assert_eq!(graph_info.vertex_info_index("missing"), None);
assert_eq!(
graph_info.edge_info_index("person", "knows", "person"),
Some(0)
);
assert_eq!(
graph_info.edge_info_index("person", "unknown", "person"),
None
);
assert!(graph_info.vertex_info("person").is_some());
assert!(graph_info.vertex_info(String::from("software")).is_some());
assert!(graph_info.vertex_info("missing").is_none());
assert!(graph_info.edge_info("person", "knows", "person").is_some());
assert!(
graph_info
.edge_info("person", "missing", "person")
.is_none()
);
let vertex_infos = graph_info.vertex_infos();
assert_eq!(vertex_infos.len(), 2);
let vertex_infos_iter: Vec<_> = graph_info.vertex_infos_iter().collect();
assert_eq!(vertex_infos_iter.len(), 2);
let edge_infos = graph_info.edge_infos();
assert_eq!(edge_infos.len(), 1);
let edge_infos_iter: Vec<_> = graph_info.edge_infos_iter().collect();
assert_eq!(edge_infos_iter.len(), 1);
}
#[test]
fn test_graph_info_builder_replace_collections_and_version_opt() {
let person = make_vertex_info("person");
let software = make_vertex_info("software");
let knows = make_edge_info("person", "knows", "person");
let graph_info = GraphInfo::builder("my_graph")
.push_vertex_info(make_vertex_info("tmp"))
.vertex_infos(vec![person, software])
.push_edge_info(make_edge_info("tmp", "tmp", "tmp"))
.edge_infos(vec![knows])
.version_opt(None)
.build();
assert_eq!(graph_info.vertex_info_num(), 2);
assert_eq!(graph_info.edge_info_num(), 1);
assert!(graph_info.version().is_none());
}
#[test]
fn test_graph_info_builder_default_prefix() {
let graph_info = GraphInfo::builder("my_graph")
.push_vertex_info(make_vertex_info("person"))
.push_edge_info(make_edge_info("person", "knows", "person"))
.build();
assert_eq!(graph_info.prefix(), "./");
}
#[test]
fn test_graph_info_dump_save_and_load() {
let graph_info = GraphInfo::builder("my_graph")
.push_vertex_info(make_vertex_info("person"))
.push_edge_info(make_edge_info("person", "knows", "person"))
.prefix("graph/")
.labels_from_iter(["person"])
.build();
let dumped = graph_info.dump().unwrap();
assert!(!dumped.trim().is_empty(), "dumped={dumped:?}");
assert!(dumped.contains("name: my_graph"), "dumped={dumped:?}");
assert!(dumped.contains("prefix: graph/"), "dumped={dumped:?}");
assert!(
dumped.contains("person_knows_person.edge.yaml"),
"dumped={dumped:?}"
);
assert!(dumped.contains("person.vertex.yaml"), "dumped={dumped:?}");
let dir = tempdir().unwrap();
let graph_path = dir.path().join("my_graph.graph.yaml");
graph_info.save(&graph_path).unwrap();
graph_info
.vertex_info("person")
.unwrap()
.save(dir.path().join("person.vertex.yaml"))
.unwrap();
graph_info
.edge_info("person", "knows", "person")
.unwrap()
.save(dir.path().join("person_knows_person.edge.yaml"))
.unwrap();
let loaded = GraphInfo::load(&graph_path).unwrap();
assert_eq!(loaded.name(), "my_graph");
assert_eq!(loaded.prefix(), "graph/");
assert_eq!(loaded.vertex_info_num(), 1);
assert_eq!(loaded.edge_info_num(), 1);
assert!(loaded.vertex_info("person").is_some());
assert!(loaded.edge_info("person", "knows", "person").is_some());
}
#[test]
fn test_graph_info_try_new_error_paths() {
let err = GraphInfo::try_new("", vec![], vec![], vec![], "graph/", None)
.err()
.unwrap();
let msg = err.to_string();
assert!(
msg.contains("CreateGraphInfo") && msg.contains("name must not be empty"),
"unexpected error message: {msg:?}"
);
}
#[test]
#[should_panic]
fn test_graph_info_new_panics_on_invalid_args() {
let _ = GraphInfo::new("", vec![], vec![], vec![], "graph/", None);
}
#[cfg(unix)]
#[test]
fn test_graph_info_save_and_load_non_utf8_path() {
use std::os::unix::ffi::OsStringExt;
let graph_info = GraphInfo::builder("my_graph")
.push_vertex_info(make_vertex_info("person"))
.push_edge_info(make_edge_info("person", "knows", "person"))
.build();
let dir = tempdir().unwrap();
let mut path = dir.path().to_path_buf();
path.push(std::ffi::OsString::from_vec(
b"graph_info_\xFF_non_utf8.yaml".to_vec(),
));
let err = graph_info.save(&path).err().unwrap();
assert!(
matches!(err, crate::Error::NonUtf8Path(_)),
"unexpected error: {err:?}"
);
let err = GraphInfo::load(&path).err().unwrap();
assert!(
matches!(err, crate::Error::NonUtf8Path(_)),
"unexpected error: {err:?}"
);
}
}