sqlitegraph/backend.rs
1//! Backend trait bridging sqlitegraph with higher-level graph consumers.
2//!
3//! This module contains the core GraphBackend trait and redirects to modular
4//! backend implementations. SQLite-specific implementations are in the sqlite submodule.
5
6// Include the modular backend structure
7mod sqlite;
8
9// Include native backend storage layer (no GraphBackend implementation yet)
10pub mod native;
11
12// Re-export from sqlite submodule
13pub use sqlite::SqliteGraphBackend;
14
15// Re-export from native submodule
16pub use native::NativeGraphBackend;
17
18// Re-export types for external users
19pub use crate::multi_hop::ChainStep;
20#[allow(unused_imports)] // Backend trait API types for future GraphBackend implementations
21pub use sqlite::types::{BackendDirection, EdgeSpec, NeighborQuery, NodeSpec};
22
23// KV store types (re-exported for public API)
24#[cfg(feature = "native-v2")]
25pub use crate::backend::native::v2::kv_store::types::{KvStoreError, KvValue};
26
27// Pub/Sub types (re-exported for public API)
28#[cfg(feature = "native-v2")]
29pub use crate::backend::native::v2::pubsub::{PubSubEvent, SubscriptionFilter};
30
31use crate::{
32 SqliteGraphError,
33 graph::GraphEntity,
34 pattern::{PatternMatch, PatternQuery},
35 snapshot::SnapshotId,
36};
37
38/// Backend trait defining the interface for graph database backends.
39///
40/// Each trait method delegates to backend-specific primitives while ensuring
41/// deterministic behavior and a single integration surface for consumers.
42///
43/// # Snapshot Isolation
44///
45/// All read operations require a `snapshot_id: SnapshotId` parameter to enforce
46/// ACID compliance. Reads only observe data committed at or before the snapshot.
47pub trait GraphBackend {
48 // Write operations (unchanged - commit returns SnapshotId in future)
49 fn insert_node(&self, node: NodeSpec) -> Result<i64, SqliteGraphError>;
50 fn insert_edge(&self, edge: EdgeSpec) -> Result<i64, SqliteGraphError>;
51
52 // Read operations (require snapshot_id parameter)
53 fn get_node(&self, snapshot_id: SnapshotId, id: i64) -> Result<GraphEntity, SqliteGraphError>;
54 fn neighbors(
55 &self,
56 snapshot_id: SnapshotId,
57 node: i64,
58 query: NeighborQuery,
59 ) -> Result<Vec<i64>, SqliteGraphError>;
60 fn bfs(
61 &self,
62 snapshot_id: SnapshotId,
63 start: i64,
64 depth: u32,
65 ) -> Result<Vec<i64>, SqliteGraphError>;
66 fn shortest_path(
67 &self,
68 snapshot_id: SnapshotId,
69 start: i64,
70 end: i64,
71 ) -> Result<Option<Vec<i64>>, SqliteGraphError>;
72 fn node_degree(
73 &self,
74 snapshot_id: SnapshotId,
75 node: i64,
76 ) -> Result<(usize, usize), SqliteGraphError>;
77 fn k_hop(
78 &self,
79 snapshot_id: SnapshotId,
80 start: i64,
81 depth: u32,
82 direction: BackendDirection,
83 ) -> Result<Vec<i64>, SqliteGraphError>;
84 fn k_hop_filtered(
85 &self,
86 snapshot_id: SnapshotId,
87 start: i64,
88 depth: u32,
89 direction: BackendDirection,
90 allowed_edge_types: &[&str],
91 ) -> Result<Vec<i64>, SqliteGraphError>;
92 fn chain_query(
93 &self,
94 snapshot_id: SnapshotId,
95 start: i64,
96 chain: &[crate::multi_hop::ChainStep],
97 ) -> Result<Vec<i64>, SqliteGraphError>;
98 fn pattern_search(
99 &self,
100 snapshot_id: SnapshotId,
101 start: i64,
102 pattern: &PatternQuery,
103 ) -> Result<Vec<PatternMatch>, SqliteGraphError>;
104
105 /// Trigger WAL checkpoint for backends that support write-ahead logging
106 ///
107 /// For Native backend with WAL: flushes WAL to graph file
108 /// For SQLite backend: executes PRAGMA wal_checkpoint(TRUNCATE)
109 /// For backends without WAL: returns Ok(()) as no-op
110 fn checkpoint(&self) -> Result<(), SqliteGraphError>;
111
112 /// Create a backup of the database
113 ///
114 /// Creates a consistent snapshot of the database including all data pages.
115 /// For Native V2 backend, optionally checkpoints before backup to ensure
116 /// WAL is applied and snapshot is consistent.
117 ///
118 /// # Arguments
119 /// * `backup_dir` - Destination directory for backup files
120 ///
121 /// # Returns
122 /// Backup result with paths, checksum, and metadata
123 fn backup(&self, backup_dir: &std::path::Path) -> Result<BackupResult, SqliteGraphError>;
124
125 /// Export database snapshot to the specified directory
126 ///
127 /// Creates a consistent snapshot of the current database state.
128 /// For Native backend: uses V2 snapshot format
129 /// For SQLite backend: uses JSON dump format
130 ///
131 /// # Arguments
132 /// * `export_dir` - Directory path where snapshot will be written
133 ///
134 /// # Returns
135 /// Snapshot metadata including file paths and size information
136 fn snapshot_export(
137 &self,
138 export_dir: &std::path::Path,
139 ) -> Result<SnapshotMetadata, SqliteGraphError>;
140
141 /// Import database snapshot from the specified directory
142 ///
143 /// Restores database state from a previously created snapshot.
144 /// For Native backend: loads V2 snapshot format
145 /// For SQLite backend: loads JSON dump format
146 ///
147 /// # Arguments
148 /// * `import_dir` - Directory path containing snapshot files
149 ///
150 /// # Returns
151 /// Import metadata including number of records imported
152 fn snapshot_import(
153 &self,
154 import_dir: &std::path::Path,
155 ) -> Result<ImportMetadata, SqliteGraphError>;
156
157 /// Get a value from the KV store at the given snapshot
158 ///
159 /// # Arguments
160 /// * `snapshot_id` - Only return data committed at or before this snapshot
161 /// * `key` - Key to retrieve (arbitrary bytes)
162 ///
163 /// # Returns
164 /// The value if found and visible at snapshot, or None if not found
165 #[cfg(feature = "native-v2")]
166 fn kv_get(
167 &self,
168 snapshot_id: SnapshotId,
169 key: &[u8],
170 ) -> Result<Option<crate::backend::native::v2::kv_store::types::KvValue>, SqliteGraphError>;
171
172 /// Set a value in the KV store
173 ///
174 /// This operation participates in the current transaction and will
175 /// be committed atomically with other graph operations.
176 ///
177 /// # Arguments
178 /// * `key` - Key to set (arbitrary bytes)
179 /// * `value` - Value to store
180 /// * `ttl_seconds` - Optional TTL in seconds (None = no expiration)
181 #[cfg(feature = "native-v2")]
182 fn kv_set(
183 &self,
184 key: Vec<u8>,
185 value: crate::backend::native::v2::kv_store::types::KvValue,
186 ttl_seconds: Option<u64>,
187 ) -> Result<(), SqliteGraphError>;
188
189 /// Delete a value from the KV store
190 ///
191 /// This operation participates in the current transaction and will
192 /// be committed atomically with other graph operations.
193 ///
194 /// # Arguments
195 /// * `key` - Key to delete
196 #[cfg(feature = "native-v2")]
197 fn kv_delete(&self, key: &[u8]) -> Result<(), SqliteGraphError>;
198
199 // Pub/Sub operations (in-process event notification)
200
201 /// Subscribe to graph change events
202 ///
203 /// Returns a subscriber ID and a receiver channel for events.
204 /// The receiver will receive events that match the given filter.
205 ///
206 /// # Events
207 ///
208 /// Events are emitted ONLY on transaction commit:
209 /// - `NodeChanged` - node created or modified
210 /// - `EdgeChanged` - edge created or modified
211 /// - `KVChanged` - KV entry created, modified, or deleted
212 /// - `SnapshotCommitted` - transaction committed
213 ///
214 /// # Best-Effort Delivery
215 ///
216 /// - No persistence of events
217 /// - If receiver is dropped, events are silently dropped
218 /// - If channel is full, events are silently dropped
219 /// - No delivery guarantees
220 ///
221 /// # Example
222 ///
223 /// ```ignore
224 /// let (sub_id, rx) = graph.subscribe(SubscriptionFilter::all());
225 /// // In another thread/task:
226 /// for event in rx {
227 /// match event {
228 /// PubSubEvent::NodeChanged { node_id, snapshot_id } => {
229 /// // Read node state at snapshot_id
230 /// }
231 /// _ => {}
232 /// }
233 /// }
234 /// }
235 /// ```
236 #[cfg(feature = "native-v2")]
237 fn subscribe(
238 &self,
239 filter: SubscriptionFilter,
240 ) -> Result<(u64, std::sync::mpsc::Receiver<PubSubEvent>), SqliteGraphError>;
241
242 /// Unsubscribe from events
243 ///
244 /// Cancels the subscription and stops receiving events.
245 /// Returns true if subscription existed and was removed.
246 ///
247 /// # Arguments
248 /// * `subscriber_id` - The subscriber ID returned by subscribe()
249 #[cfg(feature = "native-v2")]
250 fn unsubscribe(&self, subscriber_id: u64) -> Result<bool, SqliteGraphError>;
251
252 // ========== Pub/Sub Enhancement APIs (v1.4.0) ==========
253
254 /// Scan all KV entries with a given prefix
255 ///
256 /// Returns all keys that start with the given prefix, along with their values.
257 /// Results are in lexicographic order by key.
258 ///
259 /// # Arguments
260 /// * `snapshot_id` - Only return data committed at or before this snapshot
261 /// * `prefix` - Prefix to match (empty prefix returns all keys)
262 ///
263 /// # Returns
264 /// Vector of (key, value) pairs for all matching keys
265 #[cfg(feature = "native-v2")]
266 fn kv_prefix_scan(
267 &self,
268 snapshot_id: SnapshotId,
269 prefix: &[u8],
270 ) -> Result<Vec<(Vec<u8>, crate::backend::native::v2::kv_store::types::KvValue)>, SqliteGraphError>;
271
272 /// Query all nodes with a given kind
273 ///
274 /// Returns all node IDs where the node's kind equals the given string.
275 /// Results are sorted by node ID for deterministic output.
276 ///
277 /// # Arguments
278 /// * `snapshot_id` - Only return data committed at or before this snapshot
279 /// * `kind` - Kind string to match (case-sensitive)
280 ///
281 /// # Returns
282 /// Vector of node IDs with matching kind
283 fn query_nodes_by_kind(
284 &self,
285 snapshot_id: SnapshotId,
286 kind: &str,
287 ) -> Result<Vec<i64>, SqliteGraphError>;
288
289 /// Query nodes by name pattern using glob matching
290 ///
291 /// Returns all node IDs where the node's label matches the glob pattern.
292 /// Pattern syntax:
293 /// - `*` matches any sequence of characters
294 /// - `?` matches exactly one character
295 ///
296 /// # Arguments
297 /// * `snapshot_id` - Only return data committed at or before this snapshot
298 /// * `pattern` - Glob pattern to match against node labels
299 ///
300 /// # Returns
301 /// Vector of node IDs with matching labels
302 fn query_nodes_by_name_pattern(
303 &self,
304 snapshot_id: SnapshotId,
305 pattern: &str,
306 ) -> Result<Vec<i64>, SqliteGraphError>;
307}
308
309/// Metadata returned by snapshot export operations
310#[derive(Debug, Clone)]
311pub struct SnapshotMetadata {
312 /// Path to the snapshot file
313 pub snapshot_path: std::path::PathBuf,
314 /// Snapshot size in bytes
315 pub size_bytes: u64,
316 /// Number of entities in snapshot
317 pub entity_count: u64,
318 /// Number of edges in snapshot
319 pub edge_count: u64,
320}
321
322/// Metadata returned by snapshot import operations
323#[derive(Debug, Clone)]
324pub struct ImportMetadata {
325 /// Path to the imported snapshot
326 pub snapshot_path: std::path::PathBuf,
327 /// Number of entities imported
328 pub entities_imported: u64,
329 /// Number of edges imported
330 pub edges_imported: u64,
331}
332
333/// Result returned by backup operations
334#[derive(Debug, Clone)]
335pub struct BackupResult {
336 /// Path to backup snapshot file
337 pub snapshot_path: std::path::PathBuf,
338
339 /// Path to backup manifest file
340 pub manifest_path: std::path::PathBuf,
341
342 /// Backup size in bytes
343 pub size_bytes: u64,
344
345 /// Backup checksum
346 pub checksum: u64,
347
348 /// Number of records in backup
349 pub record_count: u64,
350
351 /// Backup duration in seconds
352 pub duration_secs: f64,
353
354 /// Backup timestamp (Unix epoch)
355 pub timestamp: u64,
356
357 /// Whether checkpoint was performed before backup
358 pub checkpoint_performed: bool,
359}
360
361/// Reference implementation for GraphBackend trait that works with references.
362impl<B> GraphBackend for &B
363where
364 B: GraphBackend + ?Sized,
365{
366 fn insert_node(&self, node: NodeSpec) -> Result<i64, SqliteGraphError> {
367 (*self).insert_node(node)
368 }
369
370 fn get_node(&self, snapshot_id: SnapshotId, id: i64) -> Result<GraphEntity, SqliteGraphError> {
371 (*self).get_node(snapshot_id, id)
372 }
373
374 fn insert_edge(&self, edge: EdgeSpec) -> Result<i64, SqliteGraphError> {
375 (*self).insert_edge(edge)
376 }
377
378 fn neighbors(
379 &self,
380 snapshot_id: SnapshotId,
381 node: i64,
382 query: NeighborQuery,
383 ) -> Result<Vec<i64>, SqliteGraphError> {
384 (*self).neighbors(snapshot_id, node, query)
385 }
386
387 fn bfs(
388 &self,
389 snapshot_id: SnapshotId,
390 start: i64,
391 depth: u32,
392 ) -> Result<Vec<i64>, SqliteGraphError> {
393 (*self).bfs(snapshot_id, start, depth)
394 }
395
396 fn shortest_path(
397 &self,
398 snapshot_id: SnapshotId,
399 start: i64,
400 end: i64,
401 ) -> Result<Option<Vec<i64>>, SqliteGraphError> {
402 (*self).shortest_path(snapshot_id, start, end)
403 }
404
405 fn node_degree(
406 &self,
407 snapshot_id: SnapshotId,
408 node: i64,
409 ) -> Result<(usize, usize), SqliteGraphError> {
410 (*self).node_degree(snapshot_id, node)
411 }
412
413 fn k_hop(
414 &self,
415 snapshot_id: SnapshotId,
416 start: i64,
417 depth: u32,
418 direction: BackendDirection,
419 ) -> Result<Vec<i64>, SqliteGraphError> {
420 (*self).k_hop(snapshot_id, start, depth, direction)
421 }
422
423 fn k_hop_filtered(
424 &self,
425 snapshot_id: SnapshotId,
426 start: i64,
427 depth: u32,
428 direction: BackendDirection,
429 allowed_edge_types: &[&str],
430 ) -> Result<Vec<i64>, SqliteGraphError> {
431 (*self).k_hop_filtered(snapshot_id, start, depth, direction, allowed_edge_types)
432 }
433
434 fn chain_query(
435 &self,
436 snapshot_id: SnapshotId,
437 start: i64,
438 chain: &[crate::multi_hop::ChainStep],
439 ) -> Result<Vec<i64>, SqliteGraphError> {
440 (*self).chain_query(snapshot_id, start, chain)
441 }
442
443 fn pattern_search(
444 &self,
445 snapshot_id: SnapshotId,
446 start: i64,
447 pattern: &PatternQuery,
448 ) -> Result<Vec<PatternMatch>, SqliteGraphError> {
449 (*self).pattern_search(snapshot_id, start, pattern)
450 }
451
452 fn checkpoint(&self) -> Result<(), SqliteGraphError> {
453 (*self).checkpoint()
454 }
455
456 fn backup(&self, backup_dir: &std::path::Path) -> Result<BackupResult, SqliteGraphError> {
457 (*self).backup(backup_dir)
458 }
459
460 fn snapshot_export(
461 &self,
462 export_dir: &std::path::Path,
463 ) -> Result<SnapshotMetadata, SqliteGraphError> {
464 (*self).snapshot_export(export_dir)
465 }
466
467 fn snapshot_import(
468 &self,
469 import_dir: &std::path::Path,
470 ) -> Result<ImportMetadata, SqliteGraphError> {
471 (*self).snapshot_import(import_dir)
472 }
473
474 #[cfg(feature = "native-v2")]
475 fn kv_get(
476 &self,
477 snapshot_id: SnapshotId,
478 key: &[u8],
479 ) -> Result<Option<crate::backend::native::v2::kv_store::types::KvValue>, SqliteGraphError>
480 {
481 (*self).kv_get(snapshot_id, key)
482 }
483
484 #[cfg(feature = "native-v2")]
485 fn kv_set(
486 &self,
487 key: Vec<u8>,
488 value: crate::backend::native::v2::kv_store::types::KvValue,
489 ttl_seconds: Option<u64>,
490 ) -> Result<(), SqliteGraphError> {
491 (*self).kv_set(key, value, ttl_seconds)
492 }
493
494 #[cfg(feature = "native-v2")]
495 fn kv_delete(&self, key: &[u8]) -> Result<(), SqliteGraphError> {
496 (*self).kv_delete(key)
497 }
498
499 #[cfg(feature = "native-v2")]
500 fn subscribe(
501 &self,
502 filter: SubscriptionFilter,
503 ) -> Result<(u64, std::sync::mpsc::Receiver<PubSubEvent>), SqliteGraphError> {
504 (*self).subscribe(filter)
505 }
506
507 #[cfg(feature = "native-v2")]
508 fn unsubscribe(&self, subscriber_id: u64) -> Result<bool, SqliteGraphError> {
509 (*self).unsubscribe(subscriber_id)
510 }
511
512 #[cfg(feature = "native-v2")]
513 fn kv_prefix_scan(
514 &self,
515 snapshot_id: SnapshotId,
516 prefix: &[u8],
517 ) -> Result<Vec<(Vec<u8>, crate::backend::native::v2::kv_store::types::KvValue)>, SqliteGraphError> {
518 (*self).kv_prefix_scan(snapshot_id, prefix)
519 }
520
521 fn query_nodes_by_kind(
522 &self,
523 snapshot_id: SnapshotId,
524 kind: &str,
525 ) -> Result<Vec<i64>, SqliteGraphError> {
526 (*self).query_nodes_by_kind(snapshot_id, kind)
527 }
528
529 fn query_nodes_by_name_pattern(
530 &self,
531 snapshot_id: SnapshotId,
532 pattern: &str,
533 ) -> Result<Vec<i64>, SqliteGraphError> {
534 (*self).query_nodes_by_name_pattern(snapshot_id, pattern)
535 }
536}