Skip to main content

uqa_storage_sqlite/
graph.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Standalone `SQLite` graph storage using indexed records directly.
8//!
9//! The handle does not load a graph on open or retain entity/adjacency maps.
10//! Existing per-table schemas and legacy property encodings remain readable.
11//! Mutations use physical transactions/savepoints; multi-read queries pin one
12//! storage snapshot. Owned query results belong to the caller, not the store.
13
14mod access;
15
16use std::collections::{BTreeMap, BTreeSet};
17use std::sync::Arc;
18
19use crate::{ManagedConnection, SQLiteError, SQLiteStorageBackend};
20use uqa_core::{Edge, Value, Vertex};
21use uqa_storage::PersistentStorageBackend;
22
23use uqa_graph::{begin_graph_write, GraphStorage};
24use uqa_graph::{Direction, GraphStore, GraphStoreError, GraphStoreResult, PersistentGraphStore};
25
26const LEGACY_PROPERTIES_FORMAT: i64 = 1;
27const TAGGED_PROPERTIES_FORMAT: i64 = 2;
28
29/// Direct durable access to a standalone graph table family.
30pub struct SQLiteGraphStore {
31    inner: PersistentGraphStore,
32    backend: Arc<dyn PersistentStorageBackend>,
33    operation_gate: parking_lot::ReentrantMutex<()>,
34}
35
36fn graph_store_error(error: &GraphStoreError) -> SQLiteError {
37    SQLiteError::StorageBackend(error.to_string())
38}
39
40fn sqlite_graph_error(error: &SQLiteError) -> GraphStoreError {
41    GraphStoreError::Storage(error.to_string())
42}
43
44impl SQLiteGraphStore {
45    /// Open a table family and migrate small access metadata once. Existing
46    /// records are streamed only for a legacy label-watermark migration.
47    pub fn open(conn: ManagedConnection, table_name: Option<&str>) -> Result<Self, SQLiteError> {
48        let suffix = table_name.unwrap_or("");
49        if !suffix
50            .chars()
51            .all(|character| character.is_ascii_alphanumeric() || character == '_')
52        {
53            return Err(SQLiteError::StorageBackend(format!(
54                "invalid graph table suffix {suffix:?}"
55            )));
56        }
57        let suffix = if suffix.is_empty() {
58            String::new()
59        } else {
60            format!("_{suffix}")
61        };
62        let backend: Arc<dyn PersistentStorageBackend> =
63            Arc::new(SQLiteStorageBackend::new(conn.clone()));
64        let storage = Arc::new(access::SQLiteGraphStorage {
65            conn,
66            backend: Arc::clone(&backend),
67            vtx_table: format!("_graph_vertices{suffix}"),
68            edge_table: format!("_graph_edges{suffix}"),
69            member_table: format!("_graph_membership{suffix}"),
70            catalog_table: format!("_graph_catalog{suffix}"),
71            metadata_table: format!("_graph_metadata{suffix}"),
72        });
73        let mut inner = PersistentGraphStore::from_storage(storage.clone());
74        let mut checkpoint =
75            begin_graph_write(Arc::clone(&backend)).map_err(|error| graph_store_error(&error))?;
76        let opened = (|| {
77            storage.ensure_tables()?;
78            match storage.metadata("direct_access_version")?.as_deref() {
79                Some("1") => {}
80                None => {
81                    for graph in storage
82                        .graph_names()
83                        .map_err(|error| graph_store_error(&error))?
84                    {
85                        inner
86                            .rebuild_label_registry_from_ids(&graph)
87                            .map_err(|error| graph_store_error(&error))?;
88                    }
89                    storage.save_metadata("direct_access_version", "1")?;
90                }
91                Some(other) => {
92                    return Err(SQLiteError::StorageBackend(format!(
93                        "unsupported standalone graph access version {other}"
94                    )))
95                }
96            }
97            Ok(())
98        })();
99        if let Err(error) = opened {
100            checkpoint
101                .rollback()
102                .map_err(|error| graph_store_error(&error))?;
103            return Err(error);
104        }
105        checkpoint
106            .commit()
107            .map_err(|error| graph_store_error(&error))?;
108        Ok(Self {
109            inner,
110            backend,
111            operation_gate: parking_lot::ReentrantMutex::new(()),
112        })
113    }
114
115    /// Execute several graph reads against one pinned physical snapshot.
116    /// Callers must serialize transaction ownership on this storage session.
117    pub fn read_snapshot<T>(
118        &self,
119        read: impl FnOnce(&PersistentGraphStore) -> GraphStoreResult<T>,
120    ) -> GraphStoreResult<T> {
121        struct ReadCheckpoint(Option<Arc<dyn PersistentStorageBackend>>);
122        impl Drop for ReadCheckpoint {
123            fn drop(&mut self) {
124                if let Some(backend) = &self.0 {
125                    let _ = backend.rollback_transaction();
126                }
127            }
128        }
129        let _operation = self.operation_gate.lock();
130        if self.backend.in_transaction() {
131            return read(&self.inner);
132        }
133        self.backend.begin_read_transaction()?;
134        let mut checkpoint = ReadCheckpoint(Some(Arc::clone(&self.backend)));
135        let result = read(&self.inner);
136        self.backend.rollback_transaction()?;
137        checkpoint.0 = None;
138        result
139    }
140
141    /// Borrow the durable handle within an existing storage transaction.
142    pub fn as_graph_store(&self) -> &PersistentGraphStore {
143        &self.inner
144    }
145
146    pub fn create_graph(&mut self, name: &str) -> Result<(), SQLiteError> {
147        self.inner
148            .create_graph(name)
149            .map_err(|error| graph_store_error(&error))
150    }
151
152    pub fn drop_graph(&mut self, name: &str) -> Result<(), SQLiteError> {
153        self.inner
154            .drop_graph(name)
155            .map_err(|error| graph_store_error(&error))
156    }
157
158    pub fn graph_names(&self) -> Result<Vec<String>, SQLiteError> {
159        self.read_snapshot(GraphStore::graph_names)
160            .map_err(|error| graph_store_error(&error))
161    }
162
163    pub fn has_graph(&self, name: &str) -> Result<bool, SQLiteError> {
164        self.read_snapshot(|store| store.has_graph(name))
165            .map_err(|error| graph_store_error(&error))
166    }
167
168    pub fn union_graphs(&mut self, g1: &str, g2: &str, target: &str) -> Result<(), SQLiteError> {
169        self.inner
170            .union_graphs(g1, g2, target)
171            .map_err(|error| graph_store_error(&error))
172    }
173
174    pub fn intersect_graphs(
175        &mut self,
176        g1: &str,
177        g2: &str,
178        target: &str,
179    ) -> Result<(), SQLiteError> {
180        self.inner
181            .intersect_graphs(g1, g2, target)
182            .map_err(|error| graph_store_error(&error))
183    }
184
185    pub fn difference_graphs(
186        &mut self,
187        g1: &str,
188        g2: &str,
189        target: &str,
190    ) -> Result<(), SQLiteError> {
191        self.inner
192            .difference_graphs(g1, g2, target)
193            .map_err(|error| graph_store_error(&error))
194    }
195
196    pub fn copy_graph(&mut self, source: &str, target: &str) -> Result<(), SQLiteError> {
197        self.inner
198            .copy_graph(source, target)
199            .map_err(|error| graph_store_error(&error))
200    }
201
202    pub fn add_vertex(&mut self, vertex: Vertex, graph: &str) -> Result<(), SQLiteError> {
203        self.inner
204            .add_vertex(vertex, graph)
205            .map_err(|error| graph_store_error(&error))
206    }
207
208    pub fn add_edge(&mut self, edge: Edge, graph: &str) -> Result<(), SQLiteError> {
209        self.inner
210            .add_edge(edge, graph)
211            .map_err(|error| graph_store_error(&error))
212    }
213
214    pub fn remove_vertex(&mut self, vertex_id: u64, graph: &str) -> Result<(), SQLiteError> {
215        self.inner
216            .remove_vertex(vertex_id, graph)
217            .map_err(|error| graph_store_error(&error))
218    }
219
220    pub fn remove_edge(&mut self, edge_id: u64, graph: &str) -> Result<(), SQLiteError> {
221        self.inner
222            .remove_edge(edge_id, graph)
223            .map_err(|error| graph_store_error(&error))
224    }
225
226    pub fn neighbors(
227        &self,
228        vertex_id: u64,
229        label: Option<&str>,
230        direction: Direction,
231        graph: &str,
232    ) -> Result<Vec<u64>, SQLiteError> {
233        self.read_snapshot(|store| store.neighbors(vertex_id, label, direction, graph))
234            .map_err(|error| graph_store_error(&error))
235    }
236
237    pub fn vertices_by_label(&self, label: &str, graph: &str) -> Result<Vec<Vertex>, SQLiteError> {
238        self.read_snapshot(|store| store.vertices_by_label(label, graph))
239            .map_err(|error| graph_store_error(&error))
240    }
241
242    pub fn vertex_ids_by_label(&self, label: &str, graph: &str) -> Result<Vec<u64>, SQLiteError> {
243        self.read_snapshot(|store| store.vertex_ids_by_label(label, graph))
244            .map_err(|error| graph_store_error(&error))
245    }
246
247    pub fn vertices_in_graph(&self, graph: &str) -> Result<Vec<Vertex>, SQLiteError> {
248        self.read_snapshot(|store| store.vertices_in_graph(graph))
249            .map_err(|error| graph_store_error(&error))
250    }
251
252    pub fn edges_in_graph(&self, graph: &str) -> Result<Vec<Edge>, SQLiteError> {
253        self.read_snapshot(|store| store.edges_in_graph(graph))
254            .map_err(|error| graph_store_error(&error))
255    }
256
257    pub fn vertex_graphs(&self, vertex_id: u64) -> Result<BTreeSet<String>, SQLiteError> {
258        self.read_snapshot(|store| store.vertex_graphs(vertex_id))
259            .map_err(|error| graph_store_error(&error))
260    }
261
262    pub fn out_edge_ids(&self, vertex_id: u64, graph: &str) -> Result<BTreeSet<u64>, SQLiteError> {
263        self.read_snapshot(|store| store.out_edge_ids(vertex_id, graph))
264            .map_err(|error| graph_store_error(&error))
265    }
266
267    pub fn in_edge_ids(&self, vertex_id: u64, graph: &str) -> Result<BTreeSet<u64>, SQLiteError> {
268        self.read_snapshot(|store| store.in_edge_ids(vertex_id, graph))
269            .map_err(|error| graph_store_error(&error))
270    }
271
272    pub fn edge_ids_by_label(
273        &self,
274        label: &str,
275        graph: &str,
276    ) -> Result<BTreeSet<u64>, SQLiteError> {
277        self.read_snapshot(|store| store.edge_ids_by_label(label, graph))
278            .map_err(|error| graph_store_error(&error))
279    }
280
281    pub fn vertex_ids_in_graph(&self, graph: &str) -> Result<BTreeSet<u64>, SQLiteError> {
282        self.read_snapshot(|store| store.vertex_ids_in_graph(graph))
283            .map_err(|error| graph_store_error(&error))
284    }
285
286    pub fn require_vertex_in_graph(&self, vertex_id: u64, graph: &str) -> Result<(), SQLiteError> {
287        self.read_snapshot(|store| store.require_vertex_in_graph(vertex_id, graph))
288            .map_err(|error| graph_store_error(&error))
289    }
290
291    pub fn degree_distribution(&self, graph: &str) -> Result<BTreeMap<u64, u64>, SQLiteError> {
292        self.read_snapshot(|store| store.degree_distribution(graph))
293            .map_err(|error| graph_store_error(&error))
294    }
295
296    pub fn label_degree(&self, label: &str, graph: &str) -> Result<f64, SQLiteError> {
297        self.read_snapshot(|store| store.label_degree(label, graph))
298            .map_err(|error| graph_store_error(&error))
299    }
300
301    pub fn vertex_label_counts(&self, graph: &str) -> Result<BTreeMap<String, u64>, SQLiteError> {
302        self.read_snapshot(|store| store.vertex_label_counts(graph))
303            .map_err(|error| graph_store_error(&error))
304    }
305
306    pub fn get_vertex(&self, vertex_id: u64) -> Result<Option<Vertex>, SQLiteError> {
307        self.read_snapshot(|store| store.get_vertex(vertex_id))
308            .map_err(|error| graph_store_error(&error))
309    }
310
311    pub fn get_edge(&self, edge_id: u64) -> Result<Option<Edge>, SQLiteError> {
312        self.read_snapshot(|store| store.get_edge(edge_id))
313            .map_err(|error| graph_store_error(&error))
314    }
315
316    pub fn next_vertex_id(&mut self) -> Result<u64, SQLiteError> {
317        self.inner
318            .next_vertex_id()
319            .map_err(|error| graph_store_error(&error))
320    }
321
322    pub fn next_edge_id(&mut self) -> Result<u64, SQLiteError> {
323        self.inner
324            .next_edge_id()
325            .map_err(|error| graph_store_error(&error))
326    }
327
328    pub fn allocate_vertex_id(&mut self, label: &str, graph: &str) -> Result<u64, SQLiteError> {
329        self.inner
330            .allocate_vertex_id(label, graph)
331            .map_err(|error| graph_store_error(&error))
332    }
333
334    pub fn allocate_edge_id(&mut self, label: &str, graph: &str) -> Result<u64, SQLiteError> {
335        self.inner
336            .allocate_edge_id(label, graph)
337            .map_err(|error| graph_store_error(&error))
338    }
339
340    pub fn clear(&mut self) -> Result<(), SQLiteError> {
341        self.inner
342            .clear()
343            .map_err(|error| graph_store_error(&error))
344    }
345
346    pub fn vertices(&self) -> Result<BTreeMap<u64, Vertex>, SQLiteError> {
347        self.read_snapshot(GraphStore::vertices)
348            .map_err(|error| graph_store_error(&error))
349    }
350
351    pub fn edges(&self) -> Result<BTreeMap<u64, Edge>, SQLiteError> {
352        self.read_snapshot(GraphStore::edges)
353            .map_err(|error| graph_store_error(&error))
354    }
355}
356
357impl GraphStore for SQLiteGraphStore {
358    fn vertex_id_page(
359        &self,
360        graph: &str,
361        after: Option<u64>,
362        limit: usize,
363    ) -> GraphStoreResult<Vec<u64>> {
364        self.read_snapshot(|store| store.vertex_id_page(graph, after, limit))
365    }
366    fn edge_id_page(
367        &self,
368        graph: &str,
369        after: Option<u64>,
370        limit: usize,
371    ) -> GraphStoreResult<Vec<u64>> {
372        self.read_snapshot(|store| store.edge_id_page(graph, after, limit))
373    }
374    fn transaction<T>(
375        &mut self,
376        operation: impl FnOnce(&mut Self) -> GraphStoreResult<T>,
377    ) -> GraphStoreResult<T> {
378        let mut checkpoint = self.inner.clone();
379        checkpoint.transaction(|_| operation(self))
380    }
381
382    fn create_graph(&mut self, name: &str) -> GraphStoreResult<()> {
383        self.inner.create_graph(name)
384    }
385
386    fn drop_graph(&mut self, name: &str) -> GraphStoreResult<()> {
387        self.inner.drop_graph(name)
388    }
389
390    fn graph_names(&self) -> GraphStoreResult<Vec<String>> {
391        self.read_snapshot(GraphStore::graph_names)
392    }
393
394    fn has_graph(&self, name: &str) -> GraphStoreResult<bool> {
395        self.read_snapshot(|store| store.has_graph(name))
396    }
397
398    fn union_graphs(&mut self, g1: &str, g2: &str, target: &str) -> GraphStoreResult<()> {
399        self.inner.union_graphs(g1, g2, target)
400    }
401
402    fn intersect_graphs(&mut self, g1: &str, g2: &str, target: &str) -> GraphStoreResult<()> {
403        self.inner.intersect_graphs(g1, g2, target)
404    }
405
406    fn difference_graphs(&mut self, g1: &str, g2: &str, target: &str) -> GraphStoreResult<()> {
407        self.inner.difference_graphs(g1, g2, target)
408    }
409
410    fn copy_graph(&mut self, source: &str, target: &str) -> GraphStoreResult<()> {
411        self.inner.copy_graph(source, target)
412    }
413
414    fn add_vertex(&mut self, vertex: Vertex, graph: &str) -> GraphStoreResult<()> {
415        self.inner.add_vertex(vertex, graph)
416    }
417
418    fn add_edge(&mut self, edge: Edge, graph: &str) -> GraphStoreResult<()> {
419        self.inner.add_edge(edge, graph)
420    }
421
422    fn remove_vertex(&mut self, vertex_id: u64, graph: &str) -> GraphStoreResult<()> {
423        self.inner.remove_vertex(vertex_id, graph)
424    }
425
426    fn remove_edge(&mut self, edge_id: u64, graph: &str) -> GraphStoreResult<()> {
427        self.inner.remove_edge(edge_id, graph)
428    }
429
430    fn neighbors(
431        &self,
432        vertex_id: u64,
433        label: Option<&str>,
434        direction: Direction,
435        graph: &str,
436    ) -> GraphStoreResult<Vec<u64>> {
437        self.read_snapshot(|store| store.neighbors(vertex_id, label, direction, graph))
438    }
439
440    fn vertices_by_label(&self, label: &str, graph: &str) -> GraphStoreResult<Vec<Vertex>> {
441        self.read_snapshot(|store| store.vertices_by_label(label, graph))
442    }
443
444    fn vertex_ids_by_label(&self, label: &str, graph: &str) -> GraphStoreResult<Vec<u64>> {
445        self.read_snapshot(|store| store.vertex_ids_by_label(label, graph))
446    }
447
448    fn vertices_in_graph(&self, graph: &str) -> GraphStoreResult<Vec<Vertex>> {
449        self.read_snapshot(|store| store.vertices_in_graph(graph))
450    }
451
452    fn edges_in_graph(&self, graph: &str) -> GraphStoreResult<Vec<Edge>> {
453        self.read_snapshot(|store| store.edges_in_graph(graph))
454    }
455
456    fn vertex_graphs(&self, vertex_id: u64) -> GraphStoreResult<BTreeSet<String>> {
457        self.read_snapshot(|store| store.vertex_graphs(vertex_id))
458    }
459    fn edge_graphs(&self, edge_id: u64) -> GraphStoreResult<BTreeSet<String>> {
460        self.read_snapshot(|store| store.edge_graphs(edge_id))
461    }
462    fn edges_by_label(&self, label: &str, graph: &str) -> GraphStoreResult<Vec<Edge>> {
463        self.read_snapshot(|store| store.edges_by_label(label, graph))
464    }
465
466    fn out_edge_ids(&self, vertex_id: u64, graph: &str) -> GraphStoreResult<BTreeSet<u64>> {
467        self.read_snapshot(|store| store.out_edge_ids(vertex_id, graph))
468    }
469
470    fn in_edge_ids(&self, vertex_id: u64, graph: &str) -> GraphStoreResult<BTreeSet<u64>> {
471        self.read_snapshot(|store| store.in_edge_ids(vertex_id, graph))
472    }
473
474    fn edge_ids_by_label(&self, label: &str, graph: &str) -> GraphStoreResult<BTreeSet<u64>> {
475        self.read_snapshot(|store| store.edge_ids_by_label(label, graph))
476    }
477
478    fn vertex_ids_in_graph(&self, graph: &str) -> GraphStoreResult<BTreeSet<u64>> {
479        self.read_snapshot(|store| store.vertex_ids_in_graph(graph))
480    }
481
482    fn require_vertex_in_graph(&self, vertex_id: u64, graph: &str) -> GraphStoreResult<()> {
483        self.read_snapshot(|store| store.require_vertex_in_graph(vertex_id, graph))
484    }
485
486    fn degree_distribution(&self, graph: &str) -> GraphStoreResult<BTreeMap<u64, u64>> {
487        self.read_snapshot(|store| store.degree_distribution(graph))
488    }
489
490    fn label_degree(&self, label: &str, graph: &str) -> GraphStoreResult<f64> {
491        self.read_snapshot(|store| store.label_degree(label, graph))
492    }
493
494    fn vertex_label_counts(&self, graph: &str) -> GraphStoreResult<BTreeMap<String, u64>> {
495        self.read_snapshot(|store| store.vertex_label_counts(graph))
496    }
497
498    fn get_vertex(&self, vertex_id: u64) -> GraphStoreResult<Option<Vertex>> {
499        self.read_snapshot(|store| store.get_vertex(vertex_id))
500    }
501
502    fn get_edge(&self, edge_id: u64) -> GraphStoreResult<Option<Edge>> {
503        self.read_snapshot(|store| store.get_edge(edge_id))
504    }
505
506    fn next_vertex_id(&mut self) -> GraphStoreResult<u64> {
507        self.inner.next_vertex_id()
508    }
509
510    fn next_edge_id(&mut self) -> GraphStoreResult<u64> {
511        self.inner.next_edge_id()
512    }
513
514    fn allocate_vertex_id(&mut self, label: &str, graph: &str) -> GraphStoreResult<u64> {
515        self.inner.allocate_vertex_id(label, graph)
516    }
517
518    fn allocate_edge_id(&mut self, label: &str, graph: &str) -> GraphStoreResult<u64> {
519        self.inner.allocate_edge_id(label, graph)
520    }
521
522    fn clear(&mut self) -> GraphStoreResult<()> {
523        self.inner.clear()
524    }
525
526    fn vertices(&self) -> GraphStoreResult<BTreeMap<u64, Vertex>> {
527        self.read_snapshot(GraphStore::vertices)
528    }
529
530    fn edges(&self) -> GraphStoreResult<BTreeMap<u64, Edge>> {
531        self.read_snapshot(GraphStore::edges)
532    }
533}
534
535fn decode_graph_id(kind: &str, id: i64) -> Result<u64, SQLiteError> {
536    u64::try_from(id).map_err(|_| {
537        SQLiteError::StorageBackend(format!("invalid negative {kind} id {id} in graph store"))
538    })
539}
540
541fn decode_properties(
542    properties_json: &str,
543    properties_format: i64,
544) -> Result<BTreeMap<String, Value>, SQLiteError> {
545    match properties_format {
546        LEGACY_PROPERTIES_FORMAT => {
547            let raw: BTreeMap<String, serde_json::Value> =
548                serde_json::from_str(properties_json).map_err(SQLiteError::from)?;
549            raw.into_iter()
550                .map(|(key, value)| decode_legacy_value(value).map(|value| (key, value)))
551                .collect()
552        }
553        TAGGED_PROPERTIES_FORMAT => {
554            serde_json::from_str(properties_json).map_err(SQLiteError::from)
555        }
556        other => Err(SQLiteError::StorageBackend(format!(
557            "unsupported graph properties format version {other}"
558        ))),
559    }
560}
561
562/// Decode records written by the original untagged `Value` serializer.
563/// Its `Bytes(Vec<u8>)` variant preceded `List`, so a JSON array made solely
564/// from byte-range integers (including `[]`) represented bytes at every
565/// nesting depth. New records use an explicit bytes tag and reserve raw JSON
566/// arrays for `Value::List`.
567fn decode_legacy_value(raw: serde_json::Value) -> Result<Value, SQLiteError> {
568    match raw {
569        serde_json::Value::Array(items) => {
570            let bytes = items
571                .iter()
572                .map(|item| item.as_u64().and_then(|number| u8::try_from(number).ok()))
573                .collect::<Option<Vec<_>>>();
574            if let Some(bytes) = bytes {
575                return Ok(Value::Bytes(bytes));
576            }
577            items
578                .into_iter()
579                .map(decode_legacy_value)
580                .collect::<Result<Vec<_>, _>>()
581                .map(Value::List)
582        }
583        serde_json::Value::Object(map) => {
584            if map
585                .get("$uqa_type")
586                .and_then(serde_json::Value::as_str)
587                .is_some()
588            {
589                let tagged: Value = serde_json::from_value(serde_json::Value::Object(map.clone()))
590                    .map_err(SQLiteError::from)?;
591                if !matches!(tagged, Value::Map(_)) {
592                    return Ok(tagged);
593                }
594            }
595            map.into_iter()
596                .map(|(key, value)| decode_legacy_value(value).map(|value| (key, value)))
597                .collect::<Result<BTreeMap<_, _>, _>>()
598                .map(Value::Map)
599        }
600        scalar => serde_json::from_value(scalar).map_err(SQLiteError::from),
601    }
602}
603
604fn encode_graph_id(kind: &str, id: u64) -> Result<i64, SQLiteError> {
605    i64::try_from(id).map_err(|_| {
606        SQLiteError::StorageBackend(format!("{kind} id {id} exceeds SQLite INTEGER range"))
607    })
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613    use uqa_core::{Value, Vertex};
614
615    #[test]
616    fn round_trip_through_sqlite() {
617        let conn = ManagedConnection::open_in_memory().unwrap();
618        let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
619        store.create_graph("g").unwrap();
620        store.add_vertex(Vertex::new(1, "person"), "g").unwrap();
621        store.add_vertex(Vertex::new(2, "person"), "g").unwrap();
622        store.add_edge(Edge::new(1, 1, 2, "knows"), "g").unwrap();
623        let other = SQLiteGraphStore::open(conn, None).unwrap();
624        assert!(other.has_graph("g").unwrap());
625        let vs = other.vertices_in_graph("g").unwrap();
626        assert_eq!(vs.len(), 2);
627        let es = other.edges_in_graph("g").unwrap();
628        assert_eq!(es.len(), 1);
629    }
630
631    #[test]
632    fn legacy_raw_bytes_and_edge_first_memberships_survive_reopen() {
633        let conn = ManagedConnection::open_in_memory().unwrap();
634        conn.with(|connection| {
635            // Exact pre-versioning table shapes. Insert the edge membership
636            // first to prove hydration is independent of SQLite row order.
637            connection.execute_batch(
638                r#"
639                CREATE TABLE _graph_vertices (
640                    vertex_id INTEGER PRIMARY KEY,
641                    label TEXT NOT NULL DEFAULT '',
642                    properties_json TEXT NOT NULL
643                );
644                CREATE TABLE _graph_edges (
645                    edge_id INTEGER PRIMARY KEY,
646                    source_id INTEGER NOT NULL,
647                    target_id INTEGER NOT NULL,
648                    label TEXT NOT NULL,
649                    properties_json TEXT NOT NULL
650                );
651                CREATE TABLE _graph_membership (
652                    graph TEXT NOT NULL,
653                    entity_kind TEXT NOT NULL,
654                    entity_id INTEGER NOT NULL,
655                    PRIMARY KEY (graph, entity_kind, entity_id)
656                );
657                CREATE TABLE _graph_catalog (name TEXT PRIMARY KEY);
658                INSERT INTO _graph_catalog (name) VALUES ('g');
659                INSERT INTO _graph_vertices
660                    (vertex_id, label, properties_json)
661                    VALUES
662                    (1, 'person', '{"bytes":[1,2],"nested":[[3,4]],"list":[256]}'),
663                    (2, 'person', '{}');
664                INSERT INTO _graph_edges
665                    (edge_id, source_id, target_id, label, properties_json)
666                    VALUES (10, 1, 2, 'knows', '{"bytes":[5,6]}');
667                INSERT INTO _graph_membership
668                    (graph, entity_kind, entity_id) VALUES ('g', 'e', 10);
669                INSERT INTO _graph_membership
670                    (graph, entity_kind, entity_id) VALUES ('g', 'v', 1);
671                INSERT INTO _graph_membership
672                    (graph, entity_kind, entity_id) VALUES ('g', 'v', 2);
673                "#,
674            )?;
675            Ok(())
676        })
677        .unwrap();
678
679        let reopened = SQLiteGraphStore::open(conn, None).unwrap();
680        let vertex = reopened.get_vertex(1).unwrap().unwrap();
681        assert_eq!(vertex.properties["bytes"], Value::Bytes(vec![1, 2]));
682        assert_eq!(
683            vertex.properties["nested"],
684            Value::List(vec![Value::Bytes(vec![3, 4])])
685        );
686        assert_eq!(
687            vertex.properties["list"],
688            Value::List(vec![Value::Int(256)])
689        );
690        assert_eq!(
691            reopened.get_edge(10).unwrap().unwrap().properties["bytes"],
692            Value::Bytes(vec![5, 6])
693        );
694    }
695
696    #[test]
697    fn list_and_explicit_bytes_properties_remain_distinct_after_reopen() {
698        let conn = ManagedConnection::open_in_memory().unwrap();
699        let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
700        store.create_graph("g").unwrap();
701        let mut vertex = Vertex::new(1, "payload");
702        vertex.properties.insert(
703            "list".into(),
704            Value::List(vec![Value::Int(1), Value::Int(2)]),
705        );
706        vertex
707            .properties
708            .insert("bytes".into(), Value::Bytes(vec![1, 2]));
709        store.add_vertex(vertex, "g").unwrap();
710        drop(store);
711
712        let reopened = SQLiteGraphStore::open(conn, None).unwrap();
713        let restored = reopened.get_vertex(1).unwrap().unwrap();
714        assert_eq!(
715            restored.properties["list"],
716            Value::List(vec![Value::Int(1), Value::Int(2)])
717        );
718        assert_eq!(restored.properties["bytes"], Value::Bytes(vec![1, 2]));
719    }
720
721    #[test]
722    fn failed_persistence_does_not_publish_partial_disk_state() {
723        let conn = ManagedConnection::open_in_memory().unwrap();
724        let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
725        store.create_graph("g").unwrap();
726        store.add_vertex(Vertex::new(1, "person"), "g").unwrap();
727        conn.with(|connection| {
728            connection.execute_batch(
729                r#"
730                CREATE TRIGGER fail_graph_membership
731                BEFORE INSERT ON "_graph_membership"
732                WHEN NEW.entity_id = 2
733                BEGIN
734                    SELECT RAISE(ABORT, 'forced graph persistence failure');
735                END;
736                "#,
737            )?;
738            Ok(())
739        })
740        .unwrap();
741
742        assert!(store.add_vertex(Vertex::new(2, "person"), "g").is_err());
743        assert!(store.get_vertex(1).unwrap().is_some());
744        assert!(store.get_vertex(2).unwrap().is_none());
745
746        conn.with(|connection| {
747            connection.execute_batch("DROP TRIGGER fail_graph_membership")?;
748            Ok(())
749        })
750        .unwrap();
751        let reopened = SQLiteGraphStore::open(conn, None).unwrap();
752        assert_eq!(reopened.vertices_in_graph("g").unwrap().len(), 1);
753        assert!(reopened.get_vertex(2).unwrap().is_none());
754    }
755
756    #[test]
757    fn unrelated_corrupt_payload_is_not_loaded_until_queried() {
758        let conn = ManagedConnection::open_in_memory().unwrap();
759        let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
760        store.create_graph("g").unwrap();
761        store.add_vertex(Vertex::new(1, "person"), "g").unwrap();
762        conn.with(|connection| {
763            connection.execute(
764                "UPDATE _graph_vertices SET properties_json = '{' WHERE vertex_id = 1",
765                [],
766            )?;
767            Ok(())
768        })
769        .unwrap();
770
771        store.add_vertex(Vertex::new(2, "person"), "g").unwrap();
772        let reopened = SQLiteGraphStore::open(conn, None).unwrap();
773        assert!(reopened.get_vertex(2).unwrap().is_some());
774        assert!(reopened.get_vertex(1).is_err());
775    }
776
777    #[test]
778    fn allocated_label_sequence_survives_reopen() {
779        let conn = ManagedConnection::open_in_memory().unwrap();
780        let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
781        store.create_graph("g").unwrap();
782        let first = store.allocate_vertex_id("person", "g").unwrap();
783        drop(store);
784
785        let mut reopened = SQLiteGraphStore::open(conn, None).unwrap();
786        let second = reopened.allocate_vertex_id("person", "g").unwrap();
787        assert_ne!(first, second);
788        assert!(second > first);
789    }
790}