Skip to main content

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