1use std::collections::BTreeMap;
43use std::sync::{Arc, OnceLock, RwLock};
44
45use datafusion::catalog::{CatalogProviderList, TableProvider};
46use datafusion::common::TableReference;
47use datafusion::execution::context::SessionState;
48use datafusion::prelude::{SessionConfig, SessionContext};
49
50mod define;
51mod describe;
52pub mod schema;
53
54pub use define::{
55 check_ddl, check_drop, check_drop_schema, check_replace, closest_name, create_graph,
56 drop_graph, unknown_graph, EdgeSpec, GraphSpec, VertexSpec,
57};
58pub use describe::{describe_graph, show_graphs};
59pub use schema::{edges_plan, edges_plan_from, nodes_plan, nodes_plan_from, GraphSchema};
60
61mod catalog_table;
62mod exec;
63pub mod functions;
64mod index;
65mod kernels;
66mod node;
67pub mod path_search;
68pub mod pattern;
69mod relation;
70mod session;
71pub mod traversal;
72
73pub use catalog_table::UrsaFunctions;
74pub use exec::{
75 GraphAlgorithmExec, GraphPlanner, GraphTraversalExec, KeepEdgeOrder, UrsaQueryPlanner,
76};
77pub use index::{index_builds, index_loads, FileIdentity, GraphIndex, IndexCache, Weights};
78pub use node::{GraphAlgorithmNode, GraphTraversalNode, Output};
79pub(crate) use session::{edge_order_rule, query_planner, register_functions, with_algorithms};
80
81#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct PropertyGraph {
88 pub name: String,
91 pub vertex_tables: Vec<VertexTable>,
92 pub edge_tables: Vec<EdgeTable>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct GraphDef {
100 pub name: String,
102 pub edges: EdgeTable,
103 pub vertices: Option<VertexTable>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct EdgeTable {
110 pub name: String,
112 pub table: TableReference,
113 pub source: String,
115 pub destination: String,
117 pub source_vertex: Option<String>,
119 pub destination_vertex: Option<String>,
121 pub labels: Vec<Label>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct VertexTable {
127 pub name: String,
129 pub table: TableReference,
130 pub key: String,
132 pub labels: Vec<Label>,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct Label {
138 pub name: String,
139 pub properties: Properties,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum Properties {
145 AllColumns { except: Vec<String> },
148 Columns(Vec<(String, String)>),
150 None,
152}
153
154impl Label {
155 pub fn default_for(element: &str) -> Self {
157 Label {
158 name: element.to_string(),
159 properties: Properties::AllColumns { except: Vec::new() },
160 }
161 }
162}
163
164fn labels_or_default<'a>(labels: &'a [Label], name: &str) -> std::borrow::Cow<'a, [Label]> {
166 if labels.is_empty() {
167 std::borrow::Cow::Owned(vec![Label::default_for(name)])
168 } else {
169 std::borrow::Cow::Borrowed(labels)
170 }
171}
172
173impl VertexTable {
174 pub fn new(table: impl Into<TableReference>, key: impl Into<String>) -> Self {
176 let table = table.into();
177 VertexTable {
178 name: table.table().to_string(),
179 table,
180 key: key.into(),
181 labels: Vec::new(),
182 }
183 }
184
185 pub fn labels(&self) -> std::borrow::Cow<'_, [Label]> {
187 labels_or_default(&self.labels, &self.name)
188 }
189
190 pub fn answers_to(&self, label: &str) -> bool {
192 self.name.eq_ignore_ascii_case(label)
193 || self
194 .labels
195 .iter()
196 .any(|l| l.name.eq_ignore_ascii_case(label))
197 }
198}
199
200impl EdgeTable {
201 pub fn new(
204 table: impl Into<TableReference>,
205 source: impl Into<String>,
206 destination: impl Into<String>,
207 ) -> Self {
208 let table = table.into();
209 EdgeTable {
210 name: table.table().to_string(),
211 table,
212 source: source.into(),
213 destination: destination.into(),
214 source_vertex: None,
215 destination_vertex: None,
216 labels: Vec::new(),
217 }
218 }
219
220 pub fn labels(&self) -> std::borrow::Cow<'_, [Label]> {
222 labels_or_default(&self.labels, &self.name)
223 }
224
225 pub fn answers_to(&self, label: &str) -> bool {
227 self.name.eq_ignore_ascii_case(label)
228 || self
229 .labels
230 .iter()
231 .any(|l| l.name.eq_ignore_ascii_case(label))
232 }
233}
234
235impl From<GraphDef> for PropertyGraph {
237 fn from(def: GraphDef) -> Self {
238 PropertyGraph {
239 name: def.name,
240 vertex_tables: def.vertices.into_iter().collect(),
241 edge_tables: vec![def.edges],
242 }
243 }
244}
245
246impl PropertyGraph {
247 pub fn tables(&self) -> impl Iterator<Item = &TableReference> {
249 self.vertex_tables
250 .iter()
251 .map(|v| &v.table)
252 .chain(self.edge_tables.iter().map(|e| &e.table))
253 }
254
255 pub fn vertex_table(&self, label: &str) -> Option<&VertexTable> {
257 self.vertex_tables
258 .iter()
259 .find(|v| v.name.eq_ignore_ascii_case(label))
260 .or_else(|| self.vertex_tables.iter().find(|v| v.answers_to(label)))
261 }
262
263 pub fn edge_table(&self, label: &str) -> Option<&EdgeTable> {
265 self.edge_tables
266 .iter()
267 .find(|e| e.name.eq_ignore_ascii_case(label))
268 .or_else(|| self.edge_tables.iter().find(|e| e.answers_to(label)))
269 }
270
271 pub fn endpoint_vertex(&self, edge: &EdgeTable, source: bool) -> Option<&VertexTable> {
274 let named = if source {
275 &edge.source_vertex
276 } else {
277 &edge.destination_vertex
278 };
279 match named {
280 Some(name) => self.vertex_tables.iter().find(|v| &v.name == name),
281 None if self.vertex_tables.len() == 1 => self.vertex_tables.first(),
282 None => None,
283 }
284 }
285
286 pub fn pair(
291 &self,
292 vertex: Option<&str>,
293 edge: Option<&str>,
294 ) -> datafusion::error::Result<GraphDef> {
295 use datafusion::error::DataFusionError;
296 let plan_err = |m: String| Err(DataFusionError::Plan(m));
297 let edges = match edge {
298 Some(label) => match self.edge_table(label) {
299 Some(e) => e,
300 None => {
301 let names: Vec<&str> =
302 self.edge_tables.iter().map(|e| e.name.as_str()).collect();
303 return plan_err(format!(
304 "graph '{}' has no edge table '{label}'; its edge tables are {}",
305 self.name,
306 if names.is_empty() {
307 "none".to_string()
308 } else {
309 names.join(", ")
310 }
311 ));
312 }
313 },
314 None => match self.edge_tables.as_slice() {
315 [only] => only,
316 [] => {
317 return plan_err(format!(
318 "graph '{}' has no edge tables, so there is nothing for an algorithm to \
319 run on",
320 self.name
321 ))
322 }
323 several => {
324 let names: Vec<&str> = several.iter().map(|e| e.name.as_str()).collect();
325 return plan_err(format!(
326 "graph '{}' has {} edge tables ({}); name the vertex and edge table to \
327 run on, as in pagerank({}, <vertex table>, <edge table>)",
328 self.name,
329 several.len(),
330 names.join(", "),
331 self.name
332 ));
333 }
334 },
335 };
336 let source = self.endpoint_vertex(edges, true);
337 let destination = self.endpoint_vertex(edges, false);
338 if let (Some(s), Some(d)) = (source, destination) {
339 if s.name != d.name {
340 return plan_err(format!(
341 "edge table '{}' of graph '{}' goes from vertex table '{}' to '{}'; an \
342 algorithm runs on edges between one vertex table",
343 edges.name, self.name, s.name, d.name
344 ));
345 }
346 }
347 let vertices = source.or(destination);
348 if let Some(label) = vertex {
349 let Some(asked) = self.vertex_table(label) else {
350 let names: Vec<&str> = self.vertex_tables.iter().map(|v| v.name.as_str()).collect();
351 return plan_err(format!(
352 "graph '{}' has no vertex table '{label}'; its vertex tables are {}",
353 self.name,
354 if names.is_empty() {
355 "none".to_string()
356 } else {
357 names.join(", ")
358 }
359 ));
360 };
361 if vertices.is_some_and(|v| v.name != asked.name) {
362 return plan_err(format!(
363 "edge table '{}' of graph '{}' connects vertex table '{}', not '{}'",
364 edges.name,
365 self.name,
366 vertices.map(|v| v.name.as_str()).unwrap_or_default(),
367 asked.name
368 ));
369 }
370 return Ok(GraphDef {
371 name: self.name.clone(),
372 edges: edges.clone(),
373 vertices: Some(asked.clone()),
374 });
375 }
376 Ok(GraphDef {
377 name: self.name.clone(),
378 edges: edges.clone(),
379 vertices: vertices.cloned(),
380 })
381 }
382
383 pub fn default_pair(&self) -> Option<GraphDef> {
386 if self.vertex_tables.len() > 1 {
387 return None;
388 }
389 self.pair(None, None).ok()
390 }
391}
392
393#[derive(Debug, Clone)]
395pub struct FileTable {
396 pub table: TableReference,
398 pub provider: Arc<dyn TableProvider>,
399}
400
401#[derive(Debug, Default)]
404pub struct GraphRegistry {
405 graphs: RwLock<BTreeMap<String, Arc<PropertyGraph>>>,
406 files: RwLock<BTreeMap<String, Vec<FileTable>>>,
408 catalogs: OnceLock<Arc<dyn CatalogProviderList>>,
411}
412
413impl GraphRegistry {
414 pub fn get(&self, name: &str) -> Option<Arc<PropertyGraph>> {
416 self.graphs
417 .read()
418 .unwrap_or_else(|e| e.into_inner())
419 .get(name)
420 .cloned()
421 }
422
423 pub fn all(&self) -> Vec<Arc<PropertyGraph>> {
425 self.graphs
426 .read()
427 .unwrap_or_else(|e| e.into_inner())
428 .values()
429 .cloned()
430 .collect()
431 }
432
433 pub fn put(&self, def: impl Into<PropertyGraph>) -> Option<Arc<PropertyGraph>> {
435 self.put_with_files(def.into(), Vec::new())
436 }
437
438 pub fn put_with_files(
440 &self,
441 def: PropertyGraph,
442 files: Vec<FileTable>,
443 ) -> Option<Arc<PropertyGraph>> {
444 let mut graphs = self.graphs.write().unwrap_or_else(|e| e.into_inner());
445 let mut all_files = self.files.write().unwrap_or_else(|e| e.into_inner());
446 if files.is_empty() {
447 all_files.remove(&def.name);
448 } else {
449 all_files.insert(def.name.clone(), files);
450 }
451 graphs.insert(def.name.clone(), Arc::new(def))
452 }
453
454 pub fn remove(&self, name: &str) -> Option<Arc<PropertyGraph>> {
456 let mut graphs = self.graphs.write().unwrap_or_else(|e| e.into_inner());
457 self.files
458 .write()
459 .unwrap_or_else(|e| e.into_inner())
460 .remove(name);
461 graphs.remove(name)
462 }
463
464 pub fn file_table(
467 &self,
468 graph: &str,
469 table: &TableReference,
470 ) -> Option<Arc<dyn TableProvider>> {
471 self.files
472 .read()
473 .unwrap_or_else(|e| e.into_inner())
474 .get(graph)?
475 .iter()
476 .find(|f| &f.table == table)
477 .map(|f| Arc::clone(&f.provider))
478 }
479
480 pub(crate) fn set_catalogs(&self, catalogs: Arc<dyn CatalogProviderList>) {
482 let _ = self.catalogs.set(catalogs);
483 }
484
485 pub(crate) fn catalogs(&self) -> Option<&Arc<dyn CatalogProviderList>> {
486 self.catalogs.get()
487 }
488
489 pub fn names(&self) -> Vec<String> {
491 self.graphs
492 .read()
493 .unwrap_or_else(|e| e.into_inner())
494 .keys()
495 .cloned()
496 .collect()
497 }
498}
499
500pub(crate) fn with_registry(config: SessionConfig) -> SessionConfig {
502 config.with_extension(Arc::new(GraphRegistry::default()))
503}
504
505pub fn registry(state: &SessionState) -> Arc<GraphRegistry> {
507 state
508 .config()
509 .get_extension::<GraphRegistry>()
510 .expect("an Ursa session has a graph registry (built by ursa_engine::new_session)")
511}
512
513pub fn registry_of(ctx: &SessionContext) -> Arc<GraphRegistry> {
515 registry(&ctx.state())
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn a_session_has_an_empty_registry() {
524 let ctx = crate::new_session();
525 let graphs = registry_of(&ctx);
526 assert!(graphs.all().is_empty());
527 graphs.put(PropertyGraph {
528 name: "g".into(),
529 vertex_tables: Vec::new(),
530 edge_tables: vec![EdgeTable::new(TableReference::bare("e"), "a", "b")],
531 });
532 assert_eq!(registry_of(&ctx).names(), vec!["g".to_string()]);
534 }
535}