1use std::collections::{BTreeMap, BTreeSet};
42
43use rusqlite::params;
44use uqa_core::{Edge, EdgeId, Value, Vertex, VertexId};
45use uqa_storage::{ManagedConnection, SQLiteError};
46
47use crate::memory_store::MemoryGraphStore;
48use crate::store::{GraphStore, GraphStoreError, GraphStoreResult};
49use crate::types::Direction;
50
51const LEGACY_PROPERTIES_FORMAT: i64 = 1;
52const TAGGED_PROPERTIES_FORMAT: i64 = 2;
53
54pub struct SQLiteGraphStore {
55 inner: MemoryGraphStore,
56 conn: ManagedConnection,
57 vtx_table: String,
58 edge_table: String,
59 member_table: String,
60 catalog_table: String,
61}
62
63fn graph_store_error(error: &GraphStoreError) -> SQLiteError {
64 SQLiteError::StorageBackend(error.to_string())
65}
66
67impl SQLiteGraphStore {
68 pub fn open(conn: ManagedConnection, table_name: Option<&str>) -> Result<Self, SQLiteError> {
71 let suffix = table_name.unwrap_or("");
72 if !suffix
73 .chars()
74 .all(|character| character.is_ascii_alphanumeric() || character == '_')
75 {
76 return Err(SQLiteError::StorageBackend(format!(
77 "invalid graph table suffix {suffix:?}"
78 )));
79 }
80 let (vtx, edg, mem, cat) = if suffix.is_empty() {
81 (
82 "_graph_vertices".to_string(),
83 "_graph_edges".to_string(),
84 "_graph_membership".to_string(),
85 "_graph_catalog".to_string(),
86 )
87 } else {
88 (
89 format!("_graph_vertices_{suffix}"),
90 format!("_graph_edges_{suffix}"),
91 format!("_graph_membership_{suffix}"),
92 format!("_graph_catalog_{suffix}"),
93 )
94 };
95
96 let mut store = Self {
97 inner: MemoryGraphStore::new(),
98 conn,
99 vtx_table: vtx,
100 edge_table: edg,
101 member_table: mem,
102 catalog_table: cat,
103 };
104 store.ensure_tables()?;
105 store.load_from_sqlite()?;
106 Ok(store)
107 }
108
109 fn ensure_tables(&self) -> Result<(), SQLiteError> {
110 let v = &self.vtx_table;
111 let e = &self.edge_table;
112 let m = &self.member_table;
113 let c = &self.catalog_table;
114 self.conn.with(|conn| {
115 conn.execute_batch(&format!(
116 r#"
117 CREATE TABLE IF NOT EXISTS "{v}" (
118 vertex_id INTEGER PRIMARY KEY,
119 label TEXT NOT NULL DEFAULT '',
120 properties_json TEXT NOT NULL,
121 properties_format INTEGER NOT NULL DEFAULT 2
122 CHECK (properties_format IN (1, 2))
123 );
124 CREATE TABLE IF NOT EXISTS "{e}" (
125 edge_id INTEGER PRIMARY KEY,
126 source_id INTEGER NOT NULL,
127 target_id INTEGER NOT NULL,
128 label TEXT NOT NULL,
129 properties_json TEXT NOT NULL,
130 properties_format INTEGER NOT NULL DEFAULT 2
131 CHECK (properties_format IN (1, 2))
132 );
133 CREATE TABLE IF NOT EXISTS "{m}" (
134 graph TEXT NOT NULL,
135 entity_kind TEXT NOT NULL CHECK (entity_kind IN ('v', 'e')),
136 entity_id INTEGER NOT NULL,
137 PRIMARY KEY (graph, entity_kind, entity_id)
138 );
139 CREATE TABLE IF NOT EXISTS "{c}" (
140 name TEXT PRIMARY KEY,
141 registry_json TEXT NOT NULL DEFAULT '{{}}'
142 );
143 CREATE INDEX IF NOT EXISTS "{e}_source_idx" ON "{e}" (source_id);
144 CREATE INDEX IF NOT EXISTS "{e}_target_idx" ON "{e}" (target_id);
145 CREATE INDEX IF NOT EXISTS "{m}_entity_idx" ON "{m}" (entity_kind, entity_id);
146 "#
147 ))?;
148 let mut columns = conn.prepare(&format!("PRAGMA table_info(\"{c}\")"))?;
149 let has_registry = columns
150 .query_map([], |row| row.get::<_, String>(1))?
151 .collect::<Result<Vec<_>, _>>()?
152 .iter()
153 .any(|column| column == "registry_json");
154 if !has_registry {
155 conn.execute(
156 &format!(
157 "ALTER TABLE \"{c}\" ADD COLUMN registry_json TEXT NOT NULL DEFAULT '{{}}'"
158 ),
159 [],
160 )?;
161 }
162 for table in [&v, &e] {
163 let mut columns = conn.prepare(&format!("PRAGMA table_info(\"{table}\")"))?;
164 let has_properties_format = columns
165 .query_map([], |row| row.get::<_, String>(1))?
166 .collect::<Result<Vec<_>, _>>()?
167 .iter()
168 .any(|column| column == "properties_format");
169 if !has_properties_format {
170 conn.execute(
174 &format!(
175 "ALTER TABLE \"{table}\" ADD COLUMN properties_format \
176 INTEGER NOT NULL DEFAULT {LEGACY_PROPERTIES_FORMAT} \
177 CHECK (properties_format IN (1, 2))"
178 ),
179 [],
180 )?;
181 }
182 }
183 Ok(())
184 })
185 }
186
187 #[expect(
188 clippy::too_many_lines,
189 reason = "one read transaction reconstructs a mutually consistent graph snapshot"
190 )]
191 fn load_from_sqlite(&mut self) -> Result<(), SQLiteError> {
192 let v = self.vtx_table.clone();
193 let e = self.edge_table.clone();
194 let m = self.member_table.clone();
195 let c = self.catalog_table.clone();
196 self.conn.with(|conn| {
197 let mut stmt = conn.prepare(&format!("SELECT name, registry_json FROM \"{c}\""))?;
199 let graphs: Vec<(String, String)> = stmt
200 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
201 .collect::<Result<_, _>>()?;
202 for (name, registry_json) in &graphs {
203 self.inner.create_graph(name);
204 let registry = serde_json::from_str(registry_json).map_err(SQLiteError::from)?;
205 self.inner.import_label_registry(name, ®istry);
206 }
207
208 let mut stmt = conn.prepare(&format!(
210 "SELECT vertex_id, label, properties_json, properties_format FROM \"{v}\""
211 ))?;
212 for row in stmt.query_map([], |r| {
213 Ok((
214 r.get::<_, i64>(0)?,
215 r.get::<_, String>(1)?,
216 r.get::<_, String>(2)?,
217 r.get::<_, i64>(3)?,
218 ))
219 })? {
220 let (vid, label, props_json, properties_format) = row?;
221 let properties = decode_properties(&props_json, properties_format)?;
222 let vertex = Vertex {
223 vertex_id: decode_graph_id("vertex", vid)?,
224 label,
225 properties,
226 };
227 self.inner
231 .insert_raw_vertex(vertex)
232 .map_err(|error| SQLiteError::StorageBackend(error.to_string()))?;
233 }
234
235 let mut stmt = conn.prepare(&format!(
237 "SELECT edge_id, source_id, target_id, label, properties_json, \
238 properties_format FROM \"{e}\""
239 ))?;
240 for row in stmt.query_map([], |r| {
241 Ok((
242 r.get::<_, i64>(0)?,
243 r.get::<_, i64>(1)?,
244 r.get::<_, i64>(2)?,
245 r.get::<_, String>(3)?,
246 r.get::<_, String>(4)?,
247 r.get::<_, i64>(5)?,
248 ))
249 })? {
250 let (eid, src, tgt, label, props_json, properties_format) = row?;
251 let properties = decode_properties(&props_json, properties_format)?;
252 let edge = Edge {
253 edge_id: decode_graph_id("edge", eid)?,
254 source_id: decode_graph_id("edge source", src)?,
255 target_id: decode_graph_id("edge target", tgt)?,
256 label,
257 properties,
258 };
259 self.inner
260 .insert_raw_edge(edge)
261 .map_err(|error| SQLiteError::StorageBackend(error.to_string()))?;
262 }
263
264 let mut stmt = conn.prepare(&format!(
266 "SELECT graph, entity_kind, entity_id FROM \"{m}\""
267 ))?;
268 let memberships = stmt
269 .query_map([], |r| {
270 Ok((
271 r.get::<_, String>(0)?,
272 r.get::<_, String>(1)?,
273 r.get::<_, i64>(2)?,
274 ))
275 })?
276 .collect::<Result<Vec<_>, _>>()?;
277 let mut decoded_memberships = Vec::with_capacity(memberships.len());
278 for (graph, kind, id) in memberships {
279 if !self.inner.has_graph(&graph) {
280 return Err(SQLiteError::StorageBackend(format!(
281 "membership references unknown graph {graph:?}"
282 )));
283 }
284 let id = decode_graph_id("membership", id)?;
285 match kind.as_str() {
286 "v" if self.inner.get_vertex(id).is_some() => {}
287 "v" => {
288 return Err(SQLiteError::StorageBackend(format!(
289 "graph {graph:?} references missing vertex {id}"
290 )));
291 }
292 "e" if self.inner.get_edge(id).is_some() => {}
293 "e" => {
294 return Err(SQLiteError::StorageBackend(format!(
295 "graph {graph:?} references missing edge {id}"
296 )));
297 }
298 _ => {
299 return Err(SQLiteError::StorageBackend(format!(
300 "invalid graph membership kind {kind:?}"
301 )));
302 }
303 }
304 decoded_memberships.push((graph, kind, id));
305 }
306 for (graph, kind, id) in &decoded_memberships {
310 if kind == "v" {
311 self.inner
312 .attach_vertex(*id, graph)
313 .map_err(|error| SQLiteError::StorageBackend(error.to_string()))?;
314 }
315 }
316 for (graph, kind, id) in &decoded_memberships {
317 if kind == "e" {
318 self.inner
319 .attach_edge(*id, graph)
320 .map_err(|error| SQLiteError::StorageBackend(error.to_string()))?;
321 }
322 }
323 for (name, _) in &graphs {
324 self.inner.rebuild_label_registry_from_ids(name);
325 }
326 Ok(())
327 })
328 }
329
330 #[expect(
335 clippy::too_many_lines,
336 reason = "one savepoint writes every graph registry before memory publication"
337 )]
338 fn persist_snapshot(&self, snapshot: &MemoryGraphStore) -> Result<(), SQLiteError> {
339 let graph_rows: Vec<(String, String)> = snapshot
340 .graph_names()
341 .into_iter()
342 .map(|name| {
343 let registry = serde_json::to_string(&snapshot.label_registry(&name))?;
344 Ok((name, registry))
345 })
346 .collect::<Result<_, SQLiteError>>()?;
347 let vertex_rows: Vec<(i64, String, String)> = snapshot
348 .vertices()
349 .into_values()
350 .map(|vertex| {
351 Ok((
352 encode_graph_id("vertex", vertex.vertex_id)?,
353 vertex.label,
354 serde_json::to_string(&vertex.properties)?,
355 ))
356 })
357 .collect::<Result<_, SQLiteError>>()?;
358 let edge_rows: Vec<(i64, i64, i64, String, String)> = snapshot
359 .edges()
360 .into_values()
361 .map(|edge| {
362 Ok((
363 encode_graph_id("edge", edge.edge_id)?,
364 encode_graph_id("edge source", edge.source_id)?,
365 encode_graph_id("edge target", edge.target_id)?,
366 edge.label,
367 serde_json::to_string(&edge.properties)?,
368 ))
369 })
370 .collect::<Result<_, SQLiteError>>()?;
371 let mut memberships = Vec::new();
372 for (graph, _) in &graph_rows {
373 for vertex_id in snapshot
374 .vertex_ids_in_graph(graph)
375 .map_err(|error| graph_store_error(&error))?
376 {
377 memberships.push((
378 graph.clone(),
379 "v",
380 encode_graph_id("vertex membership", vertex_id)?,
381 ));
382 }
383 for edge_id in snapshot
384 .out_edge_ids_for_graph(graph)
385 .map_err(|error| SQLiteError::StorageBackend(error.to_string()))?
386 {
387 memberships.push((
388 graph.clone(),
389 "e",
390 encode_graph_id("edge membership", edge_id)?,
391 ));
392 }
393 }
394
395 let vertex_table = self.vtx_table.clone();
396 let edge_table = self.edge_table.clone();
397 let member_table = self.member_table.clone();
398 let catalog_table = self.catalog_table.clone();
399 self.conn.with_mut(|connection| {
400 let savepoint = connection.savepoint()?;
401 savepoint.execute(&format!("DELETE FROM \"{member_table}\""), [])?;
402 savepoint.execute(&format!("DELETE FROM \"{catalog_table}\""), [])?;
403 savepoint.execute(&format!("DELETE FROM \"{edge_table}\""), [])?;
404 savepoint.execute(&format!("DELETE FROM \"{vertex_table}\""), [])?;
405 for (name, registry_json) in &graph_rows {
406 savepoint.execute(
407 &format!(
408 "INSERT INTO \"{catalog_table}\" (name, registry_json) VALUES (?1, ?2)"
409 ),
410 params![name, registry_json],
411 )?;
412 }
413 for (vertex_id, label, properties_json) in &vertex_rows {
414 savepoint.execute(
415 &format!(
416 "INSERT INTO \"{vertex_table}\" \
417 (vertex_id, label, properties_json, properties_format) \
418 VALUES (?1, ?2, ?3, ?4)"
419 ),
420 params![
421 vertex_id,
422 label,
423 properties_json,
424 TAGGED_PROPERTIES_FORMAT
425 ],
426 )?;
427 }
428 for (edge_id, source_id, target_id, label, properties_json) in &edge_rows {
429 savepoint.execute(
430 &format!(
431 "INSERT INTO \"{edge_table}\" \
432 (edge_id, source_id, target_id, label, properties_json, properties_format) \
433 VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
434 ),
435 params![
436 edge_id,
437 source_id,
438 target_id,
439 label,
440 properties_json,
441 TAGGED_PROPERTIES_FORMAT
442 ],
443 )?;
444 }
445 for (graph, kind, entity_id) in &memberships {
446 savepoint.execute(
447 &format!(
448 "INSERT INTO \"{member_table}\" \
449 (graph, entity_kind, entity_id) VALUES (?1, ?2, ?3)"
450 ),
451 params![graph, kind, entity_id],
452 )?;
453 }
454 savepoint.commit()?;
455 Ok(())
456 })
457 }
458
459 fn apply_mutation(
460 &mut self,
461 mutation: impl FnOnce(&mut MemoryGraphStore) -> GraphStoreResult<()>,
462 ) -> Result<(), SQLiteError> {
463 let mut candidate = self.inner.clone();
464 mutation(&mut candidate).map_err(|error| graph_store_error(&error))?;
465 self.persist_snapshot(&candidate)?;
466 self.inner = candidate;
467 Ok(())
468 }
469
470 fn require_graph(&self, graph: &str) -> Result<(), SQLiteError> {
471 if self.inner.has_graph(graph) {
472 Ok(())
473 } else {
474 Err(SQLiteError::StorageBackend(format!(
475 "unknown graph {graph:?}"
476 )))
477 }
478 }
479
480 pub fn as_memory_store(&self) -> &MemoryGraphStore {
481 &self.inner
482 }
483
484 pub fn create_graph(&mut self, name: &str) -> Result<(), SQLiteError> {
485 self.apply_mutation(|store| {
486 store.create_graph(name);
487 Ok(())
488 })
489 }
490
491 pub fn drop_graph(&mut self, name: &str) -> Result<(), SQLiteError> {
492 self.apply_mutation(|store| {
493 store.drop_graph(name);
494 Ok(())
495 })
496 }
497
498 pub fn union_graphs(
499 &mut self,
500 left: &str,
501 right: &str,
502 target: &str,
503 ) -> Result<(), SQLiteError> {
504 self.require_graph(left)?;
505 self.require_graph(right)?;
506 self.apply_mutation(|store| store.union_graphs(left, right, target))
507 }
508
509 pub fn intersect_graphs(
510 &mut self,
511 left: &str,
512 right: &str,
513 target: &str,
514 ) -> Result<(), SQLiteError> {
515 self.require_graph(left)?;
516 self.require_graph(right)?;
517 self.apply_mutation(|store| store.intersect_graphs(left, right, target))
518 }
519
520 pub fn difference_graphs(
521 &mut self,
522 left: &str,
523 right: &str,
524 target: &str,
525 ) -> Result<(), SQLiteError> {
526 self.require_graph(left)?;
527 self.require_graph(right)?;
528 self.apply_mutation(|store| store.difference_graphs(left, right, target))
529 }
530
531 pub fn copy_graph(&mut self, source: &str, target: &str) -> Result<(), SQLiteError> {
532 self.require_graph(source)?;
533 self.apply_mutation(|store| store.copy_graph(source, target))
534 }
535
536 pub fn add_vertex(&mut self, vertex: Vertex, graph: &str) -> Result<(), SQLiteError> {
537 self.apply_mutation(|store| store.add_vertex(vertex, graph))
538 }
539
540 pub fn add_edge(&mut self, edge: Edge, graph: &str) -> Result<(), SQLiteError> {
541 self.apply_mutation(|store| store.add_edge(edge, graph))
542 }
543
544 pub fn remove_vertex(&mut self, vertex_id: VertexId, graph: &str) -> Result<(), SQLiteError> {
545 self.apply_mutation(|store| store.remove_vertex(vertex_id, graph))
546 }
547
548 pub fn remove_edge(&mut self, edge_id: EdgeId, graph: &str) -> Result<(), SQLiteError> {
549 self.apply_mutation(|store| store.remove_edge(edge_id, graph))
550 }
551
552 pub fn allocate_vertex_id(
553 &mut self,
554 label: &str,
555 graph: &str,
556 ) -> Result<VertexId, SQLiteError> {
557 self.require_graph(graph)?;
558 let mut candidate = self.inner.clone();
559 let id = candidate
560 .allocate_vertex_id(label, graph)
561 .map_err(|error| SQLiteError::StorageBackend(error.to_string()))?;
562 self.persist_snapshot(&candidate)?;
563 self.inner = candidate;
564 Ok(id)
565 }
566
567 pub fn allocate_edge_id(&mut self, label: &str, graph: &str) -> Result<EdgeId, SQLiteError> {
568 self.require_graph(graph)?;
569 let mut candidate = self.inner.clone();
570 let id = candidate
571 .allocate_edge_id(label, graph)
572 .map_err(|error| SQLiteError::StorageBackend(error.to_string()))?;
573 self.persist_snapshot(&candidate)?;
574 self.inner = candidate;
575 Ok(id)
576 }
577
578 pub fn clear(&mut self) -> Result<(), SQLiteError> {
579 self.apply_mutation(|store| {
580 store.clear();
581 Ok(())
582 })
583 }
584
585 pub fn graph_names(&self) -> Vec<String> {
586 self.inner.graph_names()
587 }
588
589 pub fn has_graph(&self, name: &str) -> bool {
590 self.inner.has_graph(name)
591 }
592
593 pub fn neighbors(
594 &self,
595 vertex_id: VertexId,
596 label: Option<&str>,
597 direction: Direction,
598 graph: &str,
599 ) -> Result<Vec<VertexId>, SQLiteError> {
600 self.require_graph(graph)?;
601 self.inner
602 .neighbors(vertex_id, label, direction, graph)
603 .map_err(|error| graph_store_error(&error))
604 }
605
606 pub fn vertices_by_label(&self, label: &str, graph: &str) -> Result<Vec<Vertex>, SQLiteError> {
607 self.require_graph(graph)?;
608 self.inner
609 .vertices_by_label(label, graph)
610 .map_err(|error| graph_store_error(&error))
611 }
612
613 pub fn vertex_ids_by_label(
614 &self,
615 label: &str,
616 graph: &str,
617 ) -> Result<Vec<VertexId>, SQLiteError> {
618 self.require_graph(graph)?;
619 self.inner
620 .vertex_ids_by_label(label, graph)
621 .map_err(|error| graph_store_error(&error))
622 }
623
624 pub fn vertices_in_graph(&self, graph: &str) -> Result<Vec<Vertex>, SQLiteError> {
625 self.require_graph(graph)?;
626 self.inner
627 .vertices_in_graph(graph)
628 .map_err(|error| graph_store_error(&error))
629 }
630
631 pub fn edges_in_graph(&self, graph: &str) -> Result<Vec<Edge>, SQLiteError> {
632 self.require_graph(graph)?;
633 self.inner
634 .edges_in_graph(graph)
635 .map_err(|error| graph_store_error(&error))
636 }
637
638 pub fn vertex_graphs(&self, vertex_id: VertexId) -> BTreeSet<String> {
639 self.inner.vertex_graphs(vertex_id)
640 }
641
642 pub fn get_vertex(&self, vertex_id: VertexId) -> Option<&Vertex> {
643 self.inner.get_vertex(vertex_id)
644 }
645
646 pub fn get_edge(&self, edge_id: EdgeId) -> Option<&Edge> {
647 self.inner.get_edge(edge_id)
648 }
649
650 pub fn vertices(&self) -> BTreeMap<VertexId, Vertex> {
651 self.inner.vertices()
652 }
653
654 pub fn edges(&self) -> BTreeMap<EdgeId, Edge> {
655 self.inner.edges()
656 }
657}
658
659fn decode_graph_id(kind: &str, id: i64) -> Result<u64, SQLiteError> {
660 u64::try_from(id).map_err(|_| {
661 SQLiteError::StorageBackend(format!("invalid negative {kind} id {id} in graph store"))
662 })
663}
664
665fn decode_properties(
666 properties_json: &str,
667 properties_format: i64,
668) -> Result<BTreeMap<String, Value>, SQLiteError> {
669 match properties_format {
670 LEGACY_PROPERTIES_FORMAT => {
671 let raw: BTreeMap<String, serde_json::Value> =
672 serde_json::from_str(properties_json).map_err(SQLiteError::from)?;
673 raw.into_iter()
674 .map(|(key, value)| decode_legacy_value(value).map(|value| (key, value)))
675 .collect()
676 }
677 TAGGED_PROPERTIES_FORMAT => {
678 serde_json::from_str(properties_json).map_err(SQLiteError::from)
679 }
680 other => Err(SQLiteError::StorageBackend(format!(
681 "unsupported graph properties format version {other}"
682 ))),
683 }
684}
685
686fn decode_legacy_value(raw: serde_json::Value) -> Result<Value, SQLiteError> {
692 match raw {
693 serde_json::Value::Array(items) => {
694 let bytes = items
695 .iter()
696 .map(|item| item.as_u64().and_then(|number| u8::try_from(number).ok()))
697 .collect::<Option<Vec<_>>>();
698 if let Some(bytes) = bytes {
699 return Ok(Value::Bytes(bytes));
700 }
701 items
702 .into_iter()
703 .map(decode_legacy_value)
704 .collect::<Result<Vec<_>, _>>()
705 .map(Value::List)
706 }
707 serde_json::Value::Object(map) => {
708 if map
709 .get("$uqa_type")
710 .and_then(serde_json::Value::as_str)
711 .is_some()
712 {
713 let tagged: Value = serde_json::from_value(serde_json::Value::Object(map.clone()))
714 .map_err(SQLiteError::from)?;
715 if !matches!(tagged, Value::Map(_)) {
716 return Ok(tagged);
717 }
718 }
719 map.into_iter()
720 .map(|(key, value)| decode_legacy_value(value).map(|value| (key, value)))
721 .collect::<Result<BTreeMap<_, _>, _>>()
722 .map(Value::Map)
723 }
724 scalar => serde_json::from_value(scalar).map_err(SQLiteError::from),
725 }
726}
727
728fn encode_graph_id(kind: &str, id: u64) -> Result<i64, SQLiteError> {
729 i64::try_from(id).map_err(|_| {
730 SQLiteError::StorageBackend(format!("{kind} id {id} exceeds SQLite INTEGER range"))
731 })
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use uqa_core::{Value, Vertex};
738
739 #[test]
740 fn round_trip_through_sqlite() {
741 let conn = ManagedConnection::open_in_memory().unwrap();
742 let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
743 store.create_graph("g").unwrap();
744 store.add_vertex(Vertex::new(1, "person"), "g").unwrap();
745 store.add_vertex(Vertex::new(2, "person"), "g").unwrap();
746 store.add_edge(Edge::new(1, 1, 2, "knows"), "g").unwrap();
747 let other = SQLiteGraphStore::open(conn, None).unwrap();
748 assert!(other.has_graph("g"));
749 let vs = other.vertices_in_graph("g").unwrap();
750 assert_eq!(vs.len(), 2);
751 let es = other.edges_in_graph("g").unwrap();
752 assert_eq!(es.len(), 1);
753 }
754
755 #[test]
756 fn legacy_raw_bytes_and_edge_first_memberships_survive_reopen() {
757 let conn = ManagedConnection::open_in_memory().unwrap();
758 conn.with(|connection| {
759 connection.execute_batch(
762 r#"
763 CREATE TABLE _graph_vertices (
764 vertex_id INTEGER PRIMARY KEY,
765 label TEXT NOT NULL DEFAULT '',
766 properties_json TEXT NOT NULL
767 );
768 CREATE TABLE _graph_edges (
769 edge_id INTEGER PRIMARY KEY,
770 source_id INTEGER NOT NULL,
771 target_id INTEGER NOT NULL,
772 label TEXT NOT NULL,
773 properties_json TEXT NOT NULL
774 );
775 CREATE TABLE _graph_membership (
776 graph TEXT NOT NULL,
777 entity_kind TEXT NOT NULL,
778 entity_id INTEGER NOT NULL,
779 PRIMARY KEY (graph, entity_kind, entity_id)
780 );
781 CREATE TABLE _graph_catalog (name TEXT PRIMARY KEY);
782 INSERT INTO _graph_catalog (name) VALUES ('g');
783 INSERT INTO _graph_vertices
784 (vertex_id, label, properties_json)
785 VALUES
786 (1, 'person', '{"bytes":[1,2],"nested":[[3,4]],"list":[256]}'),
787 (2, 'person', '{}');
788 INSERT INTO _graph_edges
789 (edge_id, source_id, target_id, label, properties_json)
790 VALUES (10, 1, 2, 'knows', '{"bytes":[5,6]}');
791 INSERT INTO _graph_membership
792 (graph, entity_kind, entity_id) VALUES ('g', 'e', 10);
793 INSERT INTO _graph_membership
794 (graph, entity_kind, entity_id) VALUES ('g', 'v', 1);
795 INSERT INTO _graph_membership
796 (graph, entity_kind, entity_id) VALUES ('g', 'v', 2);
797 "#,
798 )?;
799 Ok(())
800 })
801 .unwrap();
802
803 let reopened = SQLiteGraphStore::open(conn, None).unwrap();
804 let vertex = reopened.get_vertex(1).unwrap();
805 assert_eq!(vertex.properties["bytes"], Value::Bytes(vec![1, 2]));
806 assert_eq!(
807 vertex.properties["nested"],
808 Value::List(vec![Value::Bytes(vec![3, 4])])
809 );
810 assert_eq!(
811 vertex.properties["list"],
812 Value::List(vec![Value::Int(256)])
813 );
814 assert_eq!(
815 reopened.get_edge(10).unwrap().properties["bytes"],
816 Value::Bytes(vec![5, 6])
817 );
818 }
819
820 #[test]
821 fn list_and_explicit_bytes_properties_remain_distinct_after_reopen() {
822 let conn = ManagedConnection::open_in_memory().unwrap();
823 let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
824 store.create_graph("g").unwrap();
825 let mut vertex = Vertex::new(1, "payload");
826 vertex.properties.insert(
827 "list".into(),
828 Value::List(vec![Value::Int(1), Value::Int(2)]),
829 );
830 vertex
831 .properties
832 .insert("bytes".into(), Value::Bytes(vec![1, 2]));
833 store.add_vertex(vertex, "g").unwrap();
834 drop(store);
835
836 let reopened = SQLiteGraphStore::open(conn, None).unwrap();
837 let restored = reopened.get_vertex(1).unwrap();
838 assert_eq!(
839 restored.properties["list"],
840 Value::List(vec![Value::Int(1), Value::Int(2)])
841 );
842 assert_eq!(restored.properties["bytes"], Value::Bytes(vec![1, 2]));
843 }
844
845 #[test]
846 fn failed_persistence_does_not_publish_memory_or_partial_disk_state() {
847 let conn = ManagedConnection::open_in_memory().unwrap();
848 let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
849 store.create_graph("g").unwrap();
850 store.add_vertex(Vertex::new(1, "person"), "g").unwrap();
851 conn.with(|connection| {
852 connection.execute_batch(
853 r#"
854 CREATE TRIGGER fail_graph_membership
855 BEFORE INSERT ON "_graph_membership"
856 WHEN NEW.entity_id = 2
857 BEGIN
858 SELECT RAISE(ABORT, 'forced graph persistence failure');
859 END;
860 "#,
861 )?;
862 Ok(())
863 })
864 .unwrap();
865
866 assert!(store.add_vertex(Vertex::new(2, "person"), "g").is_err());
867 assert!(store.get_vertex(1).is_some());
868 assert!(store.get_vertex(2).is_none());
869
870 conn.with(|connection| {
871 connection.execute_batch("DROP TRIGGER fail_graph_membership")?;
872 Ok(())
873 })
874 .unwrap();
875 let reopened = SQLiteGraphStore::open(conn, None).unwrap();
876 assert_eq!(reopened.vertices_in_graph("g").unwrap().len(), 1);
877 assert!(reopened.get_vertex(2).is_none());
878 }
879
880 #[test]
881 fn corrupt_property_json_is_an_open_error() {
882 let conn = ManagedConnection::open_in_memory().unwrap();
883 let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
884 store.create_graph("g").unwrap();
885 store.add_vertex(Vertex::new(1, "person"), "g").unwrap();
886 conn.with(|connection| {
887 connection.execute(
888 "UPDATE _graph_vertices SET properties_json = '{' WHERE vertex_id = 1",
889 [],
890 )?;
891 Ok(())
892 })
893 .unwrap();
894
895 assert!(SQLiteGraphStore::open(conn, None).is_err());
896 }
897
898 #[test]
899 fn allocated_label_sequence_survives_reopen() {
900 let conn = ManagedConnection::open_in_memory().unwrap();
901 let mut store = SQLiteGraphStore::open(conn.clone(), None).unwrap();
902 store.create_graph("g").unwrap();
903 let first = store.allocate_vertex_id("person", "g").unwrap();
904 drop(store);
905
906 let mut reopened = SQLiteGraphStore::open(conn, None).unwrap();
907 let second = reopened.allocate_vertex_id("person", "g").unwrap();
908 assert_ne!(first, second);
909 assert!(second > first);
910 }
911}