ursa_engine/graph/mod.rs
1//! Graphs in a session: their definitions and where they are kept.
2//!
3//! A graph is defined by `CREATE PROPERTY GRAPH` (SQL/PGQ) over tables or views
4//! already in the session: one edge table with a source and a destination
5//! column, and optionally one vertex table keyed by an id column. Without a
6//! vertex table, the nodes are the endpoints of the edges.
7//!
8//! This module is the contract between the graph DDL (which creates and drops
9//! [`GraphDef`]s) and everything that reads graphs: the `<graph>.nodes` and
10//! `<graph>.edges` tables and the algorithm table functions. Definitions are
11//! held per session in a [`GraphRegistry`], stored as a session config
12//! extension so any code holding the session state can reach it.
13//!
14//! A definition names tables; it does not copy or pin their rows. Every query
15//! plans the edge table afresh, so a graph over a view of a file sees the
16//! file's current contents.
17//!
18//! An edge or vertex table may also be a file path or URL, as in `FROM`
19//! (`EDGE TABLES ('edges.csv' SOURCE KEY (src) DESTINATION KEY (dst))`). It is
20//! opened once, when the graph is created, as a listing table that the
21//! registry keeps for that graph ([`GraphRegistry::file_table`]); it is not
22//! registered under any name, so it never shows in a listing. Like a view over
23//! the file, it fixes the file's columns at that point and reads the file's
24//! current rows on every query.
25//!
26//! Tables a graph uses cannot be dropped, or replaced by ones the graph's
27//! definition no longer fits, while the graph exists ([`check_drop`],
28//! [`check_replace`]). If one disappears anyway (through the DataFusion API),
29//! `<graph>.nodes` and `<graph>.edges` fail when queried, and session listings
30//! keep working.
31//!
32//! Algorithms over a graph are table functions, `FROM pagerank(g)`, planned
33//! as a [`GraphAlgorithmNode`] whose child is the edge table ([`functions`],
34//! `relation`, `node`, `exec`), with the built index cached per session
35//! ([`IndexCache`]).
36//!
37//! Traversals (`hop`, `shortest_path`, `random_walk`) are table functions
38//! too, planned as a [`GraphTraversalNode`] over the same edge plan, with one
39//! more child per node-id argument (a list, or a subquery that runs when the
40//! node runs); see [`traversal`].
41
42use 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,
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;
67mod relation;
68mod session;
69pub mod traversal;
70
71pub use catalog_table::UrsaFunctions;
72pub use exec::{
73 GraphAlgorithmExec, GraphPlanner, GraphTraversalExec, KeepEdgeOrder, UrsaQueryPlanner,
74};
75pub use index::{index_builds, index_loads, FileIdentity, GraphIndex, IndexCache, Weights};
76pub use node::{GraphAlgorithmNode, GraphTraversalNode, Output};
77pub(crate) use session::{edge_order_rule, query_planner, register_functions, with_algorithms};
78
79/// One graph, as `CREATE PROPERTY GRAPH` defined it.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct GraphDef {
82 /// The graph's name: also the name of the schema holding its `nodes` and
83 /// `edges` tables.
84 pub name: String,
85 pub edges: EdgeTable,
86 /// The vertex table, if the definition has `VERTEX TABLES`.
87 pub vertices: Option<VertexTable>,
88}
89
90/// The edge table: each row is an edge from `source` to `destination`.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct EdgeTable {
93 pub table: TableReference,
94 /// `SOURCE KEY (column)`.
95 pub source: String,
96 /// `DESTINATION KEY (column)`.
97 pub destination: String,
98}
99
100/// The vertex table: each row is a node, keyed by `key`.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct VertexTable {
103 pub table: TableReference,
104 /// `KEY (column)`.
105 pub key: String,
106}
107
108/// A table a graph opened from a file path or URL when it was created.
109#[derive(Debug, Clone)]
110pub struct FileTable {
111 /// The table reference the definition names: the path, as written.
112 pub table: TableReference,
113 pub provider: Arc<dyn TableProvider>,
114}
115
116/// The graphs defined in one session, by name. Names are compared as given
117/// (the DDL normalizes identifiers the way DataFusion does for tables).
118#[derive(Debug, Default)]
119pub struct GraphRegistry {
120 graphs: RwLock<BTreeMap<String, Arc<GraphDef>>>,
121 /// Per graph, the tables it opened from file paths (see the module docs).
122 files: RwLock<BTreeMap<String, Vec<FileTable>>>,
123 /// The session's catalogs before the file-table wrapper: graph schemas are
124 /// registered and dropped there (the wrapper cannot drop a schema).
125 catalogs: OnceLock<Arc<dyn CatalogProviderList>>,
126}
127
128impl GraphRegistry {
129 /// The graph called `name`.
130 pub fn get(&self, name: &str) -> Option<Arc<GraphDef>> {
131 self.graphs
132 .read()
133 .unwrap_or_else(|e| e.into_inner())
134 .get(name)
135 .cloned()
136 }
137
138 /// Every graph, sorted by name.
139 pub fn all(&self) -> Vec<Arc<GraphDef>> {
140 self.graphs
141 .read()
142 .unwrap_or_else(|e| e.into_inner())
143 .values()
144 .cloned()
145 .collect()
146 }
147
148 /// Add or replace `def`; returns the definition it replaced.
149 pub fn put(&self, def: GraphDef) -> Option<Arc<GraphDef>> {
150 self.put_with_files(def, Vec::new())
151 }
152
153 /// Add or replace `def`, with the tables it opened from file paths.
154 pub fn put_with_files(&self, def: GraphDef, files: Vec<FileTable>) -> Option<Arc<GraphDef>> {
155 let mut graphs = self.graphs.write().unwrap_or_else(|e| e.into_inner());
156 let mut all_files = self.files.write().unwrap_or_else(|e| e.into_inner());
157 if files.is_empty() {
158 all_files.remove(&def.name);
159 } else {
160 all_files.insert(def.name.clone(), files);
161 }
162 graphs.insert(def.name.clone(), Arc::new(def))
163 }
164
165 /// Remove the graph called `name`; returns it if it existed.
166 pub fn remove(&self, name: &str) -> Option<Arc<GraphDef>> {
167 let mut graphs = self.graphs.write().unwrap_or_else(|e| e.into_inner());
168 self.files
169 .write()
170 .unwrap_or_else(|e| e.into_inner())
171 .remove(name);
172 graphs.remove(name)
173 }
174
175 /// The table graph `graph` opened from the file path `table`, if `table`
176 /// is one of its file tables.
177 pub fn file_table(
178 &self,
179 graph: &str,
180 table: &TableReference,
181 ) -> Option<Arc<dyn TableProvider>> {
182 self.files
183 .read()
184 .unwrap_or_else(|e| e.into_inner())
185 .get(graph)?
186 .iter()
187 .find(|f| &f.table == table)
188 .map(|f| Arc::clone(&f.provider))
189 }
190
191 /// Record the session's unwrapped catalog list (done by `new_session`).
192 pub(crate) fn set_catalogs(&self, catalogs: Arc<dyn CatalogProviderList>) {
193 let _ = self.catalogs.set(catalogs);
194 }
195
196 pub(crate) fn catalogs(&self) -> Option<&Arc<dyn CatalogProviderList>> {
197 self.catalogs.get()
198 }
199
200 /// The names of all graphs, sorted: for "did you mean" suggestions.
201 pub fn names(&self) -> Vec<String> {
202 self.graphs
203 .read()
204 .unwrap_or_else(|e| e.into_inner())
205 .keys()
206 .cloned()
207 .collect()
208 }
209}
210
211/// Install an empty registry on `config` (done by `new_session`).
212pub(crate) fn with_registry(config: SessionConfig) -> SessionConfig {
213 config.with_extension(Arc::new(GraphRegistry::default()))
214}
215
216/// The session's graph registry.
217pub fn registry(state: &SessionState) -> Arc<GraphRegistry> {
218 state
219 .config()
220 .get_extension::<GraphRegistry>()
221 .expect("an Ursa session has a graph registry (built by ursa_engine::new_session)")
222}
223
224/// The graph registry of `ctx`.
225pub fn registry_of(ctx: &SessionContext) -> Arc<GraphRegistry> {
226 registry(&ctx.state())
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn a_session_has_an_empty_registry() {
235 let ctx = crate::new_session();
236 let graphs = registry_of(&ctx);
237 assert!(graphs.all().is_empty());
238 graphs.put(GraphDef {
239 name: "g".into(),
240 edges: EdgeTable {
241 table: TableReference::bare("e"),
242 source: "a".into(),
243 destination: "b".into(),
244 },
245 vertices: None,
246 });
247 // The registry is shared by every handle on the session.
248 assert_eq!(registry_of(&ctx).names(), vec!["g".to_string()]);
249 }
250}