use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock, RwLock};
use datafusion::catalog::{CatalogProviderList, TableProvider};
use datafusion::common::TableReference;
use datafusion::execution::context::SessionState;
use datafusion::prelude::{SessionConfig, SessionContext};
mod define;
mod describe;
pub mod schema;
pub use define::{
check_ddl, check_drop, check_drop_schema, check_replace, closest_name, create_graph,
drop_graph, unknown_graph, EdgeSpec, GraphSpec, VertexSpec,
};
pub use describe::{describe_graph, show_graphs};
pub use schema::{edges_plan, edges_plan_from, nodes_plan, nodes_plan_from, GraphSchema};
mod catalog_table;
mod exec;
pub mod functions;
mod index;
mod kernels;
mod node;
pub mod path_search;
pub mod pattern;
mod relation;
mod session;
pub mod traversal;
pub use catalog_table::UrsaFunctions;
pub use exec::{
GraphAlgorithmExec, GraphPlanner, GraphTraversalExec, KeepEdgeOrder, UrsaQueryPlanner,
};
pub use index::{
index_builds, index_loads, BuildInfo, FileIdentity, GraphIndex, IndexCache, Weights,
};
pub use node::{GraphAlgorithmNode, GraphTraversalNode, Output};
pub(crate) use session::{edge_order_rule, query_planner, register_functions, with_algorithms};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropertyGraph {
pub name: String,
pub vertex_tables: Vec<VertexTable>,
pub edge_tables: Vec<EdgeTable>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphDef {
pub name: String,
pub edges: EdgeTable,
pub vertices: Option<VertexTable>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EdgeTable {
pub name: String,
pub table: TableReference,
pub source: String,
pub destination: String,
pub source_vertex: Option<String>,
pub destination_vertex: Option<String>,
pub labels: Vec<Label>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VertexTable {
pub name: String,
pub table: TableReference,
pub key: String,
pub labels: Vec<Label>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Label {
pub name: String,
pub properties: Properties,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Properties {
AllColumns { except: Vec<String> },
Columns(Vec<(String, String)>),
None,
}
impl Label {
pub fn default_for(element: &str) -> Self {
Label {
name: element.to_string(),
properties: Properties::AllColumns { except: Vec::new() },
}
}
}
fn labels_or_default<'a>(labels: &'a [Label], name: &str) -> std::borrow::Cow<'a, [Label]> {
if labels.is_empty() {
std::borrow::Cow::Owned(vec![Label::default_for(name)])
} else {
std::borrow::Cow::Borrowed(labels)
}
}
impl VertexTable {
pub fn new(table: impl Into<TableReference>, key: impl Into<String>) -> Self {
let table = table.into();
VertexTable {
name: table.table().to_string(),
table,
key: key.into(),
labels: Vec::new(),
}
}
pub fn labels(&self) -> std::borrow::Cow<'_, [Label]> {
labels_or_default(&self.labels, &self.name)
}
pub fn answers_to(&self, label: &str) -> bool {
self.name.eq_ignore_ascii_case(label)
|| self
.labels
.iter()
.any(|l| l.name.eq_ignore_ascii_case(label))
}
}
impl EdgeTable {
pub fn new(
table: impl Into<TableReference>,
source: impl Into<String>,
destination: impl Into<String>,
) -> Self {
let table = table.into();
EdgeTable {
name: table.table().to_string(),
table,
source: source.into(),
destination: destination.into(),
source_vertex: None,
destination_vertex: None,
labels: Vec::new(),
}
}
pub fn labels(&self) -> std::borrow::Cow<'_, [Label]> {
labels_or_default(&self.labels, &self.name)
}
pub fn answers_to(&self, label: &str) -> bool {
self.name.eq_ignore_ascii_case(label)
|| self
.labels
.iter()
.any(|l| l.name.eq_ignore_ascii_case(label))
}
}
impl From<GraphDef> for PropertyGraph {
fn from(def: GraphDef) -> Self {
PropertyGraph {
name: def.name,
vertex_tables: def.vertices.into_iter().collect(),
edge_tables: vec![def.edges],
}
}
}
impl PropertyGraph {
pub fn tables(&self) -> impl Iterator<Item = &TableReference> {
self.vertex_tables
.iter()
.map(|v| &v.table)
.chain(self.edge_tables.iter().map(|e| &e.table))
}
pub fn vertex_table(&self, label: &str) -> Option<&VertexTable> {
self.vertex_tables
.iter()
.find(|v| v.name.eq_ignore_ascii_case(label))
.or_else(|| self.vertex_tables.iter().find(|v| v.answers_to(label)))
}
pub fn edge_table(&self, label: &str) -> Option<&EdgeTable> {
self.edge_tables
.iter()
.find(|e| e.name.eq_ignore_ascii_case(label))
.or_else(|| self.edge_tables.iter().find(|e| e.answers_to(label)))
}
pub fn endpoint_vertex(&self, edge: &EdgeTable, source: bool) -> Option<&VertexTable> {
let named = if source {
&edge.source_vertex
} else {
&edge.destination_vertex
};
match named {
Some(name) => self.vertex_tables.iter().find(|v| &v.name == name),
None if self.vertex_tables.len() == 1 => self.vertex_tables.first(),
None => None,
}
}
pub fn pair(
&self,
vertex: Option<&str>,
edge: Option<&str>,
) -> datafusion::error::Result<GraphDef> {
use datafusion::error::DataFusionError;
let plan_err = |m: String| Err(DataFusionError::Plan(m));
let edges = match edge {
Some(label) => match self.edge_table(label) {
Some(e) => e,
None => {
let names: Vec<&str> =
self.edge_tables.iter().map(|e| e.name.as_str()).collect();
return plan_err(format!(
"graph '{}' has no edge table '{label}'; its edge tables are {}",
self.name,
if names.is_empty() {
"none".to_string()
} else {
names.join(", ")
}
));
}
},
None => match self.edge_tables.as_slice() {
[only] => only,
[] => {
return plan_err(format!(
"graph '{}' has no edge tables, so there is nothing for an algorithm to \
run on",
self.name
))
}
several => {
let names: Vec<&str> = several.iter().map(|e| e.name.as_str()).collect();
return plan_err(format!(
"graph '{}' has {} edge tables ({}); name the vertex and edge table to \
run on, as in pagerank({}, <vertex table>, <edge table>)",
self.name,
several.len(),
names.join(", "),
self.name
));
}
},
};
let source = self.endpoint_vertex(edges, true);
let destination = self.endpoint_vertex(edges, false);
if let (Some(s), Some(d)) = (source, destination) {
if s.name != d.name {
return plan_err(format!(
"edge table '{}' of graph '{}' goes from vertex table '{}' to '{}'; an \
algorithm runs on edges between one vertex table",
edges.name, self.name, s.name, d.name
));
}
}
let vertices = source.or(destination);
if let Some(label) = vertex {
let Some(asked) = self.vertex_table(label) else {
let names: Vec<&str> = self.vertex_tables.iter().map(|v| v.name.as_str()).collect();
return plan_err(format!(
"graph '{}' has no vertex table '{label}'; its vertex tables are {}",
self.name,
if names.is_empty() {
"none".to_string()
} else {
names.join(", ")
}
));
};
if vertices.is_some_and(|v| v.name != asked.name) {
return plan_err(format!(
"edge table '{}' of graph '{}' connects vertex table '{}', not '{}'",
edges.name,
self.name,
vertices.map(|v| v.name.as_str()).unwrap_or_default(),
asked.name
));
}
return Ok(GraphDef {
name: self.name.clone(),
edges: edges.clone(),
vertices: Some(asked.clone()),
});
}
Ok(GraphDef {
name: self.name.clone(),
edges: edges.clone(),
vertices: vertices.cloned(),
})
}
pub fn default_pair(&self) -> Option<GraphDef> {
if self.vertex_tables.len() > 1 {
return None;
}
self.pair(None, None).ok()
}
}
#[derive(Debug, Clone)]
pub struct FileTable {
pub table: TableReference,
pub provider: Arc<dyn TableProvider>,
}
#[derive(Debug, Default)]
pub struct GraphRegistry {
graphs: RwLock<BTreeMap<String, Arc<PropertyGraph>>>,
files: RwLock<BTreeMap<String, Vec<FileTable>>>,
catalogs: OnceLock<Arc<dyn CatalogProviderList>>,
}
impl GraphRegistry {
pub fn get(&self, name: &str) -> Option<Arc<PropertyGraph>> {
self.graphs
.read()
.unwrap_or_else(|e| e.into_inner())
.get(name)
.cloned()
}
pub fn all(&self) -> Vec<Arc<PropertyGraph>> {
self.graphs
.read()
.unwrap_or_else(|e| e.into_inner())
.values()
.cloned()
.collect()
}
pub fn put(&self, def: impl Into<PropertyGraph>) -> Option<Arc<PropertyGraph>> {
self.put_with_files(def.into(), Vec::new())
}
pub fn put_with_files(
&self,
def: PropertyGraph,
files: Vec<FileTable>,
) -> Option<Arc<PropertyGraph>> {
let mut graphs = self.graphs.write().unwrap_or_else(|e| e.into_inner());
let mut all_files = self.files.write().unwrap_or_else(|e| e.into_inner());
if files.is_empty() {
all_files.remove(&def.name);
} else {
all_files.insert(def.name.clone(), files);
}
graphs.insert(def.name.clone(), Arc::new(def))
}
pub fn remove(&self, name: &str) -> Option<Arc<PropertyGraph>> {
let mut graphs = self.graphs.write().unwrap_or_else(|e| e.into_inner());
self.files
.write()
.unwrap_or_else(|e| e.into_inner())
.remove(name);
graphs.remove(name)
}
pub fn file_table(
&self,
graph: &str,
table: &TableReference,
) -> Option<Arc<dyn TableProvider>> {
self.files
.read()
.unwrap_or_else(|e| e.into_inner())
.get(graph)?
.iter()
.find(|f| &f.table == table)
.map(|f| Arc::clone(&f.provider))
}
pub(crate) fn set_catalogs(&self, catalogs: Arc<dyn CatalogProviderList>) {
let _ = self.catalogs.set(catalogs);
}
pub(crate) fn catalogs(&self) -> Option<&Arc<dyn CatalogProviderList>> {
self.catalogs.get()
}
pub fn names(&self) -> Vec<String> {
self.graphs
.read()
.unwrap_or_else(|e| e.into_inner())
.keys()
.cloned()
.collect()
}
}
pub(crate) fn with_registry(config: SessionConfig) -> SessionConfig {
config.with_extension(Arc::new(GraphRegistry::default()))
}
pub fn registry(state: &SessionState) -> Arc<GraphRegistry> {
state
.config()
.get_extension::<GraphRegistry>()
.expect("an Ursa session has a graph registry (built by ursa_engine::new_session)")
}
pub fn registry_of(ctx: &SessionContext) -> Arc<GraphRegistry> {
registry(&ctx.state())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_session_has_an_empty_registry() {
let ctx = crate::new_session();
let graphs = registry_of(&ctx);
assert!(graphs.all().is_empty());
graphs.put(PropertyGraph {
name: "g".into(),
vertex_tables: Vec::new(),
edge_tables: vec![EdgeTable::new(TableReference::bare("e"), "a", "b")],
});
assert_eq!(registry_of(&ctx).names(), vec!["g".to_string()]);
}
}