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::{
78 index_builds, index_loads, BuildInfo, FileIdentity, GraphIndex, IndexCache, Weights,
79};
80pub use node::{GraphAlgorithmNode, GraphTraversalNode, Output};
81pub(crate) use session::{edge_order_rule, query_planner, register_functions, with_algorithms};
82
83#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct PropertyGraph {
90 pub name: String,
93 pub vertex_tables: Vec<VertexTable>,
94 pub edge_tables: Vec<EdgeTable>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct GraphDef {
102 pub name: String,
104 pub edges: EdgeTable,
105 pub vertices: Option<VertexTable>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct EdgeTable {
112 pub name: String,
114 pub table: TableReference,
115 pub source: String,
117 pub destination: String,
119 pub source_vertex: Option<String>,
121 pub destination_vertex: Option<String>,
123 pub labels: Vec<Label>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct VertexTable {
129 pub name: String,
131 pub table: TableReference,
132 pub key: String,
134 pub labels: Vec<Label>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct Label {
140 pub name: String,
141 pub properties: Properties,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum Properties {
147 AllColumns { except: Vec<String> },
150 Columns(Vec<(String, String)>),
152 None,
154}
155
156impl Label {
157 pub fn default_for(element: &str) -> Self {
159 Label {
160 name: element.to_string(),
161 properties: Properties::AllColumns { except: Vec::new() },
162 }
163 }
164}
165
166fn labels_or_default<'a>(labels: &'a [Label], name: &str) -> std::borrow::Cow<'a, [Label]> {
168 if labels.is_empty() {
169 std::borrow::Cow::Owned(vec![Label::default_for(name)])
170 } else {
171 std::borrow::Cow::Borrowed(labels)
172 }
173}
174
175impl VertexTable {
176 pub fn new(table: impl Into<TableReference>, key: impl Into<String>) -> Self {
178 let table = table.into();
179 VertexTable {
180 name: table.table().to_string(),
181 table,
182 key: key.into(),
183 labels: Vec::new(),
184 }
185 }
186
187 pub fn labels(&self) -> std::borrow::Cow<'_, [Label]> {
189 labels_or_default(&self.labels, &self.name)
190 }
191
192 pub fn answers_to(&self, label: &str) -> bool {
194 self.name.eq_ignore_ascii_case(label)
195 || self
196 .labels
197 .iter()
198 .any(|l| l.name.eq_ignore_ascii_case(label))
199 }
200}
201
202impl EdgeTable {
203 pub fn new(
206 table: impl Into<TableReference>,
207 source: impl Into<String>,
208 destination: impl Into<String>,
209 ) -> Self {
210 let table = table.into();
211 EdgeTable {
212 name: table.table().to_string(),
213 table,
214 source: source.into(),
215 destination: destination.into(),
216 source_vertex: None,
217 destination_vertex: None,
218 labels: Vec::new(),
219 }
220 }
221
222 pub fn labels(&self) -> std::borrow::Cow<'_, [Label]> {
224 labels_or_default(&self.labels, &self.name)
225 }
226
227 pub fn answers_to(&self, label: &str) -> bool {
229 self.name.eq_ignore_ascii_case(label)
230 || self
231 .labels
232 .iter()
233 .any(|l| l.name.eq_ignore_ascii_case(label))
234 }
235}
236
237impl From<GraphDef> for PropertyGraph {
239 fn from(def: GraphDef) -> Self {
240 PropertyGraph {
241 name: def.name,
242 vertex_tables: def.vertices.into_iter().collect(),
243 edge_tables: vec![def.edges],
244 }
245 }
246}
247
248impl PropertyGraph {
249 pub fn tables(&self) -> impl Iterator<Item = &TableReference> {
251 self.vertex_tables
252 .iter()
253 .map(|v| &v.table)
254 .chain(self.edge_tables.iter().map(|e| &e.table))
255 }
256
257 pub fn vertex_table(&self, label: &str) -> Option<&VertexTable> {
259 self.vertex_tables
260 .iter()
261 .find(|v| v.name.eq_ignore_ascii_case(label))
262 .or_else(|| self.vertex_tables.iter().find(|v| v.answers_to(label)))
263 }
264
265 pub fn edge_table(&self, label: &str) -> Option<&EdgeTable> {
267 self.edge_tables
268 .iter()
269 .find(|e| e.name.eq_ignore_ascii_case(label))
270 .or_else(|| self.edge_tables.iter().find(|e| e.answers_to(label)))
271 }
272
273 pub fn endpoint_vertex(&self, edge: &EdgeTable, source: bool) -> Option<&VertexTable> {
276 let named = if source {
277 &edge.source_vertex
278 } else {
279 &edge.destination_vertex
280 };
281 match named {
282 Some(name) => self.vertex_tables.iter().find(|v| &v.name == name),
283 None if self.vertex_tables.len() == 1 => self.vertex_tables.first(),
284 None => None,
285 }
286 }
287
288 pub fn pair(
293 &self,
294 vertex: Option<&str>,
295 edge: Option<&str>,
296 ) -> datafusion::error::Result<GraphDef> {
297 use datafusion::error::DataFusionError;
298 let plan_err = |m: String| Err(DataFusionError::Plan(m));
299 let edges = match edge {
300 Some(label) => match self.edge_table(label) {
301 Some(e) => e,
302 None => {
303 let names: Vec<&str> =
304 self.edge_tables.iter().map(|e| e.name.as_str()).collect();
305 return plan_err(format!(
306 "graph '{}' has no edge table '{label}'; its edge tables are {}",
307 self.name,
308 if names.is_empty() {
309 "none".to_string()
310 } else {
311 names.join(", ")
312 }
313 ));
314 }
315 },
316 None => match self.edge_tables.as_slice() {
317 [only] => only,
318 [] => {
319 return plan_err(format!(
320 "graph '{}' has no edge tables, so there is nothing for an algorithm to \
321 run on",
322 self.name
323 ))
324 }
325 several => {
326 let names: Vec<&str> = several.iter().map(|e| e.name.as_str()).collect();
327 return plan_err(format!(
328 "graph '{}' has {} edge tables ({}); name the vertex and edge table to \
329 run on, as in pagerank({}, <vertex table>, <edge table>)",
330 self.name,
331 several.len(),
332 names.join(", "),
333 self.name
334 ));
335 }
336 },
337 };
338 let source = self.endpoint_vertex(edges, true);
339 let destination = self.endpoint_vertex(edges, false);
340 if let (Some(s), Some(d)) = (source, destination) {
341 if s.name != d.name {
342 return plan_err(format!(
343 "edge table '{}' of graph '{}' goes from vertex table '{}' to '{}'; an \
344 algorithm runs on edges between one vertex table",
345 edges.name, self.name, s.name, d.name
346 ));
347 }
348 }
349 let vertices = source.or(destination);
350 if let Some(label) = vertex {
351 let Some(asked) = self.vertex_table(label) else {
352 let names: Vec<&str> = self.vertex_tables.iter().map(|v| v.name.as_str()).collect();
353 return plan_err(format!(
354 "graph '{}' has no vertex table '{label}'; its vertex tables are {}",
355 self.name,
356 if names.is_empty() {
357 "none".to_string()
358 } else {
359 names.join(", ")
360 }
361 ));
362 };
363 if vertices.is_some_and(|v| v.name != asked.name) {
364 return plan_err(format!(
365 "edge table '{}' of graph '{}' connects vertex table '{}', not '{}'",
366 edges.name,
367 self.name,
368 vertices.map(|v| v.name.as_str()).unwrap_or_default(),
369 asked.name
370 ));
371 }
372 return Ok(GraphDef {
373 name: self.name.clone(),
374 edges: edges.clone(),
375 vertices: Some(asked.clone()),
376 });
377 }
378 Ok(GraphDef {
379 name: self.name.clone(),
380 edges: edges.clone(),
381 vertices: vertices.cloned(),
382 })
383 }
384
385 pub fn default_pair(&self) -> Option<GraphDef> {
388 if self.vertex_tables.len() > 1 {
389 return None;
390 }
391 self.pair(None, None).ok()
392 }
393}
394
395#[derive(Debug, Clone)]
397pub struct FileTable {
398 pub table: TableReference,
400 pub provider: Arc<dyn TableProvider>,
401}
402
403#[derive(Debug, Default)]
406pub struct GraphRegistry {
407 graphs: RwLock<BTreeMap<String, Arc<PropertyGraph>>>,
408 files: RwLock<BTreeMap<String, Vec<FileTable>>>,
410 catalogs: OnceLock<Arc<dyn CatalogProviderList>>,
413}
414
415impl GraphRegistry {
416 pub fn get(&self, name: &str) -> Option<Arc<PropertyGraph>> {
418 self.graphs
419 .read()
420 .unwrap_or_else(|e| e.into_inner())
421 .get(name)
422 .cloned()
423 }
424
425 pub fn all(&self) -> Vec<Arc<PropertyGraph>> {
427 self.graphs
428 .read()
429 .unwrap_or_else(|e| e.into_inner())
430 .values()
431 .cloned()
432 .collect()
433 }
434
435 pub fn put(&self, def: impl Into<PropertyGraph>) -> Option<Arc<PropertyGraph>> {
437 self.put_with_files(def.into(), Vec::new())
438 }
439
440 pub fn put_with_files(
442 &self,
443 def: PropertyGraph,
444 files: Vec<FileTable>,
445 ) -> Option<Arc<PropertyGraph>> {
446 let mut graphs = self.graphs.write().unwrap_or_else(|e| e.into_inner());
447 let mut all_files = self.files.write().unwrap_or_else(|e| e.into_inner());
448 if files.is_empty() {
449 all_files.remove(&def.name);
450 } else {
451 all_files.insert(def.name.clone(), files);
452 }
453 graphs.insert(def.name.clone(), Arc::new(def))
454 }
455
456 pub fn remove(&self, name: &str) -> Option<Arc<PropertyGraph>> {
458 let mut graphs = self.graphs.write().unwrap_or_else(|e| e.into_inner());
459 self.files
460 .write()
461 .unwrap_or_else(|e| e.into_inner())
462 .remove(name);
463 graphs.remove(name)
464 }
465
466 pub fn file_table(
469 &self,
470 graph: &str,
471 table: &TableReference,
472 ) -> Option<Arc<dyn TableProvider>> {
473 self.files
474 .read()
475 .unwrap_or_else(|e| e.into_inner())
476 .get(graph)?
477 .iter()
478 .find(|f| &f.table == table)
479 .map(|f| Arc::clone(&f.provider))
480 }
481
482 pub(crate) fn set_catalogs(&self, catalogs: Arc<dyn CatalogProviderList>) {
484 let _ = self.catalogs.set(catalogs);
485 }
486
487 pub(crate) fn catalogs(&self) -> Option<&Arc<dyn CatalogProviderList>> {
488 self.catalogs.get()
489 }
490
491 pub fn names(&self) -> Vec<String> {
493 self.graphs
494 .read()
495 .unwrap_or_else(|e| e.into_inner())
496 .keys()
497 .cloned()
498 .collect()
499 }
500}
501
502pub(crate) fn with_registry(config: SessionConfig) -> SessionConfig {
504 config.with_extension(Arc::new(GraphRegistry::default()))
505}
506
507pub fn registry(state: &SessionState) -> Arc<GraphRegistry> {
509 state
510 .config()
511 .get_extension::<GraphRegistry>()
512 .expect("an Ursa session has a graph registry (built by ursa_engine::new_session)")
513}
514
515pub fn registry_of(ctx: &SessionContext) -> Arc<GraphRegistry> {
517 registry(&ctx.state())
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn a_session_has_an_empty_registry() {
526 let ctx = crate::new_session();
527 let graphs = registry_of(&ctx);
528 assert!(graphs.all().is_empty());
529 graphs.put(PropertyGraph {
530 name: "g".into(),
531 vertex_tables: Vec::new(),
532 edge_tables: vec![EdgeTable::new(TableReference::bare("e"), "a", "b")],
533 });
534 assert_eq!(registry_of(&ctx).names(), vec!["g".to_string()]);
536 }
537}