1use crate::ingest::{IngestOptions, IngestReport};
2use crate::subscription::{
3 event_matches, DbEvent, SubEntry, SubFilter, SubInner, Subscription, DEFAULT_SUB_CAPACITY,
4};
5use core_query::cypher::ast::ArithOp;
6use core_query::cypher::{
7 execute, is_subscribable, is_write_tokens, lex, parse, parse_write, plan, MatchDeleteNodeStmt,
8 NodePat, Operand, Params, Pattern, PlanOp, Query, RetItem, RetVal, WriteStatement,
9};
10use core_query::{eval_filter, expand, neighborhood, Dir, Filter, GraphView, ResultSet};
11use core_rules::{
12 evaluate, EngineEdgeDelta, GraphMut, NodeView, Predicate, RuleDef, RuleEngine, RuleIvfExport,
13 ViewDef, ViewStore,
14};
15use core_storage::fs::{FileId, Fs, FsIntrospect, RealFs};
16use core_storage::fulltext::FulltextIndex;
17use core_storage::wal::{decode_all, encode_record, WalRecord};
18use core_storage::{
19 ColumnStore, Direction, EdgeProps, GraphError, IdMap, Interner, Result, Topology, Value,
20};
21use serde::{Deserialize, Serialize};
22use std::collections::{BTreeMap, BTreeSet};
23
24#[cfg(test)]
28thread_local! {
29 static DELTA_COPY_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
30}
31
32pub(crate) struct QuerySubEntry {
41 ops: Vec<PlanOp>,
43 columns: Vec<String>,
45 prev_row_map: std::collections::HashMap<String, Vec<Option<Value>>>,
48 inner: std::sync::Weak<SubInner>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77#[serde(rename_all = "snake_case")]
78pub enum MutationEvent {
79 NodeInserted {
80 label: String,
81 key: String,
82 },
83 PropSet {
84 key: String,
85 field: String,
86 },
87 PropRemoved {
88 key: String,
89 field: String,
90 },
91 EdgeInserted {
92 edge_type: String,
93 src: String,
94 dst: String,
95 },
96 EdgeDeleted {
97 edge_type: String,
98 src: String,
99 dst: String,
100 },
101 NodeDeleted {
102 key: String,
103 },
104 RuleCreated {
105 name: String,
106 },
107 RuleDeleted {
108 name: String,
109 },
110 RuleRebuilt {
111 name: String,
112 },
113 BatchApplied {
114 ops: usize,
115 },
116 Ingested {
117 label: String,
118 inserted: usize,
119 },
120}
121
122fn event_from_record(rec: &WalRecord, intern: &Interner, ids: &IdMap) -> Option<MutationEvent> {
123 match rec {
124 WalRecord::InsertNode { label, key, .. } => Some(MutationEvent::NodeInserted {
125 label: label.clone(),
126 key: key.clone(),
127 }),
128 WalRecord::InsertNodeId { label, key, .. } => Some(MutationEvent::NodeInserted {
129 label: intern.resolve(*label)?.to_string(),
130 key: key.clone(),
131 }),
132 WalRecord::SetProp { key, field, .. } => Some(MutationEvent::PropSet {
133 key: key.clone(),
134 field: field.clone(),
135 }),
136 WalRecord::SetPropId { id, field, .. } => Some(MutationEvent::PropSet {
137 key: ids.key_of(*id)?.to_string(),
138 field: intern.resolve(*field)?.to_string(),
139 }),
140 WalRecord::RemoveProp { key, field } => Some(MutationEvent::PropRemoved {
141 key: key.clone(),
142 field: field.clone(),
143 }),
144 WalRecord::InsertEdge {
145 edge_type,
146 src_key,
147 dst_key,
148 } => Some(MutationEvent::EdgeInserted {
149 edge_type: edge_type.clone(),
150 src: src_key.clone(),
151 dst: dst_key.clone(),
152 }),
153 WalRecord::InsertEdgeId { etype, src, dst } => Some(MutationEvent::EdgeInserted {
154 edge_type: intern.resolve(*etype)?.to_string(),
155 src: ids.key_of(*src)?.to_string(),
156 dst: ids.key_of(*dst)?.to_string(),
157 }),
158 WalRecord::DeleteEdge {
159 edge_type,
160 src_key,
161 dst_key,
162 } => Some(MutationEvent::EdgeDeleted {
163 edge_type: edge_type.clone(),
164 src: src_key.clone(),
165 dst: dst_key.clone(),
166 }),
167 WalRecord::DeleteNode { key } => Some(MutationEvent::NodeDeleted { key: key.clone() }),
168 WalRecord::CreateRule { def_bytes } => {
169 let def: RuleDef = bincode::deserialize(def_bytes).ok()?;
170 Some(MutationEvent::RuleCreated { name: def.name })
171 }
172 WalRecord::DeleteRule { name } => Some(MutationEvent::RuleDeleted { name: name.clone() }),
173 WalRecord::RebuildRule { name } => Some(MutationEvent::RuleRebuilt { name: name.clone() }),
174 WalRecord::Batch(_)
175 | WalRecord::CreateView { .. }
176 | WalRecord::DeleteView { .. }
177 | WalRecord::EnableFulltext { .. }
178 | WalRecord::DisableFulltext { .. }
179 | WalRecord::Intern { .. } => None,
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
185pub struct Stats {
186 pub nodes_live: usize,
187 pub nodes_tombstoned: usize,
188 pub edges: u64,
189 pub rules: Vec<RuleStats>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
200pub struct RuleStats {
201 pub name: String,
202 pub edges: u64,
203 pub tripped: bool,
204 pub fires: u64,
205 pub approximate: bool,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212pub struct PredicateSummary {
213 pub kind: String,
214 pub fields: Vec<String>,
215 pub min: Option<f64>,
216 pub tolerance: Option<f64>,
217 pub km: Option<f64>,
218 pub parts: Option<Vec<PredicateSummary>>,
219 #[serde(default)]
222 pub approximate: bool,
223}
224
225impl From<&Predicate> for PredicateSummary {
226 fn from(p: &Predicate) -> Self {
227 match p {
228 Predicate::KeyMatch { field } => PredicateSummary {
229 kind: "key_match".into(),
230 fields: vec![field.clone()],
231 min: None,
232 tolerance: None,
233 km: None,
234 parts: None,
235 approximate: false,
236 },
237 Predicate::FieldEqual { field } => PredicateSummary {
238 kind: "field_equal".into(),
239 fields: vec![field.clone()],
240 min: None,
241 tolerance: None,
242 km: None,
243 parts: None,
244 approximate: false,
245 },
246 Predicate::Overlap { field, min } => PredicateSummary {
247 kind: "overlap".into(),
248 fields: vec![field.clone()],
249 min: Some(*min),
250 tolerance: None,
251 km: None,
252 parts: None,
253 approximate: false,
254 },
255 Predicate::NumericWithin { field, tolerance } => PredicateSummary {
256 kind: "numeric_within".into(),
257 fields: vec![field.clone()],
258 min: None,
259 tolerance: Some(*tolerance),
260 km: None,
261 parts: None,
262 approximate: false,
263 },
264 Predicate::GeoRadius { field, km } => PredicateSummary {
265 kind: "geo_radius".into(),
266 fields: vec![field.clone()],
267 min: None,
268 tolerance: None,
269 km: Some(*km),
270 parts: None,
271 approximate: false,
272 },
273 Predicate::VectorSimilar { field, min } => PredicateSummary {
274 kind: "vector_similar".into(),
275 fields: vec![field.clone()],
276 min: Some(*min),
277 tolerance: None,
278 km: None,
279 parts: None,
280 approximate: false,
281 },
282 Predicate::All(inner) => {
283 let parts: Vec<PredicateSummary> = inner.iter().map(Self::from).collect();
284 let mut fields = Vec::new();
285 for part in &parts {
286 for f in &part.fields {
287 if !fields.contains(f) {
288 fields.push(f.clone());
289 }
290 }
291 }
292 PredicateSummary {
293 kind: "all".into(),
294 fields,
295 min: None,
296 tolerance: None,
297 km: None,
298 parts: Some(parts),
299 approximate: false,
300 }
301 }
302 Predicate::Any(inner) => {
303 let parts: Vec<PredicateSummary> = inner.iter().map(Self::from).collect();
304 let mut fields = Vec::new();
305 for part in &parts {
306 for f in &part.fields {
307 if !fields.contains(f) {
308 fields.push(f.clone());
309 }
310 }
311 }
312 PredicateSummary {
313 kind: "any".into(),
314 fields,
315 min: None,
316 tolerance: None,
317 km: None,
318 parts: Some(parts),
319 approximate: false,
320 }
321 }
322 }
323 }
324}
325
326#[derive(Debug, Clone, PartialEq)]
334pub struct NodeInfo {
335 pub key: String,
336 pub label: String,
337 pub props: BTreeMap<String, Value>,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Default)]
342pub struct DeleteReport {
343 pub manual_edges: u64,
345 pub derived_edges: u64,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
354pub struct EdgeInfo {
355 pub edge_type: String,
356 pub src_key: String,
357 pub dst_key: String,
358 pub derived: bool,
359}
360
361#[derive(Debug, Clone, PartialEq, Serialize)]
364pub struct Explanation {
365 pub rule: String,
366 pub edge_type: String,
367 pub src_key: String,
368 pub dst_key: String,
369 pub weight: Option<f64>,
370 pub predicate: PredicateSummary,
371}
372
373fn write_result_set() -> ResultSet {
375 ResultSet::new(vec![
376 "created".into(),
377 "properties_set".into(),
378 "deleted".into(),
379 ])
380}
381
382fn resolve_merge_set_value(op: &Operand, params: &BTreeMap<String, Value>) -> Result<Value> {
383 match op {
384 Operand::Lit(v) => Ok(v.clone()),
385 Operand::Param(name) => params
386 .get(name)
387 .cloned()
388 .ok_or_else(|| GraphError::QueryError {
389 detail: format!("missing parameter `{name}`"),
390 }),
391 _ => Err(GraphError::QueryError {
392 detail: "ON CREATE/ON MATCH SET value must be a literal or $parameter".into(),
393 }),
394 }
395}
396
397fn operand_node_vars(op: &Operand, out: &mut Vec<String>) {
398 match op {
399 Operand::Prop { var, .. } | Operand::Var(var) => {
400 if !out.contains(var) {
401 out.push(var.clone());
402 }
403 }
404 Operand::FuncCall { args, .. } => {
405 for arg in args {
406 operand_node_vars(arg, out);
407 }
408 }
409 Operand::BinArith { left, right, .. } => {
410 operand_node_vars(left, out);
411 operand_node_vars(right, out);
412 }
413 Operand::Lit(_) | Operand::Param(_) => {}
414 }
415}
416
417fn ret_node_vars(items: &[RetItem]) -> Vec<String> {
418 let mut out = Vec::new();
419 for item in items {
420 match &item.value {
421 RetVal::Var(v) | RetVal::Prop { var: v, .. } => {
422 if !out.contains(v) {
423 out.push(v.clone());
424 }
425 }
426 RetVal::FuncCall { args, .. } => {
427 for arg in args {
428 operand_node_vars(arg, &mut out);
429 }
430 }
431 RetVal::ScalarExpr(op) => operand_node_vars(op, &mut out),
432 RetVal::Agg { .. } => {}
433 }
434 }
435 out
436}
437
438fn add_var(out: &mut Vec<String>, v: &str) {
439 if !out.iter().any(|x| x == v) {
440 out.push(v.to_string());
441 }
442}
443
444fn pattern_node_vars(pats: &[Pattern]) -> Vec<String> {
445 let mut out = Vec::new();
446 for p in pats {
447 if let Some(v) = &p.start.var {
448 add_var(&mut out, v);
449 }
450 for (_, dest) in &p.chain {
451 if let Some(v) = &dest.var {
452 add_var(&mut out, v);
453 }
454 }
455 }
456 out
457}
458
459fn pattern_rel_vars(pats: &[Pattern]) -> Vec<String> {
460 let mut out = Vec::new();
461 for p in pats {
462 for (rel, _) in &p.chain {
463 if rel.hops.is_none() {
464 if let Some(v) = &rel.var {
465 add_var(&mut out, v);
466 }
467 }
468 }
469 }
470 out
471}
472
473fn rel_type_alias(var: &str) -> String {
474 format!("__rt_{var}")
475}
476
477fn ret_column_name(item: &RetItem) -> String {
478 if let Some(alias) = &item.alias {
479 return alias.clone();
480 }
481 match &item.value {
482 RetVal::Var(v) => v.clone(),
483 RetVal::Prop { var, field } => format!("{var}.{field}"),
484 RetVal::FuncCall { name, args } => {
485 let arg_strs: Vec<String> = args
486 .iter()
487 .map(|a| match a {
488 Operand::Var(v) => v.clone(),
489 Operand::Prop { var, field } => format!("{var}.{field}"),
490 Operand::Lit(_) => "<lit>".to_string(),
491 Operand::Param(p) => format!("${p}"),
492 Operand::FuncCall { name: n, .. } => format!("{n}(...)"),
493 Operand::BinArith { .. } => "<arith>".to_string(),
494 })
495 .collect();
496 format!("{name}({})", arg_strs.join(", "))
497 }
498 RetVal::ScalarExpr(_) => "<expr>".to_string(),
499 RetVal::Agg { .. } => "<agg>".to_string(),
500 }
501}
502
503fn eval_set_return_operand<F: Fs>(
504 db: &GraphDb<F>,
505 match_rs: &ResultSet,
506 row: usize,
507 rel_vars: &[String],
508 op: &Operand,
509 params: &BTreeMap<String, Value>,
510) -> Result<Option<Value>> {
511 match op {
512 Operand::Lit(v) => Ok(Some(v.clone())),
513 Operand::Param(name) => params.get(name).cloned().ok_or_else(|| GraphError::QueryError {
514 detail: format!("missing parameter `{name}`"),
515 }).map(Some),
516 Operand::Var(name) if rel_vars.iter().any(|r| r == name) => Err(GraphError::QueryError {
517 detail: format!(
518 "cannot return relationship variable '{name}' bare; return its properties ({name}.field) instead"
519 ),
520 }),
521 Operand::Var(name) => Ok(match_rs.get(row, name).cloned()),
522 Operand::Prop { var, field } => {
523 if rel_vars.iter().any(|r| r == var) {
524 return Ok(None);
525 }
526 let Some(Value::Str(key)) = match_rs.get(row, var) else {
527 return Ok(None);
528 };
529 Ok(db.get_prop(key, field).cloned())
530 }
531 Operand::FuncCall { name, args } => {
532 eval_set_return_func(db, match_rs, row, rel_vars, name, args, params)
533 }
534 Operand::BinArith { op, left, right } => {
535 let lv = eval_set_return_operand(db, match_rs, row, rel_vars, left, params)?;
536 let rv = eval_set_return_operand(db, match_rs, row, rel_vars, right, params)?;
537 eval_set_return_arith(op, lv, rv)
538 }
539 }
540}
541
542fn eval_set_return_arith(
543 op: &ArithOp,
544 lv: Option<Value>,
545 rv: Option<Value>,
546) -> Result<Option<Value>> {
547 match (lv, rv) {
548 (None, _) | (_, None) => Ok(None),
549 (Some(Value::Int(a)), Some(Value::Int(b))) => {
550 let result = match op {
551 ArithOp::Sub => a.saturating_sub(b),
552 ArithOp::Mul => a.saturating_mul(b),
553 ArithOp::Add => a.saturating_add(b),
554 ArithOp::Div => {
555 if b == 0 {
556 return Err(GraphError::QueryError {
557 detail: "division by zero".into(),
558 });
559 }
560 a.checked_div(b).unwrap_or(i64::MAX)
561 }
562 };
563 Ok(Some(Value::Int(result)))
564 }
565 (Some(lv), Some(rv)) => {
566 let a = match &lv {
567 Value::Float(f) => *f,
568 Value::Int(i) => *i as f64,
569 _ => {
570 return Err(GraphError::QueryError {
571 detail: format!("arithmetic operand must be numeric, got {lv:?}"),
572 })
573 }
574 };
575 let b = match &rv {
576 Value::Float(f) => *f,
577 Value::Int(i) => *i as f64,
578 _ => {
579 return Err(GraphError::QueryError {
580 detail: format!("arithmetic operand must be numeric, got {rv:?}"),
581 })
582 }
583 };
584 let result = match op {
585 ArithOp::Sub => a - b,
586 ArithOp::Mul => a * b,
587 ArithOp::Add => a + b,
588 ArithOp::Div => {
589 if b == 0.0 {
590 return Err(GraphError::QueryError {
591 detail: "division by zero".into(),
592 });
593 }
594 a / b
595 }
596 };
597 Ok(Some(Value::Float(result)))
598 }
599 }
600}
601
602fn eval_set_return_func<F: Fs>(
603 db: &GraphDb<F>,
604 match_rs: &ResultSet,
605 row: usize,
606 rel_vars: &[String],
607 name: &str,
608 args: &[Operand],
609 params: &BTreeMap<String, Value>,
610) -> Result<Option<Value>> {
611 let norm = name.to_ascii_lowercase();
612 if norm == "type" {
613 if args.len() != 1 {
614 return Err(GraphError::QueryError {
615 detail: format!("type() requires exactly 1 argument, got {}", args.len()),
616 });
617 }
618 let Operand::Var(rel) = &args[0] else {
619 return Err(GraphError::QueryError {
620 detail: "type() argument must be a relationship variable (e.g. type(r))".into(),
621 });
622 };
623 return Ok(match_rs.get(row, &rel_type_alias(rel)).cloned());
624 }
625 let mut vals = Vec::with_capacity(args.len());
626 for arg in args {
627 vals.push(eval_set_return_operand(
628 db, match_rs, row, rel_vars, arg, params,
629 )?);
630 }
631 match norm.as_str() {
632 "tolower" => {
633 if vals.len() != 1 {
634 return Err(GraphError::QueryError {
635 detail: format!("toLower() requires exactly 1 argument, got {}", vals.len()),
636 });
637 }
638 Ok(vals[0].clone().map(|val| match val {
639 Value::Str(s) => Value::Str(s.to_ascii_lowercase()),
640 other => other,
641 }))
642 }
643 "toupper" => {
644 if vals.len() != 1 {
645 return Err(GraphError::QueryError {
646 detail: format!("toUpper() requires exactly 1 argument, got {}", vals.len()),
647 });
648 }
649 Ok(vals[0].clone().map(|val| match val {
650 Value::Str(s) => Value::Str(s.to_ascii_uppercase()),
651 other => other,
652 }))
653 }
654 "size" => match vals.first().cloned().flatten() {
655 None => Ok(None),
656 Some(Value::Str(s)) => Ok(Some(Value::Int(s.len() as i64))),
657 Some(Value::List(items)) => Ok(Some(Value::Int(items.len() as i64))),
658 Some(_) => Ok(None),
659 },
660 "coalesce" => Ok(vals.into_iter().flatten().next()),
661 "abs" => match vals.first().cloned().flatten() {
662 None => Ok(None),
663 Some(Value::Int(n)) => Ok(Some(Value::Int(n.saturating_abs()))),
664 Some(Value::Float(f)) => Ok(Some(Value::Float(f.abs()))),
665 Some(_) => Ok(None),
666 },
667 "round" => match vals.first().cloned().flatten() {
668 None => Ok(None),
669 Some(Value::Float(f)) => Ok(Some(Value::Float(f.round()))),
670 Some(Value::Int(n)) => Ok(Some(Value::Int(n))),
671 Some(_) => Ok(None),
672 },
673 _ => Err(GraphError::QueryError {
674 detail: format!(
675 "unknown function `{name}`; supported: toLower, toUpper, size, coalesce, type, abs, round, textMatches"
676 ),
677 }),
678 }
679}
680
681fn eval_set_return_item<F: Fs>(
682 db: &GraphDb<F>,
683 match_rs: &ResultSet,
684 row: usize,
685 rel_vars: &[String],
686 item: &RetItem,
687 params: &BTreeMap<String, Value>,
688) -> Result<Option<Value>> {
689 match &item.value {
690 RetVal::Var(v) => eval_set_return_operand(
691 db,
692 match_rs,
693 row,
694 rel_vars,
695 &Operand::Var(v.clone()),
696 params,
697 ),
698 RetVal::Prop { var, field } => eval_set_return_operand(
699 db,
700 match_rs,
701 row,
702 rel_vars,
703 &Operand::Prop {
704 var: var.clone(),
705 field: field.clone(),
706 },
707 params,
708 ),
709 RetVal::FuncCall { name, args } => {
710 eval_set_return_func(db, match_rs, row, rel_vars, name, args, params)
711 }
712 RetVal::ScalarExpr(op) => eval_set_return_operand(db, match_rs, row, rel_vars, op, params),
713 RetVal::Agg { .. } => Err(GraphError::QueryError {
714 detail: "aggregates are not supported in MATCH … SET … RETURN".into(),
715 }),
716 }
717}
718
719fn project_set_return_rows<F: Fs>(
721 db: &GraphDb<F>,
722 rel_vars: &[String],
723 match_rs: &ResultSet,
724 returns: &[RetItem],
725 params: &BTreeMap<String, Value>,
726) -> Result<ResultSet> {
727 let columns: Vec<String> = returns.iter().map(ret_column_name).collect();
728 let mut out = ResultSet::new(columns);
729 for row in 0..match_rs.len() {
730 let mut cells = Vec::with_capacity(returns.len());
731 for item in returns {
732 cells.push(eval_set_return_item(
733 db, match_rs, row, rel_vars, item, params,
734 )?);
735 }
736 out.push_row(cells);
737 }
738 Ok(out)
739}
740
741fn value_as_float_list(v: &Value) -> Option<Vec<f64>> {
746 match v {
747 Value::List(items) => items
748 .iter()
749 .map(|item| match item {
750 Value::Float(f) => Some(*f),
751 Value::Int(i) => Some(*i as f64),
752 _ => None,
753 })
754 .collect(),
755 _ => None,
756 }
757}
758
759fn make_graph_mut<'a>(
760 ids: &'a IdMap,
761 syms: &'a mut Interner,
762 labels: &'a [u32],
763 props: &'a ColumnStore,
764 topo: &'a mut Topology,
765 edge_props: &'a mut EdgeProps,
766) -> GraphMut<'a> {
767 GraphMut {
768 ids,
769 syms,
770 labels,
771 props,
772 topo,
773 edge_props,
774 }
775}
776
777#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
785pub enum FsyncPolicy {
786 #[default]
788 Strict,
789 Batched,
792 Relaxed,
794}
795
796pub struct GraphDb<F: Fs> {
797 fs: F,
798 ids: IdMap,
799 syms: Interner,
800 topo: Topology,
801 props: ColumnStore,
802 labels: Vec<u32>, edge_props: EdgeProps,
804 engine: RuleEngine,
805 view_store: ViewStore,
806 fulltext: FulltextIndex,
809 event_sink: Option<Box<dyn Fn(MutationEvent) + Send + Sync>>,
810 fsync: FsyncPolicy,
812 commit_seq: u64,
816 subscriptions: Vec<SubEntry>,
819 query_subscriptions: Vec<QuerySubEntry>,
822 sub_capacity: usize,
826 read_only: bool,
830 total_wal_commits: u64,
833}
834
835#[derive(Debug, Clone, Default)]
837pub struct SnapshotOptions {
838 pub keep_wal: bool,
843}
844
845impl GraphDb<RealFs> {
846 pub fn open(dir: &std::path::Path) -> Result<Self> {
847 Self::open_with(RealFs::new(dir)?)
848 }
849
850 pub fn open_at(dir: &std::path::Path, commit: u64) -> Result<Self> {
875 Self::open_at_with(RealFs::new(dir)?, commit)
876 }
877}
878
879impl<F: Fs> GraphDb<F> {
880 pub fn open_with(fs: F) -> Result<Self> {
881 let mut db = Self {
882 fs,
883 ids: IdMap::new(),
884 syms: Interner::new(),
885 topo: Topology::new(),
886 props: ColumnStore::new(),
887 labels: Vec::new(),
888 edge_props: EdgeProps::new(),
889 engine: RuleEngine::new(),
890 view_store: ViewStore::new(),
891 fulltext: FulltextIndex::new(),
892 event_sink: None,
893 fsync: FsyncPolicy::Strict,
894 commit_seq: 0,
895 subscriptions: Vec::new(),
896 query_subscriptions: Vec::new(),
897 sub_capacity: DEFAULT_SUB_CAPACITY,
898 read_only: false,
899 total_wal_commits: 0,
900 };
901 let snap_bytes = db.fs.read(FileId::Snapshot)?;
902 if let Some(state) = core_storage::snapshot::decode(&snap_bytes)? {
903 db.restore_snapshot_state(state)?;
904 }
905 let bytes = db.fs.read(FileId::Wal)?;
906 let (records, valid_len) = decode_all(&bytes);
907 if valid_len < bytes.len() {
908 db.fs.write_atomic(FileId::Wal, &bytes[..valid_len])?;
909 }
910 for rec in records {
911 db.apply(&rec)?;
912 let _ = db.engine.drain_deltas();
915 }
916 debug_assert_eq!(
919 db.engine.pending_delta_count(),
920 0,
921 "pending_deltas non-empty after replay — \
922 per-frame drain must run inside the loop to keep memory O(1)"
923 );
924 let _ = db.engine.drain_deltas(); db.view_store
932 .rebuild_all(&mut db.props, &db.topo, &db.ids, &db.syms, &db.labels);
933 db.fulltext
936 .rebuild_all(&db.ids, &db.labels, &db.syms, &db.props);
937 Ok(db)
938 }
939
940 fn restore_snapshot_state(
947 &mut self,
948 state: core_storage::snapshot::SnapshotState,
949 ) -> Result<()> {
950 self.ids = state.ids;
951 self.syms = state.syms;
952 self.topo = state.topo;
953 self.props = state.props;
954 self.labels = state.labels;
955 self.edge_props = state.edge_props;
956 let defs: Vec<RuleDef> = state
957 .rule_defs
958 .iter()
959 .map(|b| {
960 bincode::deserialize(b).map_err(|e| GraphError::Corrupt {
961 detail: format!("snapshot rule_def deserialize: {e}"),
962 })
963 })
964 .collect::<Result<Vec<_>>>()?;
965 self.engine =
966 RuleEngine::from_persist(defs, state.provenance, state.rule_tripped, state.rule_fires);
967 let ivf_state: BTreeMap<String, RuleIvfExport> = state
970 .ivf_state
971 .into_iter()
972 .map(|(name, ps)| {
973 (
974 name,
975 (
976 (ps.src.centroids, ps.src.clusters, ps.src.drift),
977 (ps.dst.centroids, ps.dst.clusters, ps.dst.drift),
978 ),
979 )
980 })
981 .collect();
982 self.engine.reindex_all_load_ivf(
983 &self.ids,
984 &self.syms,
985 &self.labels,
986 &self.props,
987 ivf_state,
988 );
989 self.engine.load_hnsw_state(state.hnsw_state);
991 for def_bytes in &state.view_defs {
996 let def: ViewDef =
997 bincode::deserialize(def_bytes).map_err(|e| GraphError::Corrupt {
998 detail: format!("snapshot view_def deserialize: {e}"),
999 })?;
1000 self.view_store
1001 .restore_view(def)
1002 .map_err(|e| GraphError::Corrupt {
1003 detail: format!("snapshot view restore: {e}"),
1004 })?;
1005 }
1006 Ok(())
1007 }
1008
1009 fn open_at_with(fs: F, commit: u64) -> Result<Self> {
1010 let mut db = Self {
1011 fs,
1012 ids: IdMap::new(),
1013 syms: Interner::new(),
1014 topo: Topology::new(),
1015 props: ColumnStore::new(),
1016 labels: Vec::new(),
1017 edge_props: EdgeProps::new(),
1018 engine: RuleEngine::new(),
1019 view_store: ViewStore::new(),
1020 fulltext: FulltextIndex::new(),
1021 event_sink: None,
1022 fsync: FsyncPolicy::Strict,
1023 commit_seq: 0,
1024 subscriptions: Vec::new(),
1025 query_subscriptions: Vec::new(),
1026 sub_capacity: DEFAULT_SUB_CAPACITY,
1027 read_only: false, total_wal_commits: 0,
1029 };
1030 let snap_bytes = db.fs.read(FileId::Snapshot)?;
1038 if let Some(state) = core_storage::snapshot::decode(&snap_bytes)? {
1039 if state.wal_truncated {
1040 db.restore_snapshot_state(state)?;
1041 }
1042 }
1043 let bytes = db.fs.read(FileId::Wal)?;
1044 let (records, _valid_len) = decode_all(&bytes);
1045 let total = records.len() as u64;
1046 if commit >= total {
1047 return Err(GraphError::CommitOutOfRange { commit, total });
1048 }
1049 for rec in records.into_iter().take((commit + 1) as usize) {
1052 db.apply(&rec)?;
1053 let _ = db.engine.drain_deltas();
1056 }
1057 debug_assert_eq!(
1060 db.engine.pending_delta_count(),
1061 0,
1062 "pending_deltas non-empty after open_at replay — \
1063 per-frame drain must run inside the loop to keep memory O(1)"
1064 );
1065 let _ = db.engine.drain_deltas(); db.view_store
1070 .rebuild_all(&mut db.props, &db.topo, &db.ids, &db.syms, &db.labels);
1071 db.fulltext
1073 .rebuild_all(&db.ids, &db.labels, &db.syms, &db.props);
1074 db.read_only = true;
1075 db.total_wal_commits = total;
1076 Ok(db)
1077 }
1078
1079 pub fn is_read_only(&self) -> bool {
1081 self.read_only
1082 }
1083
1084 pub fn total_wal_commits(&self) -> u64 {
1087 self.total_wal_commits
1088 }
1089
1090 fn apply(&mut self, rec: &WalRecord) -> Result<()> {
1093 match rec {
1094 WalRecord::InsertNode { label, key, props } => {
1095 let id = self.ids.try_insert(key)?;
1096 let sym = self.syms.intern(label);
1097 if self.labels.len() <= id as usize {
1098 self.labels.resize(id as usize + 1, u32::MAX);
1100 }
1101 self.labels[id as usize] = sym;
1102 for (field, value) in props {
1103 self.props.set(id, field, value.clone());
1104 }
1105 self.view_store
1108 .init_node_views(id, &mut self.props, &self.syms, &self.labels);
1109 let cursor = self.engine.pending_delta_count();
1111 let mut eng = std::mem::take(&mut self.engine);
1112 {
1113 let mut gm = make_graph_mut(
1114 &self.ids,
1115 &mut self.syms,
1116 &self.labels,
1117 &self.props,
1118 &mut self.topo,
1119 &mut self.edge_props,
1120 );
1121 eng.on_node_changed(id, None, &mut gm);
1122 }
1123 self.engine = eng;
1124 if !self.view_store.is_empty() {
1127 #[cfg(test)]
1128 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1129 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1130 for d in &new_deltas {
1131 self.view_store.on_edge_changed(
1132 d.etype_sym,
1133 d.src_id,
1134 d.dst_id,
1135 d.fired,
1136 &mut self.props,
1137 &self.topo,
1138 &self.ids,
1139 &self.syms,
1140 &self.labels,
1141 );
1142 }
1143 }
1144 if self.fulltext.has_label(label) {
1146 for (field, value) in props {
1147 if self.fulltext.is_enabled(label, field) {
1148 self.fulltext.add_tokens(id, field, value);
1149 }
1150 }
1151 }
1152 }
1153 WalRecord::InsertEdge {
1154 edge_type,
1155 src_key,
1156 dst_key,
1157 } => {
1158 let src = self.ids.get(src_key).ok_or_else(|| GraphError::Corrupt {
1159 detail: format!("wal replay references unknown key {src_key}"),
1160 })?;
1161 let dst = self.ids.get(dst_key).ok_or_else(|| GraphError::Corrupt {
1162 detail: format!("wal replay references unknown key {dst_key}"),
1163 })?;
1164 let etype = self.syms.intern(edge_type);
1165 self.topo.add_edge(etype, src, dst);
1166 self.view_store.on_edge_changed(
1168 etype,
1169 src,
1170 dst,
1171 true,
1172 &mut self.props,
1173 &self.topo,
1174 &self.ids,
1175 &self.syms,
1176 &self.labels,
1177 );
1178 let cursor = self.engine.pending_delta_count();
1180 let mut eng = std::mem::take(&mut self.engine);
1181 {
1182 let mut gm = make_graph_mut(
1183 &self.ids,
1184 &mut self.syms,
1185 &self.labels,
1186 &self.props,
1187 &mut self.topo,
1188 &mut self.edge_props,
1189 );
1190 eng.on_edge_changed(edge_type, src, dst, &mut gm);
1191 }
1192 self.engine = eng;
1193 if !self.view_store.is_empty() {
1194 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1195 for d in &new_deltas {
1196 self.view_store.on_edge_changed(
1197 d.etype_sym,
1198 d.src_id,
1199 d.dst_id,
1200 d.fired,
1201 &mut self.props,
1202 &self.topo,
1203 &self.ids,
1204 &self.syms,
1205 &self.labels,
1206 );
1207 }
1208 }
1209 }
1210 WalRecord::SetProp { key, field, value } => {
1211 let id = self.ids.get(key).ok_or_else(|| GraphError::Corrupt {
1212 detail: format!("wal replay references unknown key {key}"),
1213 })?;
1214 let old_value = self.props.get(id, field).cloned();
1215 self.props.set(id, field, value.clone());
1216 let cursor = self.engine.pending_delta_count();
1218 let mut eng = std::mem::take(&mut self.engine);
1219 {
1220 let mut gm = make_graph_mut(
1221 &self.ids,
1222 &mut self.syms,
1223 &self.labels,
1224 &self.props,
1225 &mut self.topo,
1226 &mut self.edge_props,
1227 );
1228 eng.on_node_changed(id, Some((field, old_value)), &mut gm);
1229 }
1230 self.engine = eng;
1231 if !self.view_store.is_empty() {
1233 #[cfg(test)]
1234 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1235 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1236 for d in &new_deltas {
1237 self.view_store.on_edge_changed(
1238 d.etype_sym,
1239 d.src_id,
1240 d.dst_id,
1241 d.fired,
1242 &mut self.props,
1243 &self.topo,
1244 &self.ids,
1245 &self.syms,
1246 &self.labels,
1247 );
1248 }
1249 }
1250 self.view_store.on_prop_changed(
1252 id,
1253 field,
1254 &mut self.props,
1255 &self.topo,
1256 &self.ids,
1257 &self.syms,
1258 &self.labels,
1259 );
1260 if self.fulltext.field_indexed(field) {
1262 let label_opt = self.labels.get(id as usize).and_then(|&sym| {
1263 if sym == u32::MAX {
1264 None
1265 } else {
1266 self.syms.resolve(sym)
1267 }
1268 });
1269 if let Some(label) = label_opt {
1270 if self.fulltext.is_enabled(label, field) {
1271 self.fulltext.remove_node_field(id, field);
1272 self.fulltext.add_tokens(id, field, value);
1273 }
1274 }
1275 }
1276 }
1277 WalRecord::Intern { id, text } => {
1278 if let Some(existing) = self.syms.get(text) {
1279 if existing != *id {
1280 return Err(GraphError::Corrupt {
1281 detail: format!(
1282 "wal intern mismatch for {text:?}: have {existing}, record {id}"
1283 ),
1284 });
1285 }
1286 } else {
1287 let got = self.syms.intern(text);
1288 if got != *id {
1289 return Err(GraphError::Corrupt {
1290 detail: format!(
1291 "wal intern assigned {got} for {text:?}, record wanted {id}"
1292 ),
1293 });
1294 }
1295 }
1296 }
1297 WalRecord::InsertNodeId { label, key, props } => {
1298 let id = self.ids.try_insert(key)?;
1299 if self.labels.len() <= id as usize {
1300 self.labels.resize(id as usize + 1, u32::MAX);
1301 }
1302 self.labels[id as usize] = *label;
1303 let label_str = self
1304 .syms
1305 .resolve(*label)
1306 .ok_or_else(|| GraphError::Corrupt {
1307 detail: format!("wal InsertNodeId unknown label intern {label}"),
1308 })?
1309 .to_string();
1310 for (field_sym, value) in props {
1311 let field =
1312 self.syms
1313 .resolve(*field_sym)
1314 .ok_or_else(|| GraphError::Corrupt {
1315 detail: format!(
1316 "wal InsertNodeId unknown field intern {field_sym}"
1317 ),
1318 })?;
1319 self.props.set(id, field, value.clone());
1320 }
1321 self.view_store
1322 .init_node_views(id, &mut self.props, &self.syms, &self.labels);
1323 let cursor = self.engine.pending_delta_count();
1324 let mut eng = std::mem::take(&mut self.engine);
1325 {
1326 let mut gm = make_graph_mut(
1327 &self.ids,
1328 &mut self.syms,
1329 &self.labels,
1330 &self.props,
1331 &mut self.topo,
1332 &mut self.edge_props,
1333 );
1334 eng.on_node_changed(id, None, &mut gm);
1335 }
1336 self.engine = eng;
1337 if !self.view_store.is_empty() {
1338 #[cfg(test)]
1339 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1340 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1341 for d in &new_deltas {
1342 self.view_store.on_edge_changed(
1343 d.etype_sym,
1344 d.src_id,
1345 d.dst_id,
1346 d.fired,
1347 &mut self.props,
1348 &self.topo,
1349 &self.ids,
1350 &self.syms,
1351 &self.labels,
1352 );
1353 }
1354 }
1355 if self.fulltext.has_label(&label_str) {
1356 for (field_sym, value) in props {
1357 let Some(field) = self.syms.resolve(*field_sym) else {
1358 continue;
1359 };
1360 if self.fulltext.is_enabled(&label_str, field) {
1361 self.fulltext.add_tokens(id, field, value);
1362 }
1363 }
1364 }
1365 }
1366 WalRecord::InsertEdgeId { etype, src, dst } => {
1367 if self.ids.is_tombstoned(*src)
1371 || self.ids.is_tombstoned(*dst)
1372 || self.ids.key_of(*src).is_none()
1373 || self.ids.key_of(*dst).is_none()
1374 {
1375 return Ok(());
1376 }
1377 self.topo.add_edge(*etype, *src, *dst);
1378 self.view_store.on_edge_changed(
1379 *etype,
1380 *src,
1381 *dst,
1382 true,
1383 &mut self.props,
1384 &self.topo,
1385 &self.ids,
1386 &self.syms,
1387 &self.labels,
1388 );
1389 if let Some(etype_str) = self.syms.resolve(*etype).map(|s| s.to_string()) {
1392 let cursor = self.engine.pending_delta_count();
1393 let mut eng = std::mem::take(&mut self.engine);
1394 {
1395 let mut gm = make_graph_mut(
1396 &self.ids,
1397 &mut self.syms,
1398 &self.labels,
1399 &self.props,
1400 &mut self.topo,
1401 &mut self.edge_props,
1402 );
1403 eng.on_edge_changed(&etype_str, *src, *dst, &mut gm);
1404 }
1405 self.engine = eng;
1406 if !self.view_store.is_empty() {
1407 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1408 for d in &new_deltas {
1409 self.view_store.on_edge_changed(
1410 d.etype_sym,
1411 d.src_id,
1412 d.dst_id,
1413 d.fired,
1414 &mut self.props,
1415 &self.topo,
1416 &self.ids,
1417 &self.syms,
1418 &self.labels,
1419 );
1420 }
1421 }
1422 }
1423 }
1424 WalRecord::SetPropId { id, field, value } => {
1425 if self.ids.is_tombstoned(*id) || self.ids.key_of(*id).is_none() {
1426 return Ok(());
1427 }
1428 let field_str = self
1429 .syms
1430 .resolve(*field)
1431 .ok_or_else(|| GraphError::Corrupt {
1432 detail: format!("wal SetPropId unknown field intern {field}"),
1433 })?
1434 .to_string();
1435 let old_value = self.props.get(*id, &field_str).cloned();
1436 self.props.set(*id, &field_str, value.clone());
1437 let cursor = self.engine.pending_delta_count();
1438 let mut eng = std::mem::take(&mut self.engine);
1439 {
1440 let mut gm = make_graph_mut(
1441 &self.ids,
1442 &mut self.syms,
1443 &self.labels,
1444 &self.props,
1445 &mut self.topo,
1446 &mut self.edge_props,
1447 );
1448 eng.on_node_changed(*id, Some((field_str.as_str(), old_value)), &mut gm);
1449 }
1450 self.engine = eng;
1451 if !self.view_store.is_empty() {
1452 #[cfg(test)]
1453 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1454 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1455 for d in &new_deltas {
1456 self.view_store.on_edge_changed(
1457 d.etype_sym,
1458 d.src_id,
1459 d.dst_id,
1460 d.fired,
1461 &mut self.props,
1462 &self.topo,
1463 &self.ids,
1464 &self.syms,
1465 &self.labels,
1466 );
1467 }
1468 }
1469 self.view_store.on_prop_changed(
1470 *id,
1471 &field_str,
1472 &mut self.props,
1473 &self.topo,
1474 &self.ids,
1475 &self.syms,
1476 &self.labels,
1477 );
1478 if self.fulltext.field_indexed(&field_str) {
1479 let label_opt = self.labels.get(*id as usize).and_then(|&sym| {
1480 if sym == u32::MAX {
1481 None
1482 } else {
1483 self.syms.resolve(sym)
1484 }
1485 });
1486 if let Some(label) = label_opt {
1487 if self.fulltext.is_enabled(label, &field_str) {
1488 self.fulltext.remove_node_field(*id, &field_str);
1489 self.fulltext.add_tokens(*id, &field_str, value);
1490 }
1491 }
1492 }
1493 }
1494 WalRecord::CreateRule { def_bytes } => {
1495 let def: RuleDef =
1496 bincode::deserialize(def_bytes).map_err(|e| GraphError::Corrupt {
1497 detail: format!("CreateRule def_bytes deserialize failed: {e}"),
1498 })?;
1499 if self.engine.rules().any(|r| r.name == def.name) {
1504 return Ok(());
1505 }
1506 let cursor = self.engine.pending_delta_count();
1507 let mut eng = std::mem::take(&mut self.engine);
1508 let result = {
1509 let mut gm = make_graph_mut(
1510 &self.ids,
1511 &mut self.syms,
1512 &self.labels,
1513 &self.props,
1514 &mut self.topo,
1515 &mut self.edge_props,
1516 );
1517 eng.create_rule(def, &mut gm)
1518 };
1519 self.engine = eng;
1520 result.map_err(|e| GraphError::RuleInvalid { detail: e })?;
1521 if !self.view_store.is_empty() {
1524 #[cfg(test)]
1525 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1526 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1527 for d in &new_deltas {
1528 self.view_store.on_edge_changed(
1529 d.etype_sym,
1530 d.src_id,
1531 d.dst_id,
1532 d.fired,
1533 &mut self.props,
1534 &self.topo,
1535 &self.ids,
1536 &self.syms,
1537 &self.labels,
1538 );
1539 }
1540 }
1541 }
1542 WalRecord::DeleteRule { name } => {
1543 if !self.engine.rules().any(|r| r.name == *name) {
1548 return Ok(());
1549 }
1550 let cursor = self.engine.pending_delta_count();
1551 let mut eng = std::mem::take(&mut self.engine);
1552 let result = {
1553 let mut gm = make_graph_mut(
1554 &self.ids,
1555 &mut self.syms,
1556 &self.labels,
1557 &self.props,
1558 &mut self.topo,
1559 &mut self.edge_props,
1560 );
1561 eng.delete_rule(name, &mut gm)
1562 };
1563 self.engine = eng;
1564 result.map_err(|_| GraphError::RuleNotFound { name: name.clone() })?;
1565 if !self.view_store.is_empty() {
1567 #[cfg(test)]
1568 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1569 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1570 for d in &new_deltas {
1571 self.view_store.on_edge_changed(
1572 d.etype_sym,
1573 d.src_id,
1574 d.dst_id,
1575 d.fired,
1576 &mut self.props,
1577 &self.topo,
1578 &self.ids,
1579 &self.syms,
1580 &self.labels,
1581 );
1582 }
1583 }
1584 }
1585 WalRecord::RemoveProp { key, field } => {
1586 let Some(id) = self.ids.get(key) else {
1590 return Ok(());
1591 };
1592 let old = self.props.get(id, field).cloned();
1593 self.props.remove(id, field);
1594 let cursor = self.engine.pending_delta_count();
1595 let mut eng = std::mem::take(&mut self.engine);
1596 {
1597 let mut gm = make_graph_mut(
1598 &self.ids,
1599 &mut self.syms,
1600 &self.labels,
1601 &self.props,
1602 &mut self.topo,
1603 &mut self.edge_props,
1604 );
1605 eng.on_node_changed(id, Some((field, old)), &mut gm);
1606 }
1607 self.engine = eng;
1608 if !self.view_store.is_empty() {
1610 #[cfg(test)]
1611 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1612 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1613 for d in &new_deltas {
1614 self.view_store.on_edge_changed(
1615 d.etype_sym,
1616 d.src_id,
1617 d.dst_id,
1618 d.fired,
1619 &mut self.props,
1620 &self.topo,
1621 &self.ids,
1622 &self.syms,
1623 &self.labels,
1624 );
1625 }
1626 }
1627 self.view_store.on_prop_changed(
1629 id,
1630 field,
1631 &mut self.props,
1632 &self.topo,
1633 &self.ids,
1634 &self.syms,
1635 &self.labels,
1636 );
1637 if self.fulltext.field_indexed(field) {
1639 self.fulltext.remove_node_field(id, field);
1640 }
1641 }
1642 WalRecord::DeleteEdge {
1643 edge_type,
1644 src_key,
1645 dst_key,
1646 } => {
1647 let Some(src) = self.ids.get(src_key) else {
1650 return Ok(());
1651 };
1652 let Some(dst) = self.ids.get(dst_key) else {
1653 return Ok(());
1654 };
1655 let Some(etype) = self.syms.get(edge_type) else {
1656 return Ok(());
1657 };
1658 self.topo.remove_edge(etype, src, dst);
1659 self.edge_props.remove_edge(etype, src, dst);
1660 self.view_store.on_edge_changed(
1662 etype,
1663 src,
1664 dst,
1665 false,
1666 &mut self.props,
1667 &self.topo,
1668 &self.ids,
1669 &self.syms,
1670 &self.labels,
1671 );
1672 let cursor = self.engine.pending_delta_count();
1674 let mut eng = std::mem::take(&mut self.engine);
1675 {
1676 let mut gm = make_graph_mut(
1677 &self.ids,
1678 &mut self.syms,
1679 &self.labels,
1680 &self.props,
1681 &mut self.topo,
1682 &mut self.edge_props,
1683 );
1684 eng.on_edge_changed(edge_type, src, dst, &mut gm);
1685 }
1686 self.engine = eng;
1687 if !self.view_store.is_empty() {
1688 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1689 for d in &new_deltas {
1690 self.view_store.on_edge_changed(
1691 d.etype_sym,
1692 d.src_id,
1693 d.dst_id,
1694 d.fired,
1695 &mut self.props,
1696 &self.topo,
1697 &self.ids,
1698 &self.syms,
1699 &self.labels,
1700 );
1701 }
1702 }
1703 }
1704 WalRecord::DeleteNode { key } => {
1705 let Some(n) = self.ids.get(key) else {
1714 return Ok(());
1715 };
1716
1717 let cursor = self.engine.pending_delta_count();
1719 let mut eng = std::mem::take(&mut self.engine);
1720 {
1721 let mut gm = make_graph_mut(
1722 &self.ids,
1723 &mut self.syms,
1724 &self.labels,
1725 &self.props,
1726 &mut self.topo,
1727 &mut self.edge_props,
1728 );
1729 eng.on_node_removed(n, &mut gm);
1730 }
1731 self.engine = eng;
1732 if !self.view_store.is_empty() {
1734 #[cfg(test)]
1735 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1736 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1737 for d in &new_deltas {
1738 self.view_store.on_edge_changed(
1739 d.etype_sym,
1740 d.src_id,
1741 d.dst_id,
1742 d.fired,
1743 &mut self.props,
1744 &self.topo,
1745 &self.ids,
1746 &self.syms,
1747 &self.labels,
1748 );
1749 }
1750 }
1751
1752 let etypes: Vec<u32> = self.topo.etypes().collect();
1757 let mut doomed = Vec::new();
1758 for et in &etypes {
1759 for &dst in self.topo.neighbors(*et, Direction::Out, n).as_ref() {
1760 doomed.push((*et, n, dst));
1761 }
1762 for &src in self.topo.neighbors(*et, Direction::In, n).as_ref() {
1763 doomed.push((*et, src, n));
1764 }
1765 }
1766 for (et, s, d) in doomed {
1767 self.topo.remove_edge(et, s, d);
1768 self.edge_props.remove_edge(et, s, d);
1769 self.view_store.on_edge_changed(
1772 et,
1773 s,
1774 d,
1775 false,
1776 &mut self.props,
1777 &self.topo,
1778 &self.ids,
1779 &self.syms,
1780 &self.labels,
1781 );
1782 }
1783
1784 self.props.remove_all(n);
1786 self.fulltext.remove_node(n);
1788
1789 self.ids.delete(key);
1791 if let Some(slot) = self.labels.get_mut(n as usize) {
1792 *slot = u32::MAX;
1793 }
1794 }
1795 WalRecord::Batch(inner) => {
1796 for rec in inner {
1799 self.apply(rec)?;
1800 }
1801 }
1802 WalRecord::RebuildRule { name } => {
1803 if !self.engine.rules().any(|r| r.name == *name) {
1806 return Ok(());
1807 }
1808 let cursor = self.engine.pending_delta_count();
1809 let mut eng = std::mem::take(&mut self.engine);
1810 let result = {
1811 let mut gm = make_graph_mut(
1812 &self.ids,
1813 &mut self.syms,
1814 &self.labels,
1815 &self.props,
1816 &mut self.topo,
1817 &mut self.edge_props,
1818 );
1819 eng.rebuild(name, &mut gm)
1820 };
1821 self.engine = eng;
1822 result.map_err(|_| GraphError::RuleNotFound { name: name.clone() })?;
1823 if !self.view_store.is_empty() {
1825 #[cfg(test)]
1826 DELTA_COPY_COUNT.with(|c| c.set(c.get() + 1));
1827 let new_deltas: Vec<_> = self.engine.pending_deltas_since(cursor).to_vec();
1828 for d in &new_deltas {
1829 self.view_store.on_edge_changed(
1830 d.etype_sym,
1831 d.src_id,
1832 d.dst_id,
1833 d.fired,
1834 &mut self.props,
1835 &self.topo,
1836 &self.ids,
1837 &self.syms,
1838 &self.labels,
1839 );
1840 }
1841 }
1842 }
1843 WalRecord::CreateView { def_bytes } => {
1844 let def: ViewDef =
1845 bincode::deserialize(def_bytes).map_err(|e| GraphError::Corrupt {
1846 detail: format!("CreateView def_bytes deserialize failed: {e}"),
1847 })?;
1848 if self.view_store.has_view(&def.name) {
1850 return Ok(());
1851 }
1852 self.view_store
1853 .create_view(
1854 def,
1855 &mut self.props,
1856 &self.topo,
1857 &self.ids,
1858 &self.syms,
1859 &self.labels,
1860 )
1861 .map_err(|e| GraphError::RuleInvalid { detail: e })?;
1862 }
1863 WalRecord::DeleteView { name } => {
1864 if !self.view_store.has_view(name) {
1866 return Ok(());
1867 }
1868 self.view_store
1869 .delete_view(name, &mut self.props, &self.ids, &self.labels, &self.syms)
1870 .map_err(|_| GraphError::RuleNotFound { name: name.clone() })?;
1871 }
1872 WalRecord::EnableFulltext { label, field } => {
1873 if self.fulltext.is_enabled(label, field) {
1875 return Ok(());
1876 }
1877 self.fulltext.enable(label, field);
1878 let n = self.ids.len() as u32;
1880 for id in 0..n {
1881 let Some(&sym) = self.labels.get(id as usize) else {
1882 continue;
1883 };
1884 if sym == u32::MAX {
1885 continue; }
1887 let Some(lbl) = self.syms.resolve(sym) else {
1888 continue;
1889 };
1890 if lbl != label {
1891 continue;
1892 }
1893 if let Some(value) = self.props.get(id, field) {
1894 let value = value.clone();
1895 self.fulltext.add_tokens(id, field, &value);
1896 }
1897 }
1898 }
1899 WalRecord::DisableFulltext { label, field } => {
1900 if !self.fulltext.is_enabled(label, field) {
1902 return Ok(());
1903 }
1904 if self.fulltext.field_indexed_by_other(label, field) {
1909 if let Some(label_sym) = self.syms.get(label) {
1910 for (node_id, &lsym) in self.labels.iter().enumerate() {
1911 if lsym == label_sym {
1912 self.fulltext.remove_node_field(node_id as u32, field);
1913 }
1914 }
1915 }
1916 }
1917 self.fulltext.disable(label, field);
1918 }
1919 }
1920 Ok(())
1921 }
1922
1923 fn intern_wal(&mut self, s: &str) -> (u32, WalRecord) {
1928 let id = if let Some(id) = self.syms.get(s) {
1929 id
1930 } else {
1931 self.syms.intern(s)
1932 };
1933 (
1934 id,
1935 WalRecord::Intern {
1936 id,
1937 text: s.to_string(),
1938 },
1939 )
1940 }
1941
1942 fn rewrite_wal_dense(&mut self, recs: Vec<WalRecord>) -> Result<Vec<WalRecord>> {
1947 let syms_checkpoint = self.syms.len();
1948 let result = self.rewrite_wal_dense_inner(recs);
1949 if result.is_err() {
1950 self.syms.truncate(syms_checkpoint);
1951 }
1952 result
1953 }
1954
1955 fn rewrite_wal_dense_inner(&mut self, recs: Vec<WalRecord>) -> Result<Vec<WalRecord>> {
1956 let mut out = Vec::with_capacity(recs.len());
1957 let mut pending: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
1959 let mut interned = std::collections::HashSet::<u32>::new();
1960 let mut next = u32::try_from(self.ids.len()).map_err(|_| GraphError::Corrupt {
1961 detail: "id space exhausted".into(),
1962 })?;
1963 let lookup = |ids: &IdMap,
1964 pending: &std::collections::HashMap<String, u32>,
1965 key: &str|
1966 -> Option<u32> { ids.get(key).or_else(|| pending.get(key).copied()) };
1967 for rec in recs {
1968 match rec {
1969 WalRecord::InsertNode { label, key, props } => {
1970 let (label_id, intern) = self.intern_wal(&label);
1971 if interned.insert(label_id) {
1972 out.push(intern);
1973 }
1974 let mut props_id = Vec::with_capacity(props.len());
1975 for (field, value) in props {
1976 let (field_id, intern) = self.intern_wal(&field);
1977 if interned.insert(field_id) {
1978 out.push(intern);
1979 }
1980 props_id.push((field_id, value));
1981 }
1982 if lookup(&self.ids, &pending, &key).is_none() {
1983 pending.insert(key.clone(), next);
1984 next = next.checked_add(1).ok_or_else(|| GraphError::Corrupt {
1985 detail: "id space exhausted".into(),
1986 })?;
1987 }
1988 out.push(WalRecord::InsertNodeId {
1989 label: label_id,
1990 key,
1991 props: props_id,
1992 });
1993 }
1994 WalRecord::SetProp { key, field, value } => {
1995 let id =
1996 lookup(&self.ids, &pending, &key).ok_or_else(|| GraphError::Corrupt {
1997 detail: format!("dense WAL rewrite missing key {key}"),
1998 })?;
1999 let (field_id, intern) = self.intern_wal(&field);
2000 if interned.insert(field_id) {
2001 out.push(intern);
2002 }
2003 out.push(WalRecord::SetPropId {
2004 id,
2005 field: field_id,
2006 value,
2007 });
2008 }
2009 WalRecord::InsertEdge {
2010 edge_type,
2011 src_key,
2012 dst_key,
2013 } => {
2014 let (etype, intern) = self.intern_wal(&edge_type);
2015 if interned.insert(etype) {
2016 out.push(intern);
2017 }
2018 let src = lookup(&self.ids, &pending, &src_key).ok_or_else(|| {
2019 GraphError::Corrupt {
2020 detail: format!("dense WAL rewrite missing src {src_key}"),
2021 }
2022 })?;
2023 let dst = lookup(&self.ids, &pending, &dst_key).ok_or_else(|| {
2024 GraphError::Corrupt {
2025 detail: format!("dense WAL rewrite missing dst {dst_key}"),
2026 }
2027 })?;
2028 out.push(WalRecord::InsertEdgeId { etype, src, dst });
2029 }
2030 other => out.push(other),
2031 }
2032 }
2033 Ok(out)
2034 }
2035
2036 fn log_dense(&mut self, recs: Vec<WalRecord>) -> Result<()> {
2037 let recs = self.rewrite_wal_dense(recs)?;
2038 match recs.len() {
2039 0 => Ok(()),
2040 1 => self.log_then_apply(recs.into_iter().next().unwrap()),
2041 _ => self.log_then_apply(WalRecord::Batch(recs)),
2042 }
2043 }
2044
2045 fn log_then_apply(&mut self, rec: WalRecord) -> Result<()> {
2048 self.log_then_apply_with(rec, None, self.fsync)
2049 }
2050
2051 fn wal_needs_sync(policy: FsyncPolicy, rec: &WalRecord) -> bool {
2060 match policy {
2061 FsyncPolicy::Relaxed => false,
2062 FsyncPolicy::Strict => true,
2063 FsyncPolicy::Batched => match rec {
2064 WalRecord::Batch(inner) => {
2066 inner
2067 .iter()
2068 .filter(|r| !matches!(r, WalRecord::Intern { .. }))
2069 .count()
2070 > 1
2071 }
2072 _ => false,
2073 },
2074 }
2075 }
2076
2077 fn log_then_apply_with(
2099 &mut self,
2100 rec: WalRecord,
2101 ingest: Option<(String, usize)>,
2102 policy: FsyncPolicy,
2103 ) -> Result<()> {
2104 if self.read_only {
2106 return Err(GraphError::ReadOnly);
2107 }
2108 debug_assert_eq!(
2113 self.engine.pending_delta_count(),
2114 0,
2115 "stale engine deltas at log_then_apply_with entry — \
2116 a previous apply arm may have accumulated deltas before erroring; \
2117 the caller must drain_deltas() on any error path before returning"
2118 );
2119 self.fs.append(FileId::Wal, &encode_record(&rec))?;
2120 if Self::wal_needs_sync(policy, &rec) {
2121 self.fs.sync(FileId::Wal)?;
2122 }
2123 let apply_result = self.apply(&rec);
2124 if matches!(&rec, WalRecord::Batch(_)) {
2128 debug_assert!(
2129 apply_result.is_ok(),
2130 "Batch apply returned Err after successful WAL write — \
2131 the validate-then-apply invariant has been violated; \
2132 see log_then_apply_with invariant doc"
2133 );
2134 }
2135 if apply_result.is_err() {
2136 let _ = self.engine.drain_deltas();
2139 let _ = self.engine.take_rebuild_needed();
2140 apply_result?;
2141 }
2142 self.commit_seq += 1;
2143 let seq = self.commit_seq;
2144 let engine_deltas = self.engine.drain_deltas();
2147 self.distribute_events(&rec, &engine_deltas, seq);
2148 self.emit_committed(&rec, ingest);
2149 let rebuilds = self.engine.take_rebuild_needed();
2155 if !matches!(&rec, WalRecord::RebuildRule { .. }) {
2156 let mut failed = Vec::new();
2157 for name in rebuilds {
2158 if self.engine.rules().any(|r| r.name == name) {
2159 if let Err(e) =
2162 self.log_then_apply(WalRecord::RebuildRule { name: name.clone() })
2163 {
2164 eprintln!(
2165 "auto-rebuild of rule {name:?} failed after durable user commit: {e}"
2166 );
2167 failed.push(name);
2168 }
2169 }
2170 }
2171 for name in failed {
2172 self.engine.queue_rebuild_needed(name);
2173 }
2174 }
2175 Ok(())
2176 }
2177
2178 pub fn set_event_sink(&mut self, sink: Box<dyn Fn(MutationEvent) + Send + Sync>) {
2191 self.event_sink = Some(sink);
2192 }
2193
2194 pub fn has_event_sink(&self) -> bool {
2196 self.event_sink.is_some()
2197 }
2198
2199 pub fn set_fsync_policy(&mut self, p: FsyncPolicy) {
2201 self.fsync = p;
2202 }
2203
2204 fn emit(&self, ev: MutationEvent) {
2205 if let Some(sink) = &self.event_sink {
2206 sink(ev);
2207 }
2208 }
2209
2210 fn emit_committed(&self, rec: &WalRecord, ingest: Option<(String, usize)>) {
2211 match rec {
2212 WalRecord::Batch(inner) => {
2213 for r in inner {
2214 if let Some(ev) = event_from_record(r, &self.syms, &self.ids) {
2215 self.emit(ev);
2216 }
2217 }
2218 match ingest {
2219 Some((label, inserted)) => {
2220 self.emit(MutationEvent::Ingested { label, inserted })
2221 }
2222 None => {
2223 let ops = inner
2224 .iter()
2225 .filter(|r| !matches!(r, WalRecord::Intern { .. }))
2226 .count();
2227 if ops > 1 {
2228 self.emit(MutationEvent::BatchApplied { ops });
2229 }
2230 }
2231 }
2232 }
2233 other => {
2234 if let Some(ev) = event_from_record(other, &self.syms, &self.ids) {
2235 self.emit(ev);
2236 }
2237 }
2238 }
2239 }
2240
2241 fn distribute_events(&mut self, rec: &WalRecord, engine_deltas: &[EngineEdgeDelta], seq: u64) {
2254 if self.subscriptions.is_empty() && self.query_subscriptions.is_empty() {
2255 return;
2256 }
2257
2258 if !self.subscriptions.is_empty() {
2259 let write_events: Vec<DbEvent> =
2261 Self::write_events_from_record(rec, seq, &self.syms, &self.ids);
2262
2263 let edge_events: Vec<DbEvent> = engine_deltas
2266 .iter()
2267 .map(|d| {
2268 if d.fired {
2269 let weight = self
2270 .edge_props
2271 .get(d.etype_sym, d.src_id, d.dst_id, "weight")
2272 .and_then(|v| {
2273 if let core_storage::Value::Float(f) = v {
2274 Some(*f)
2275 } else {
2276 None
2277 }
2278 });
2279 DbEvent::EdgeFired {
2280 rule: d.rule.clone(),
2281 src_key: d.src_key.clone(),
2282 dst_key: d.dst_key.clone(),
2283 edge_type: d.edge_type.clone(),
2284 weight,
2285 commit_seq: seq,
2286 }
2287 } else {
2288 DbEvent::EdgeRetracted {
2289 rule: d.rule.clone(),
2290 src_key: d.src_key.clone(),
2291 dst_key: d.dst_key.clone(),
2292 edge_type: d.edge_type.clone(),
2293 commit_seq: seq,
2294 }
2295 }
2296 })
2297 .collect();
2298
2299 self.subscriptions.retain(|entry| {
2301 let Some(inner) = entry.inner.upgrade() else {
2302 return false;
2303 };
2304 for ev in &write_events {
2305 if event_matches(ev, &entry.filter) {
2306 inner.push(ev.clone());
2307 }
2308 }
2309 for ev in &edge_events {
2310 if event_matches(ev, &entry.filter) {
2311 inner.push(ev.clone());
2312 }
2313 }
2314 true
2315 });
2316
2317 if self.subscriptions.is_empty() && self.view_store.is_empty() {
2319 self.engine.set_emit_deltas(false);
2320 }
2321 }
2322
2323 if !self.query_subscriptions.is_empty() {
2327 let mut query_subs = std::mem::take(&mut self.query_subscriptions);
2329 let empty_params = BTreeMap::new();
2330 query_subs.retain_mut(|entry| {
2331 let Some(inner) = entry.inner.upgrade() else {
2332 return false; };
2334 let result = match execute(&self.view(), &entry.ops, &Params(&empty_params)) {
2335 Ok(r) => r,
2336 Err(_) => return true, };
2338 let new_row_map: std::collections::HashMap<String, Vec<Option<Value>>> = (0
2340 ..result.len())
2341 .map(|i| {
2342 let row = result.row(i).to_vec();
2343 let key =
2344 serde_json::to_string(&row).unwrap_or_else(|_| format!("{row:?}"));
2345 (key, row)
2346 })
2347 .collect();
2348 for (key, row) in &entry.prev_row_map {
2350 if !new_row_map.contains_key(key) {
2351 inner.push(DbEvent::QueryRowRemoved {
2352 columns: entry.columns.clone(),
2353 row: row.clone(),
2354 });
2355 }
2356 }
2357 for (key, row) in &new_row_map {
2359 if !entry.prev_row_map.contains_key(key) {
2360 inner.push(DbEvent::QueryRowAdded {
2361 columns: entry.columns.clone(),
2362 row: row.clone(),
2363 });
2364 }
2365 }
2366 entry.prev_row_map = new_row_map;
2367 true
2368 });
2369 self.query_subscriptions = query_subs;
2370 }
2371 }
2372
2373 fn needs_emit_deltas(&self) -> bool {
2376 !self.view_store.is_empty()
2377 || self
2378 .subscriptions
2379 .iter()
2380 .any(|e| e.inner.upgrade().is_some())
2381 }
2382
2383 fn write_events_from_record(
2385 rec: &WalRecord,
2386 seq: u64,
2387 intern: &Interner,
2388 ids: &IdMap,
2389 ) -> Vec<DbEvent> {
2390 match rec {
2391 WalRecord::InsertNode { label, key, .. } => vec![DbEvent::NodeInserted {
2392 label: label.clone(),
2393 key: key.clone(),
2394 commit_seq: seq,
2395 }],
2396 WalRecord::InsertNodeId { label, key, .. } => intern
2401 .resolve(*label)
2402 .map(|label| DbEvent::NodeInserted {
2403 label: label.to_string(),
2404 key: key.clone(),
2405 commit_seq: seq,
2406 })
2407 .into_iter()
2408 .collect(),
2409 WalRecord::SetProp { key, field, .. } => vec![DbEvent::PropSet {
2410 key: key.clone(),
2411 field: field.clone(),
2412 commit_seq: seq,
2413 }],
2414 WalRecord::SetPropId { id, field, .. } => ids
2415 .key_of(*id)
2416 .zip(intern.resolve(*field))
2417 .map(|(key, field)| DbEvent::PropSet {
2418 key: key.to_string(),
2419 field: field.to_string(),
2420 commit_seq: seq,
2421 })
2422 .into_iter()
2423 .collect(),
2424 WalRecord::RemoveProp { key, field } => vec![DbEvent::PropRemoved {
2425 key: key.clone(),
2426 field: field.clone(),
2427 commit_seq: seq,
2428 }],
2429 WalRecord::InsertEdge {
2430 edge_type,
2431 src_key,
2432 dst_key,
2433 } => vec![DbEvent::EdgeInserted {
2434 edge_type: edge_type.clone(),
2435 src: src_key.clone(),
2436 dst: dst_key.clone(),
2437 commit_seq: seq,
2438 }],
2439 WalRecord::InsertEdgeId { etype, src, dst } => (|| {
2440 Some(DbEvent::EdgeInserted {
2441 edge_type: intern.resolve(*etype)?.to_string(),
2442 src: ids.key_of(*src)?.to_string(),
2443 dst: ids.key_of(*dst)?.to_string(),
2444 commit_seq: seq,
2445 })
2446 })()
2447 .into_iter()
2448 .collect(),
2449 WalRecord::DeleteEdge {
2450 edge_type,
2451 src_key,
2452 dst_key,
2453 } => vec![DbEvent::EdgeDeleted {
2454 edge_type: edge_type.clone(),
2455 src: src_key.clone(),
2456 dst: dst_key.clone(),
2457 commit_seq: seq,
2458 }],
2459 WalRecord::DeleteNode { key } => vec![DbEvent::NodeDeleted {
2460 key: key.clone(),
2461 commit_seq: seq,
2462 }],
2463 WalRecord::Batch(inner) => inner
2464 .iter()
2465 .flat_map(|r| Self::write_events_from_record(r, seq, intern, ids))
2466 .collect(),
2467 WalRecord::CreateRule { .. }
2468 | WalRecord::DeleteRule { .. }
2469 | WalRecord::RebuildRule { .. }
2470 | WalRecord::CreateView { .. }
2471 | WalRecord::DeleteView { .. }
2472 | WalRecord::EnableFulltext { .. }
2473 | WalRecord::DisableFulltext { .. }
2474 | WalRecord::Intern { .. } => vec![],
2475 }
2476 }
2477
2478 pub fn subscribe_rule(&mut self, rule_name: &str) -> core_storage::Result<Subscription> {
2485 if self.read_only {
2486 return Err(core_storage::GraphError::ReadOnly);
2487 }
2488 if !self.engine.rules().any(|r| r.name == rule_name) {
2489 return Err(core_storage::GraphError::RuleNotFound {
2490 name: rule_name.to_string(),
2491 });
2492 }
2493 let inner = SubInner::new(self.sub_capacity());
2494 self.subscriptions.push(SubEntry {
2495 filter: SubFilter::Rule(rule_name.to_string()),
2496 inner: std::sync::Arc::downgrade(&inner),
2497 });
2498 self.engine.set_emit_deltas(true);
2499 Ok(Subscription(inner))
2500 }
2501
2502 pub fn subscribe_all_rules(&mut self) -> core_storage::Result<Subscription> {
2508 if self.read_only {
2509 return Err(core_storage::GraphError::ReadOnly);
2510 }
2511 let inner = SubInner::new(self.sub_capacity());
2512 self.subscriptions.push(SubEntry {
2513 filter: SubFilter::AllRules,
2514 inner: std::sync::Arc::downgrade(&inner),
2515 });
2516 self.engine.set_emit_deltas(true);
2517 Ok(Subscription(inner))
2518 }
2519
2520 pub fn subscribe_writes(&mut self) -> core_storage::Result<Subscription> {
2528 if self.read_only {
2529 return Err(core_storage::GraphError::ReadOnly);
2530 }
2531 let inner = SubInner::new(self.sub_capacity());
2532 self.subscriptions.push(SubEntry {
2533 filter: SubFilter::Writes,
2534 inner: std::sync::Arc::downgrade(&inner),
2535 });
2536 self.engine.set_emit_deltas(true);
2537 Ok(Subscription(inner))
2538 }
2539
2540 pub fn subscribe_query(&mut self, cypher: &str) -> Result<Subscription> {
2568 if self.read_only {
2569 return Err(GraphError::ReadOnly);
2570 }
2571 let tokens = lex(cypher).map_err(|e| GraphError::QueryError {
2572 detail: format!("lex: {e}"),
2573 })?;
2574 let ast = parse(&tokens).map_err(|e| GraphError::QueryError {
2575 detail: format!("parse: {e}"),
2576 })?;
2577 let ops = plan(&ast).map_err(|e| GraphError::QueryError {
2578 detail: format!("plan: {e}"),
2579 })?;
2580 if !is_subscribable(&ops) {
2581 return Err(GraphError::QueryError {
2582 detail: "subscribe_query only supports allowlisted plan shapes: \
2583 MATCH (n:Label) WHERE … RETURN … [LIMIT n] or \
2584 MATCH (a)-[r:TYPE]->(b) RETURN … [LIMIT n] (exactly one hop). \
2585 Not supported: multi-hop Expand chains, SKIP (creates \
2586 unstable offset windows), ORDER BY, DISTINCT, aggregates, \
2587 variable-length paths, OPTIONAL MATCH, WITH, UNWIND. \
2588 Use LIMIT to bound re-execution cost."
2589 .to_string(),
2590 });
2591 }
2592 let empty_params = BTreeMap::new();
2595 let initial = execute(&self.view(), &ops, &Params(&empty_params)).map_err(|e| {
2596 GraphError::QueryError {
2597 detail: format!("execute: {e}"),
2598 }
2599 })?;
2600 let columns = initial.columns().to_vec();
2601 let prev_row_map: std::collections::HashMap<String, Vec<Option<Value>>> = (0..initial
2602 .len())
2603 .map(|i| {
2604 let row = initial.row(i).to_vec();
2605 let key = serde_json::to_string(&row).unwrap_or_else(|_| format!("{row:?}"));
2606 (key, row)
2607 })
2608 .collect();
2609 let inner = SubInner::new(self.sub_capacity());
2610 self.query_subscriptions.push(QuerySubEntry {
2611 ops,
2612 columns,
2613 prev_row_map,
2614 inner: std::sync::Arc::downgrade(&inner),
2615 });
2616 Ok(Subscription(inner))
2617 }
2618
2619 fn sub_capacity(&self) -> usize {
2621 self.sub_capacity
2622 }
2623
2624 #[doc(hidden)]
2635 pub fn set_sub_capacity(&mut self, capacity: usize) {
2636 self.sub_capacity = capacity;
2637 }
2638
2639 pub fn batch(&mut self) -> BatchBuilder<'_, F> {
2663 BatchBuilder {
2664 db: self,
2665 ops: Vec::new(),
2666 }
2667 }
2668
2669 pub fn write_batch<C>(&mut self, build: C) -> Result<(usize, usize)>
2709 where
2710 C: FnOnce(&mut BatchBuilder<'_, F>),
2711 {
2712 let mut b = self.batch();
2713 build(&mut b);
2714 b.commit()
2715 }
2716
2717 pub fn ingest(
2726 &mut self,
2727 label: &str,
2728 rows: Vec<BTreeMap<String, Value>>,
2729 opts: &IngestOptions,
2730 ) -> Result<IngestReport> {
2731 self.ingest_with_edges(label, rows, opts, &[])
2732 }
2733
2734 pub fn ingest_with_edges(
2737 &mut self,
2738 label: &str,
2739 rows: Vec<BTreeMap<String, Value>>,
2740 opts: &IngestOptions,
2741 edges: &[(String, String, String)],
2742 ) -> Result<IngestReport> {
2743 crate::ingest::run(self, label, rows, opts, edges)
2744 }
2745
2746 pub fn ingest_json(
2753 &mut self,
2754 label: &str,
2755 json: &str,
2756 opts: &IngestOptions,
2757 ) -> Result<IngestReport> {
2758 crate::ingest::run_json(self, label, json, opts)
2759 }
2760
2761 fn commit_logged_batch(
2762 &mut self,
2763 ops: Vec<BatchOp>,
2764 ingest: Option<(String, usize)>,
2765 ) -> Result<(usize, usize)> {
2766 if self.read_only {
2769 return Err(GraphError::ReadOnly);
2770 }
2771 let recs = {
2772 let mut preview = MutPreview::new(self);
2773 let mut recs = Vec::with_capacity(ops.len());
2774 for op in ops {
2775 match op {
2776 BatchOp::InsertNode { label, key, props } => {
2777 preview.check_insert_node(&key)?;
2778 preview.note_insert_node(&key, &props);
2779 recs.push(WalRecord::InsertNode { label, key, props });
2780 }
2781 BatchOp::InsertEdge {
2782 edge_type,
2783 src_key,
2784 dst_key,
2785 } => {
2786 if preview.prepare_insert_edge(&edge_type, &src_key, &dst_key)? {
2787 preview.note_insert_edge(&edge_type, &src_key, &dst_key);
2788 recs.push(WalRecord::InsertEdge {
2789 edge_type,
2790 src_key,
2791 dst_key,
2792 });
2793 }
2794 }
2795 BatchOp::SetProp { key, field, value } => {
2796 preview.check_live_key(&key)?;
2797 preview.note_set_prop(&key, &field, &value);
2798 recs.push(WalRecord::SetProp { key, field, value });
2799 }
2800 BatchOp::RemoveProp { key, field } => {
2801 if preview.prepare_remove_prop(&key, &field)? {
2802 preview.note_remove_prop(&key, &field);
2803 recs.push(WalRecord::RemoveProp { key, field });
2804 }
2805 }
2806 BatchOp::DeleteEdge {
2807 edge_type,
2808 src_key,
2809 dst_key,
2810 } => {
2811 if preview.prepare_delete_edge(&edge_type, &src_key, &dst_key)? {
2812 preview.note_delete_edge(&edge_type, &src_key, &dst_key);
2813 recs.push(WalRecord::DeleteEdge {
2814 edge_type,
2815 src_key,
2816 dst_key,
2817 });
2818 }
2819 }
2820 BatchOp::DeleteNode { key } => {
2821 preview.check_live_key(&key)?;
2822 preview.note_delete_node(&key);
2823 recs.push(WalRecord::DeleteNode { key });
2824 }
2825 BatchOp::CreateRule(def) => {
2826 preview.check_create_rule(&def)?;
2827 let def_bytes =
2828 bincode::serialize(&def).map_err(|e| GraphError::Corrupt {
2829 detail: format!("serialize rule: {e}"),
2830 })?;
2831 preview.note_create_rule(&def.name);
2832 recs.push(WalRecord::CreateRule { def_bytes });
2833 }
2834 BatchOp::DeleteRule { name } => {
2835 preview.check_delete_rule(&name)?;
2836 preview.note_delete_rule(&name);
2837 recs.push(WalRecord::DeleteRule { name });
2838 }
2839 }
2840 }
2841 recs
2842 };
2843 if recs.is_empty() {
2844 return Ok((0, 0));
2845 }
2846 let recs = self.rewrite_wal_dense(recs)?;
2849 let nodes_inserted = recs
2850 .iter()
2851 .filter(|r| matches!(r, WalRecord::InsertNodeId { .. }))
2852 .count();
2853 let edges_inserted = recs
2854 .iter()
2855 .filter(|r| matches!(r, WalRecord::InsertEdgeId { .. }))
2856 .count();
2857 let policy = match self.fsync {
2860 FsyncPolicy::Relaxed => FsyncPolicy::Relaxed,
2861 FsyncPolicy::Strict | FsyncPolicy::Batched => FsyncPolicy::Batched,
2862 };
2863 self.log_then_apply_with(WalRecord::Batch(recs), ingest, policy)?;
2864 Ok((nodes_inserted, edges_inserted))
2865 }
2866
2867 fn commit_batch(&mut self, ops: Vec<BatchOp>) -> Result<(usize, usize)> {
2868 self.commit_logged_batch(ops, None)
2869 }
2870
2871 pub fn insert_node(
2872 &mut self,
2873 label: &str,
2874 key: &str,
2875 props: Vec<(String, Value)>,
2876 ) -> Result<()> {
2877 if self.read_only {
2878 return Err(GraphError::ReadOnly);
2879 }
2880 MutPreview::new(self).check_insert_node(key)?;
2881 self.log_dense(vec![WalRecord::InsertNode {
2882 label: label.into(),
2883 key: key.into(),
2884 props,
2885 }])
2886 }
2887
2888 pub fn insert_edge(&mut self, edge_type: &str, src_key: &str, dst_key: &str) -> Result<bool> {
2889 if self.read_only {
2890 return Err(GraphError::ReadOnly);
2891 }
2892 if !MutPreview::new(self).prepare_insert_edge(edge_type, src_key, dst_key)? {
2893 return Ok(false);
2894 }
2895 self.log_dense(vec![WalRecord::InsertEdge {
2896 edge_type: edge_type.into(),
2897 src_key: src_key.into(),
2898 dst_key: dst_key.into(),
2899 }])?;
2900 Ok(true)
2901 }
2902
2903 pub fn set_prop(&mut self, key: &str, field: &str, value: Value) -> Result<()> {
2904 if self.read_only {
2905 return Err(GraphError::ReadOnly);
2906 }
2907 if let Some(view_name) = self.view_store.view_for_prop(field) {
2908 return Err(GraphError::ViewPropReadOnly {
2909 view_name: view_name.to_string(),
2910 });
2911 }
2912 MutPreview::new(self).check_live_key(key)?;
2913 self.log_dense(vec![WalRecord::SetProp {
2914 key: key.into(),
2915 field: field.into(),
2916 value,
2917 }])
2918 }
2919
2920 pub fn remove_prop(&mut self, key: &str, field: &str) -> Result<bool> {
2923 if self.read_only {
2924 return Err(GraphError::ReadOnly);
2925 }
2926 if let Some(view_name) = self.view_store.view_for_prop(field) {
2927 return Err(GraphError::ViewPropReadOnly {
2928 view_name: view_name.to_string(),
2929 });
2930 }
2931 if !MutPreview::new(self).prepare_remove_prop(key, field)? {
2932 return Ok(false);
2933 }
2934 self.log_then_apply(WalRecord::RemoveProp {
2935 key: key.into(),
2936 field: field.into(),
2937 })?;
2938 Ok(true)
2939 }
2940
2941 pub fn delete_edge(&mut self, edge_type: &str, src_key: &str, dst_key: &str) -> Result<bool> {
2946 if self.read_only {
2947 return Err(GraphError::ReadOnly);
2948 }
2949 if !MutPreview::new(self).prepare_delete_edge(edge_type, src_key, dst_key)? {
2950 return Ok(false);
2951 }
2952 self.log_then_apply(WalRecord::DeleteEdge {
2953 edge_type: edge_type.into(),
2954 src_key: src_key.into(),
2955 dst_key: dst_key.into(),
2956 })?;
2957 Ok(true)
2958 }
2959
2960 pub fn delete_node(&mut self, key: &str) -> Result<DeleteReport> {
2968 if self.read_only {
2969 return Err(GraphError::ReadOnly);
2970 }
2971 let id = self
2972 .ids
2973 .get(key)
2974 .ok_or_else(|| GraphError::KeyNotFound { key: key.into() })?;
2975
2976 let derived_set: BTreeSet<(u32, u32, u32)> = self
2978 .engine
2979 .provenance_touching(id)
2980 .map(|(_, etype, src, dst)| (etype, src, dst))
2981 .collect();
2982 let derived_edges = derived_set.len() as u64;
2983
2984 let mut total_topo = 0u64;
2985 for et in self.topo.etypes() {
2986 total_topo += self.topo.neighbors(et, Direction::Out, id).len() as u64
2987 + self.topo.neighbors(et, Direction::In, id).len() as u64;
2988 }
2989 let manual_edges = total_topo.saturating_sub(derived_edges);
2993
2994 self.log_then_apply(WalRecord::DeleteNode { key: key.into() })?;
2995 Ok(DeleteReport {
2996 manual_edges,
2997 derived_edges,
2998 })
2999 }
3000
3001 pub fn ivf_dst_drift(&self, rule: &str) -> Option<u64> {
3008 self.engine
3010 .export_ivf_state()
3011 .remove(rule)
3012 .map(|(_src, dst)| dst.2)
3013 }
3014
3015 pub fn create_rule(&mut self, def: RuleDef) -> Result<()> {
3019 if self.read_only {
3020 return Err(GraphError::ReadOnly);
3021 }
3022 MutPreview::new(self).check_create_rule(&def)?;
3023 let def_bytes = bincode::serialize(&def).map_err(|e| GraphError::Corrupt {
3024 detail: format!("serialize rule: {e}"),
3025 })?;
3026 self.log_then_apply(WalRecord::CreateRule { def_bytes })
3027 }
3028
3029 pub fn delete_rule(&mut self, name: &str) -> Result<()> {
3031 if self.read_only {
3032 return Err(GraphError::ReadOnly);
3033 }
3034 MutPreview::new(self).check_delete_rule(name)?;
3035 self.log_then_apply(WalRecord::DeleteRule { name: name.into() })
3036 }
3037
3038 pub fn rules(&self) -> Vec<RuleDef> {
3040 self.engine.rules().cloned().collect()
3041 }
3042
3043 pub fn suggest_rules(&self) -> Vec<core_rules::RuleSuggestion> {
3053 self.suggest_rules_seeded(core_rules::SUGGEST_DEFAULT_SEED)
3054 }
3055
3056 pub fn suggest_rules_seeded(&self, seed: u64) -> Vec<core_rules::RuleSuggestion> {
3059 self.suggest_rules_with_config(&core_rules::suggest::SuggestConfig::default(), seed)
3060 .suggestions
3061 }
3062
3063 pub fn suggest_rules_with_config(
3069 &self,
3070 config: &core_rules::suggest::SuggestConfig,
3071 seed: u64,
3072 ) -> core_rules::SuggestReport {
3073 use std::collections::BTreeMap;
3074
3075 let mut label_nodes: BTreeMap<String, Vec<(u32, String)>> = BTreeMap::new();
3077 for id in 0..self.ids.len() as u32 {
3078 let Some(key) = self.ids.key_of(id) else {
3079 continue;
3080 };
3081 let Some(&sym) = self.labels.get(id as usize) else {
3082 continue;
3083 };
3084 if sym == u32::MAX {
3085 continue; }
3087 let Some(label) = self.syms.resolve(sym) else {
3088 continue;
3089 };
3090 label_nodes
3091 .entry(label.to_string())
3092 .or_default()
3093 .push((id, key.to_string()));
3094 }
3095
3096 let all_fields: Vec<String> = self.props.fields().map(String::from).collect();
3097 let existing = self.rules();
3098
3099 core_rules::suggest::suggest_rules(
3100 &label_nodes,
3101 &|id, field| self.props.get(id, field).cloned(),
3102 &all_fields,
3103 &existing,
3104 config,
3105 seed,
3106 )
3107 }
3108
3109 pub fn rebuild_rule(&mut self, name: &str) -> Result<()> {
3119 if self.read_only {
3120 return Err(GraphError::ReadOnly);
3121 }
3122 if !self.engine.rules().any(|r| r.name == name) {
3123 return Err(GraphError::RuleNotFound { name: name.into() });
3124 }
3125 self.log_then_apply(WalRecord::RebuildRule { name: name.into() })
3126 }
3127
3128 pub fn create_view(&mut self, def: ViewDef) -> Result<()> {
3139 if self.read_only {
3140 return Err(GraphError::ReadOnly);
3141 }
3142 def.validate()
3144 .map_err(|e| GraphError::RuleInvalid { detail: e })?;
3145 if self.view_store.has_view(&def.name) {
3146 return Err(GraphError::RuleInvalid {
3147 detail: format!("view {:?} already exists", def.name),
3148 });
3149 }
3150 if let Some(existing) = self.view_store.view_for_prop(&def.view_prop) {
3151 return Err(GraphError::RuleInvalid {
3152 detail: format!(
3153 "view_prop {:?} is already used by view {:?}",
3154 def.view_prop, existing
3155 ),
3156 });
3157 }
3158 let def_bytes = bincode::serialize(&def).map_err(|e| GraphError::Corrupt {
3159 detail: format!("serialize view: {e}"),
3160 })?;
3161 self.engine.set_emit_deltas(true);
3166 self.log_then_apply(WalRecord::CreateView { def_bytes })
3167 }
3168
3169 pub fn delete_view(&mut self, name: &str) -> Result<()> {
3175 if self.read_only {
3176 return Err(GraphError::ReadOnly);
3177 }
3178 if !self.view_store.has_view(name) {
3179 return Err(GraphError::RuleNotFound { name: name.into() });
3180 }
3181 let result = self.log_then_apply(WalRecord::DeleteView { name: name.into() });
3182 if !self.needs_emit_deltas() {
3184 self.engine.set_emit_deltas(false);
3185 }
3186 result
3187 }
3188
3189 pub fn views(&self) -> Vec<ViewDef> {
3191 self.view_store.views().cloned().collect()
3192 }
3193
3194 pub fn enable_fulltext(&mut self, label: &str, field: &str) -> Result<()> {
3209 if self.read_only {
3210 return Err(GraphError::ReadOnly);
3211 }
3212 if self.fulltext.is_enabled(label, field) {
3213 return Err(GraphError::RuleInvalid {
3214 detail: format!("full-text index for ({label:?}, {field:?}) already enabled"),
3215 });
3216 }
3217 self.log_then_apply(WalRecord::EnableFulltext {
3218 label: label.into(),
3219 field: field.into(),
3220 })
3221 }
3222
3223 pub fn disable_fulltext(&mut self, label: &str, field: &str) -> Result<()> {
3229 if self.read_only {
3230 return Err(GraphError::ReadOnly);
3231 }
3232 if !self.fulltext.is_enabled(label, field) {
3233 return Err(GraphError::RuleNotFound {
3234 name: format!("fulltext({label},{field})"),
3235 });
3236 }
3237 self.log_then_apply(WalRecord::DisableFulltext {
3238 label: label.into(),
3239 field: field.into(),
3240 })
3241 }
3242
3243 pub fn is_fulltext_enabled(&self, label: &str, field: &str) -> bool {
3245 self.fulltext.is_enabled(label, field)
3246 }
3247
3248 pub fn search(&self, field: &str, query: &str) -> Vec<(String, usize)> {
3266 let mut results: Vec<(String, usize)> = self
3271 .fulltext
3272 .search(field, query)
3273 .into_iter()
3274 .filter_map(|(id, count)| self.ids.key_of(id).map(|key| (key.to_string(), count)))
3275 .collect();
3276 results.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
3277 results
3278 }
3279
3280 pub fn search_hybrid(
3304 &self,
3305 text_field: &str,
3306 query_text: &str,
3307 vector_field: &str,
3308 query_vec: &[f64],
3309 label: Option<&str>,
3310 k: usize,
3311 ) -> Vec<(String, f64)> {
3312 use std::collections::HashMap;
3313
3314 const RRF_K: f64 = 60.0;
3315 let pool = 4 * k.max(1);
3316
3317 let mut scores: HashMap<String, f64> = HashMap::new();
3319
3320 let text_hits = self.search(text_field, query_text);
3322 for (rank0, (key, _count)) in text_hits.into_iter().take(pool).enumerate() {
3323 let rank = (rank0 + 1) as f64;
3324 *scores.entry(key).or_insert(0.0) += 1.0 / (RRF_K + rank);
3325 }
3326
3327 if !query_vec.is_empty() {
3329 let lbl = label.unwrap_or("");
3330 let vec_hits = self.find_similar_vector(vector_field, lbl, query_vec, pool, 0.0);
3331 for (rank0, (key, _sim)) in vec_hits.into_iter().enumerate() {
3332 let rank = (rank0 + 1) as f64;
3333 *scores.entry(key).or_insert(0.0) += 1.0 / (RRF_K + rank);
3334 }
3335 }
3336
3337 let mut ranked: Vec<(String, f64)> = scores.into_iter().collect();
3339 ranked.sort_by(|a, b| {
3340 b.1.partial_cmp(&a.1)
3341 .unwrap_or(std::cmp::Ordering::Equal)
3342 .then(a.0.cmp(&b.0))
3343 });
3344 ranked.truncate(k);
3345 ranked
3346 }
3347
3348 #[doc(hidden)]
3354 pub fn scratch_search(&self, field: &str, query: &str) -> Vec<(String, usize)> {
3355 use core_storage::fulltext::{parse_query, tokenize};
3356 use std::collections::BTreeSet;
3357 let groups = parse_query(query);
3358 let mut results: Vec<(String, usize)> = Vec::new();
3359 for id in 0..self.ids.len() as u32 {
3360 let Some(key) = self.ids.key_of(id) else {
3361 continue;
3362 };
3363 let Some(&sym) = self.labels.get(id as usize) else {
3364 continue;
3365 };
3366 if sym == u32::MAX {
3367 continue;
3368 }
3369 let label = match self.syms.resolve(sym) {
3371 Some(l) => l,
3372 None => continue,
3373 };
3374 if !self.fulltext.is_enabled(label, field) {
3375 continue;
3376 }
3377 let Some(value) = self.props.get(id, field) else {
3378 continue;
3379 };
3380 let node_tokens: BTreeSet<String> = match value {
3381 Value::Str(s) => tokenize(s).into_iter().collect(),
3382 Value::List(items) => items
3383 .iter()
3384 .flat_map(|v| {
3385 if let Value::Str(s) = v {
3386 tokenize(s)
3387 } else {
3388 vec![]
3389 }
3390 })
3391 .collect(),
3392 _ => BTreeSet::new(),
3393 };
3394 let mut count = 0usize;
3396 for group in &groups {
3397 let mut group_match = true;
3398 for term in group {
3399 let matched = if term.prefix {
3400 node_tokens.iter().any(|t| t.starts_with(&term.token))
3401 } else {
3402 node_tokens.contains(&term.token)
3403 };
3404 if !matched {
3405 group_match = false;
3406 break;
3407 }
3408 }
3409 if group_match {
3410 count += 1;
3411 }
3412 }
3413 if count > 0 {
3414 results.push((key.to_string(), count));
3415 }
3416 }
3417 results.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
3418 results
3419 }
3420
3421 pub fn get_view_prop(&self, key: &str, view_prop: &str) -> Option<&Value> {
3424 self.props.get(self.ids.get(key)?, view_prop)
3425 }
3426
3427 #[doc(hidden)]
3432 pub fn scratch_view_value(&self, key: &str, view_name: &str) -> Option<Value> {
3433 let node = self.ids.get(key)?;
3434 let def = self.view_store.views().find(|v| v.name == view_name)?;
3435 core_rules::views::compute_view_value(
3438 def,
3439 node,
3440 &self.props,
3441 &self.topo,
3442 &self.ids,
3443 &self.syms,
3444 &self.labels,
3445 )
3446 }
3447
3448 pub fn pagerank(&self, config: &crate::algo::PageRankConfig) -> crate::algo::PageRankReport {
3459 crate::algo::pagerank(&self.topo, &self.ids, &self.syms, &self.labels, config)
3460 }
3461
3462 pub fn connected_components(&self, config: &crate::algo::WccConfig) -> crate::algo::WccReport {
3468 crate::algo::wcc(&self.topo, &self.ids, &self.syms, &self.labels, config)
3469 }
3470
3471 pub fn degree_centrality(
3479 &self,
3480 config: &crate::algo::DegreeConfig,
3481 ) -> crate::algo::DegreeReport {
3482 crate::algo::degree_centrality(&self.topo, &self.ids, &self.syms, &self.labels, config)
3483 }
3484
3485 pub fn write_scores(&mut self, prop_name: &str, scores: &[(String, f64)]) -> Result<()> {
3494 if self.read_only {
3495 return Err(GraphError::ReadOnly);
3496 }
3497 if let Some(view_name) = self.view_store.view_for_prop(prop_name) {
3499 return Err(GraphError::RuleInvalid {
3500 detail: format!(
3501 "prop {:?} is managed by view {:?} and cannot be written as scores",
3502 prop_name, view_name
3503 ),
3504 });
3505 }
3506 if self.view_store.has_view(prop_name) {
3508 return Err(GraphError::RuleInvalid {
3509 detail: format!(
3510 "prop_name {:?} collides with an existing view name",
3511 prop_name
3512 ),
3513 });
3514 }
3515 self.write_batch(|b| {
3517 for (key, score) in scores {
3518 b.set_prop(key, prop_name, Value::Float(*score));
3519 }
3520 })?;
3521 Ok(())
3522 }
3523
3524 pub fn get_prop(&self, key: &str, field: &str) -> Option<&Value> {
3525 self.props.get(self.ids.get(key)?, field)
3526 }
3527
3528 pub fn has_node(&self, key: &str) -> bool {
3529 self.ids.get(key).is_some()
3530 }
3531
3532 pub(crate) fn ids(&self) -> &IdMap {
3534 &self.ids
3535 }
3536
3537 fn view(&self) -> GraphView<'_> {
3538 GraphView {
3539 ids: &self.ids,
3540 syms: &self.syms,
3541 labels: &self.labels,
3542 props: &self.props,
3543 topo: &self.topo,
3544 edge_props: &self.edge_props,
3545 mask: None,
3546 }
3547 }
3548
3549 fn view_masked<'a>(&'a self, mask: &'a crate::mask::NodeMask) -> GraphView<'a> {
3550 GraphView {
3551 ids: &self.ids,
3552 syms: &self.syms,
3553 labels: &self.labels,
3554 props: &self.props,
3555 topo: &self.topo,
3556 edge_props: &self.edge_props,
3557 mask: Some(&mask.visible),
3558 }
3559 }
3560
3561 pub fn query_masked(
3570 &self,
3571 cypher: &str,
3572 params: &std::collections::BTreeMap<String, Value>,
3573 mask: &crate::mask::NodeMask,
3574 ) -> Result<ResultSet> {
3575 let tokens = lex(cypher).map_err(|e| GraphError::QueryError {
3577 detail: format!("lex: {e}"),
3578 })?;
3579 if is_write_tokens(&tokens) {
3580 return Err(GraphError::QueryError {
3581 detail: "masked queries are read-only".into(),
3582 });
3583 }
3584 let ast = parse(&tokens).map_err(|e| GraphError::QueryError {
3585 detail: format!("parse: {e}"),
3586 })?;
3587 let ops = plan(&ast).map_err(|e| GraphError::QueryError {
3588 detail: format!("plan: {e}"),
3589 })?;
3590 execute(&self.view_masked(mask), &ops, &Params(params)).map_err(|e| {
3591 GraphError::QueryError {
3592 detail: format!("execute: {e}"),
3593 }
3594 })
3595 }
3596
3597 pub fn node_ref(&self, key: &str) -> Option<NodeRef<'_, F>> {
3598 let id = self.ids.get(key)?;
3599 Some(NodeRef { db: self, id })
3600 }
3601
3602 pub fn node_info(&self, key: &str) -> Option<NodeInfo> {
3604 let n = self.node_ref(key)?;
3605 Some(NodeInfo {
3606 key: n.key().to_string(),
3607 label: n.label().to_string(),
3608 props: n.props(),
3609 })
3610 }
3611
3612 pub fn node_edges(&self, key: &str) -> Result<Vec<EdgeInfo>> {
3619 let id = self
3620 .ids
3621 .get(key)
3622 .ok_or_else(|| GraphError::KeyNotFound { key: key.into() })?;
3623 let derived: BTreeSet<(u32, u32, u32)> = self
3624 .engine
3625 .provenance_touching(id)
3626 .map(|(_rule, etype, src, dst)| (etype, src, dst))
3627 .collect();
3628 let mut edges = Vec::new();
3629 for etype in self.topo.etypes() {
3630 let edge_type = self
3631 .syms
3632 .resolve(etype)
3633 .expect("topology etype is interned")
3634 .to_string();
3635 for dir in [Direction::Out, Direction::In] {
3636 for &nbr in self.topo.neighbors(etype, dir, id).as_ref() {
3637 let (src, dst, src_key, dst_key) = match dir {
3638 Direction::Out => (
3639 id,
3640 nbr,
3641 key.to_string(),
3642 self.ids
3643 .key_of(nbr)
3644 .ok_or_else(|| GraphError::Corrupt {
3645 detail: format!("topology id {nbr} has no key"),
3646 })?
3647 .to_string(),
3648 ),
3649 Direction::In => (
3650 nbr,
3651 id,
3652 self.ids
3653 .key_of(nbr)
3654 .ok_or_else(|| GraphError::Corrupt {
3655 detail: format!("topology id {nbr} has no key"),
3656 })?
3657 .to_string(),
3658 key.to_string(),
3659 ),
3660 };
3661 edges.push(EdgeInfo {
3662 edge_type: edge_type.clone(),
3663 src_key,
3664 dst_key,
3665 derived: derived.contains(&(etype, src, dst)),
3666 });
3667 }
3668 }
3669 }
3670 edges.sort_by(|a, b| {
3671 a.edge_type
3672 .cmp(&b.edge_type)
3673 .then(a.src_key.cmp(&b.src_key))
3674 .then(a.dst_key.cmp(&b.dst_key))
3675 });
3676 edges.dedup();
3679 Ok(edges)
3680 }
3681
3682 pub fn nodes_with_label(&self, label: &str) -> Vec<NodeRef<'_, F>> {
3683 self.view()
3684 .nodes_with_label(label)
3685 .into_iter()
3686 .map(|id| NodeRef { db: self, id })
3687 .collect()
3688 }
3689
3690 pub fn find_nodes(&self, label: &str, filter: &Filter) -> Vec<NodeRef<'_, F>> {
3691 let view = self.view();
3692 view.nodes_with_label(label)
3693 .into_iter()
3694 .filter(|&id| eval_filter(filter, &|field| view.prop(id, field).cloned()))
3695 .map(|id| NodeRef { db: self, id })
3696 .collect()
3697 }
3698
3699 pub fn find_similar_vector(
3706 &self,
3707 field: &str,
3708 label: &str,
3709 q: &[f64],
3710 k: usize,
3711 min: f64,
3712 ) -> Vec<(String, f64)> {
3713 let norm: f64 = q.iter().map(|x| x * x).sum::<f64>().sqrt();
3715 if norm == 0.0 {
3716 return vec![];
3717 }
3718 let q_unit: Vec<f64> = q.iter().map(|x| x / norm).collect();
3719
3720 if let Some(hits) = self.engine.hnsw_search_dst(field, label, &q_unit, k) {
3722 let mut out: Vec<(String, f64)> = hits
3723 .into_iter()
3724 .filter(|&(_, sim)| sim >= min)
3725 .filter_map(|(id, sim)| self.ids.key_of(id).map(|key| (key.to_string(), sim)))
3726 .collect();
3727 out.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
3728 out.truncate(k);
3729 return out;
3730 }
3731
3732 let view = self.view();
3734 let mut scored: Vec<(String, f64)> = view
3735 .nodes_with_label(label)
3736 .into_iter()
3737 .filter_map(|id| {
3738 let v = view.prop(id, field)?;
3739 let xs = value_as_float_list(v)?;
3740 let v_norm: f64 = xs.iter().map(|x| x * x).sum::<f64>().sqrt();
3741 if v_norm == 0.0 {
3742 return None;
3743 }
3744 let dot: f64 = q_unit
3745 .iter()
3746 .zip(xs.iter())
3747 .map(|(a, b)| a * (b / v_norm))
3748 .sum();
3749 if dot < min {
3750 return None;
3751 }
3752 let key = self.ids.key_of(id)?.to_string();
3753 Some((key, dot))
3754 })
3755 .collect();
3756 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
3757 scored.truncate(k);
3758 scored
3759 }
3760
3761 pub fn query(&self, cypher: &str, params: &BTreeMap<String, Value>) -> Result<ResultSet> {
3765 let tokens = lex(cypher).map_err(|e| GraphError::QueryError {
3766 detail: format!("lex: {e}"),
3767 })?;
3768 let ast = parse(&tokens).map_err(|e| GraphError::QueryError {
3769 detail: format!("parse: {e}"),
3770 })?;
3771 let ops = plan(&ast).map_err(|e| GraphError::QueryError {
3772 detail: format!("plan: {e}"),
3773 })?;
3774 execute(&self.view(), &ops, &Params(params)).map_err(|e| GraphError::QueryError {
3775 detail: format!("execute: {e}"),
3776 })
3777 }
3778
3779 pub fn query_with_params(&self, cypher: &str, params: &[(&str, Value)]) -> Result<ResultSet> {
3783 let map: BTreeMap<String, Value> = params
3784 .iter()
3785 .map(|(k, v)| (k.to_string(), v.clone()))
3786 .collect();
3787 self.query(cypher, &map)
3788 }
3789
3790 pub fn query_write(
3811 &mut self,
3812 cypher: &str,
3813 params: &BTreeMap<String, Value>,
3814 ) -> Result<ResultSet> {
3815 let tokens = lex(cypher).map_err(|e| GraphError::QueryError {
3816 detail: format!("lex: {e}"),
3817 })?;
3818 let stmt = parse_write(&tokens).map_err(|e| GraphError::QueryError {
3819 detail: format!("parse: {e}"),
3820 })?;
3821 self.exec_write_stmt(stmt, params)
3822 }
3823
3824 fn exec_write_stmt(
3825 &mut self,
3826 stmt: WriteStatement,
3827 params: &BTreeMap<String, Value>,
3828 ) -> Result<ResultSet> {
3829 match stmt {
3830 WriteStatement::Create(s) => self.exec_create(s, params),
3831 WriteStatement::MatchSet(s) => self.exec_match_set(s, params),
3832 WriteStatement::MatchDelete(s) => self.exec_match_delete(s, params),
3833 WriteStatement::MatchDeleteNode(s) => self.exec_match_delete_node(s, params),
3834 WriteStatement::Merge(s) => self.exec_merge(s, params),
3835 }
3836 }
3837
3838 fn exec_create(
3839 &mut self,
3840 stmt: core_query::cypher::CreateStmt,
3841 params: &BTreeMap<String, Value>,
3842 ) -> Result<ResultSet> {
3843 let mut var_to_key: BTreeMap<String, String> = BTreeMap::new();
3845 for node in &stmt.nodes {
3846 let var = node.var.as_deref().unwrap_or("_cn0");
3847 let key = node
3848 .props
3849 .iter()
3850 .find(|(f, _)| f == "id")
3851 .and_then(|(_, v)| {
3852 if let Value::Str(s) = v {
3853 Some(s.clone())
3854 } else {
3855 None
3856 }
3857 })
3858 .ok_or_else(|| GraphError::QueryError {
3859 detail: format!(
3860 "CREATE node ({}:{}) requires a string 'id' property",
3861 var, node.label
3862 ),
3863 })?;
3864 var_to_key.insert(var.to_string(), key);
3865 }
3866
3867 let mut batch = self.batch();
3868 let mut created: usize = 0;
3869 for node in &stmt.nodes {
3870 let var = node.var.as_deref().unwrap_or("_cn0");
3871 let key = &var_to_key[var];
3872 batch.insert_node(&node.label, key, node.props.clone());
3873 created += 1;
3874 }
3875 for edge in &stmt.edges {
3876 let src_key = var_to_key
3877 .get(&edge.src_var)
3878 .ok_or_else(|| GraphError::QueryError {
3879 detail: format!("CREATE edge src variable '{}' is not bound", edge.src_var),
3880 })?;
3881 let dst_key = var_to_key
3882 .get(&edge.dst_var)
3883 .ok_or_else(|| GraphError::QueryError {
3884 detail: format!("CREATE edge dst variable '{}' is not bound", edge.dst_var),
3885 })?;
3886 batch.insert_edge(&edge.etype, src_key, dst_key);
3887 }
3888 batch.commit()?;
3889
3890 if let Some(returns) = stmt.returns {
3892 let patterns: Vec<Pattern> = stmt
3896 .nodes
3897 .iter()
3898 .map(|node| {
3899 let var = node.var.as_deref().unwrap_or("_cn0");
3900 let key = var_to_key[var].clone();
3901 Pattern {
3902 start: NodePat {
3903 var: Some(var.to_string()),
3904 label: Some(node.label.clone()),
3905 props: vec![("id".to_string(), Operand::Lit(Value::Str(key)))],
3906 },
3907 chain: vec![],
3908 shortest: false,
3909 }
3910 })
3911 .collect();
3912 let q = Query {
3913 matches: patterns,
3914 optional_clauses: vec![],
3915 where_expr: None,
3916 unwinds: vec![],
3917 post_unwind_where: None,
3918 stages: vec![],
3919 returns,
3920 distinct: false,
3921 order_by: vec![],
3922 skip: None,
3923 limit: None,
3924 };
3925 let ops = plan(&q).map_err(|e| GraphError::QueryError {
3926 detail: format!("plan: {e}"),
3927 })?;
3928 return execute(&self.view(), &ops, &Params(params)).map_err(|e| {
3929 GraphError::QueryError {
3930 detail: format!("execute: {e}"),
3931 }
3932 });
3933 }
3934
3935 let mut rs = write_result_set();
3936 rs.push_row(vec![
3937 Some(Value::Int(created as i64)),
3938 Some(Value::Int(0)),
3939 Some(Value::Int(0)),
3940 ]);
3941 Ok(rs)
3942 }
3943
3944 fn exec_match_set(
3945 &mut self,
3946 stmt: core_query::cypher::MatchSetStmt,
3947 params: &BTreeMap<String, Value>,
3948 ) -> Result<ResultSet> {
3949 let project_returns = stmt.returns.clone();
3950 let mut set_vars: Vec<String> = Vec::new();
3953 for s in &stmt.sets {
3954 if !set_vars.contains(&s.var) {
3955 set_vars.push(s.var.clone());
3956 }
3957 }
3958 let rel_vars = pattern_rel_vars(&stmt.matches);
3959 let mut lookup_vars = set_vars.clone();
3960 for v in pattern_node_vars(&stmt.matches) {
3961 add_var(&mut lookup_vars, &v);
3962 }
3963 if let Some(ref returns) = project_returns {
3964 for v in ret_node_vars(returns) {
3965 if !rel_vars.iter().any(|r| r == &v) {
3966 add_var(&mut lookup_vars, &v);
3967 }
3968 }
3969 }
3970
3971 let mut set_returns: Vec<RetItem> = lookup_vars
3975 .iter()
3976 .map(|v| RetItem {
3977 value: RetVal::Var(v.clone()),
3978 alias: None,
3979 })
3980 .collect();
3981 let set_val_cols: Vec<String> = stmt
3983 .sets
3984 .iter()
3985 .enumerate()
3986 .map(|(i, _)| format!("__sv_{i}"))
3987 .collect();
3988 for (sc, col) in stmt.sets.iter().zip(&set_val_cols) {
3989 set_returns.push(RetItem {
3990 value: RetVal::ScalarExpr(sc.value.clone()),
3991 alias: Some(col.clone()),
3992 });
3993 }
3994 for r in &rel_vars {
3996 set_returns.push(RetItem {
3997 value: RetVal::FuncCall {
3998 name: "type".into(),
3999 args: vec![Operand::Var(r.clone())],
4000 },
4001 alias: Some(rel_type_alias(r)),
4002 });
4003 }
4004
4005 let read_q = Query {
4006 matches: stmt.matches.clone(),
4007 optional_clauses: vec![],
4008 where_expr: stmt.where_expr.clone(),
4009 unwinds: vec![],
4010 post_unwind_where: None,
4011 stages: vec![],
4012 returns: set_returns,
4013 distinct: false,
4014 order_by: vec![],
4015 skip: None,
4016 limit: None,
4017 };
4018 let ops = plan(&read_q).map_err(|e| GraphError::QueryError {
4019 detail: format!("plan: {e}"),
4020 })?;
4021 let match_rs =
4023 execute(&self.view(), &ops, &Params(params)).map_err(|e| GraphError::QueryError {
4024 detail: format!("execute: {e}"),
4025 })?;
4026
4027 let mut set_ops: Vec<(String, String, Value)> = Vec::new();
4029 for row_i in 0..match_rs.len() {
4030 for (sc, col) in stmt.sets.iter().zip(&set_val_cols) {
4031 let key = match match_rs.get(row_i, &sc.var) {
4032 Some(Value::Str(k)) => k.clone(),
4033 _ => {
4034 return Err(GraphError::QueryError {
4035 detail: format!(
4036 "SET variable '{}' did not resolve to a node key",
4037 sc.var
4038 ),
4039 })
4040 }
4041 };
4042 let value = match match_rs.get(row_i, col) {
4044 Some(v) => v.clone(),
4045 None => {
4046 return Err(GraphError::QueryError {
4047 detail: format!(
4048 "SET value for {}.{} evaluated to null",
4049 sc.var, sc.field
4050 ),
4051 })
4052 }
4053 };
4054 set_ops.push((key, sc.field.clone(), value));
4055 }
4056 }
4057
4058 let props_set = set_ops.len();
4060 let mut batch = self.batch();
4061 for (key, field, value) in set_ops {
4062 batch.set_prop(&key, &field, value);
4063 }
4064 batch.commit()?;
4065
4066 if let Some(returns) = project_returns {
4067 return project_set_return_rows(self, &rel_vars, &match_rs, &returns, params);
4068 }
4069
4070 let mut rs = write_result_set();
4071 rs.push_row(vec![
4072 Some(Value::Int(0)),
4073 Some(Value::Int(props_set as i64)),
4074 Some(Value::Int(0)),
4075 ]);
4076 Ok(rs)
4077 }
4078
4079 fn exec_match_delete(
4080 &mut self,
4081 stmt: core_query::cypher::MatchDeleteStmt,
4082 params: &BTreeMap<String, Value>,
4083 ) -> Result<ResultSet> {
4084 let mut node_vars: Vec<String> = Vec::new();
4086 for ed in &stmt.deletes {
4087 if !node_vars.contains(&ed.src_var) {
4088 node_vars.push(ed.src_var.clone());
4089 }
4090 if !node_vars.contains(&ed.dst_var) {
4091 node_vars.push(ed.dst_var.clone());
4092 }
4093 }
4094
4095 let returns: Vec<RetItem> = node_vars
4097 .iter()
4098 .map(|v| RetItem {
4099 value: RetVal::Var(v.clone()),
4100 alias: None,
4101 })
4102 .collect();
4103 let read_q = Query {
4104 matches: stmt.matches,
4105 optional_clauses: vec![],
4106 where_expr: stmt.where_expr,
4107 unwinds: vec![],
4108 post_unwind_where: None,
4109 stages: vec![],
4110 returns,
4111 distinct: false,
4112 order_by: vec![],
4113 skip: None,
4114 limit: None,
4115 };
4116 let ops = plan(&read_q).map_err(|e| GraphError::QueryError {
4117 detail: format!("plan: {e}"),
4118 })?;
4119 let match_rs =
4120 execute(&self.view(), &ops, &Params(params)).map_err(|e| GraphError::QueryError {
4121 detail: format!("execute: {e}"),
4122 })?;
4123
4124 let mut del_ops: Vec<(String, String, String)> = Vec::new();
4126 for row_i in 0..match_rs.len() {
4127 for ed in &stmt.deletes {
4128 let src_key = match match_rs.get(row_i, &ed.src_var) {
4129 Some(Value::Str(k)) => k.clone(),
4130 _ => {
4131 return Err(GraphError::QueryError {
4132 detail: format!(
4133 "DELETE src variable '{}' did not resolve to a node key",
4134 ed.src_var
4135 ),
4136 })
4137 }
4138 };
4139 let dst_key = match match_rs.get(row_i, &ed.dst_var) {
4140 Some(Value::Str(k)) => k.clone(),
4141 _ => {
4142 return Err(GraphError::QueryError {
4143 detail: format!(
4144 "DELETE dst variable '{}' did not resolve to a node key",
4145 ed.dst_var
4146 ),
4147 })
4148 }
4149 };
4150 del_ops.push((ed.etype.clone(), src_key, dst_key));
4151 }
4152 }
4153
4154 let deleted = del_ops.len();
4156 let mut batch = self.batch();
4157 for (etype, src_key, dst_key) in del_ops {
4158 batch.delete_edge(&etype, &src_key, &dst_key);
4159 }
4160 batch.commit().map_err(|e| match e {
4161 GraphError::RuleOwned { .. } => GraphError::QueryError {
4162 detail: "cannot delete derived edge; retract via the rule or change the property"
4163 .to_string(),
4164 },
4165 other => other,
4166 })?;
4167
4168 let mut rs = write_result_set();
4169 rs.push_row(vec![
4170 Some(Value::Int(0)),
4171 Some(Value::Int(0)),
4172 Some(Value::Int(deleted as i64)),
4173 ]);
4174 Ok(rs)
4175 }
4176
4177 fn exec_match_delete_node(
4184 &mut self,
4185 stmt: MatchDeleteNodeStmt,
4186 params: &BTreeMap<String, Value>,
4187 ) -> Result<ResultSet> {
4188 let returns: Vec<RetItem> = stmt
4190 .node_vars
4191 .iter()
4192 .map(|v| RetItem {
4193 value: RetVal::Var(v.clone()),
4194 alias: None,
4195 })
4196 .collect();
4197 let read_q = Query {
4198 matches: stmt.matches,
4199 optional_clauses: vec![],
4200 where_expr: stmt.where_expr,
4201 unwinds: vec![],
4202 post_unwind_where: None,
4203 stages: vec![],
4204 returns,
4205 distinct: false,
4206 order_by: vec![],
4207 skip: None,
4208 limit: None,
4209 };
4210 let ops = plan(&read_q).map_err(|e| GraphError::QueryError {
4211 detail: format!("plan: {e}"),
4212 })?;
4213 let match_rs =
4214 execute(&self.view(), &ops, &Params(params)).map_err(|e| GraphError::QueryError {
4215 detail: format!("execute: {e}"),
4216 })?;
4217
4218 let mut keys: Vec<String> = Vec::new();
4220 for row_i in 0..match_rs.len() {
4221 for var in &stmt.node_vars {
4222 if let Some(Value::Str(k)) = match_rs.get(row_i, var) {
4223 if !keys.contains(k) {
4224 keys.push(k.clone());
4225 }
4226 }
4227 }
4228 }
4229
4230 if !stmt.detach {
4231 for key in &keys {
4233 if let Some(id) = self.ids.get(key) {
4234 let has_edges = self.topo.etypes().any(|et| {
4235 !self.topo.neighbors(et, Direction::Out, id).is_empty()
4236 || !self.topo.neighbors(et, Direction::In, id).is_empty()
4237 });
4238 if has_edges {
4239 return Err(GraphError::QueryError {
4240 detail: format!(
4241 "Cannot delete node `{key}` because it still has incident edges. \
4242 Use DETACH DELETE to remove the node and all its edges."
4243 ),
4244 });
4245 }
4246 }
4247 }
4248 }
4249
4250 let mut nodes_deleted = 0i64;
4251 let mut edges_deleted = 0i64;
4252 for key in keys {
4253 match self.delete_node(&key) {
4254 Ok(report) => {
4255 nodes_deleted += 1;
4256 edges_deleted += (report.manual_edges + report.derived_edges) as i64;
4257 }
4258 Err(GraphError::KeyNotFound { .. }) => {
4259 }
4262 Err(e) => return Err(e),
4263 }
4264 }
4265
4266 let mut rs = write_result_set();
4267 rs.push_row(vec![
4268 Some(Value::Int(0)),
4269 Some(Value::Int(0)),
4270 Some(Value::Int(nodes_deleted + edges_deleted)),
4271 ]);
4272 Ok(rs)
4273 }
4274
4275 fn exec_merge(
4276 &mut self,
4277 stmt: core_query::cypher::MergeStmt,
4278 params: &BTreeMap<String, Value>,
4279 ) -> Result<ResultSet> {
4280 let key = match &stmt.key_value {
4282 Value::Str(s) => s.clone(),
4283 _ => {
4284 return Err(GraphError::QueryError {
4285 detail: format!(
4286 "MERGE key value must be a string (got {:?})",
4287 stmt.key_value
4288 ),
4289 })
4290 }
4291 };
4292
4293 if let Some(var) = stmt.var.as_deref() {
4294 for sc in stmt.on_create.iter().chain(&stmt.on_match) {
4295 if sc.var != var {
4296 return Err(GraphError::QueryError {
4297 detail: format!(
4298 "SET variable '{}' does not match MERGE variable '{var}'",
4299 sc.var
4300 ),
4301 });
4302 }
4303 }
4304 }
4305
4306 let existed = self.has_node(&key);
4307 let mut created = 0i64;
4308 if !existed || !stmt.on_match.is_empty() {
4309 let mut batch = self.batch();
4310 if !existed {
4311 let props = vec![(stmt.key_field.clone(), stmt.key_value.clone())];
4312 batch.insert_node(&stmt.label, &key, props);
4313 for sc in &stmt.on_create {
4314 let value = resolve_merge_set_value(&sc.value, params)?;
4315 batch.set_prop(&key, &sc.field, value);
4316 }
4317 created = 1;
4318 } else {
4319 for sc in &stmt.on_match {
4320 let value = resolve_merge_set_value(&sc.value, params)?;
4321 batch.set_prop(&key, &sc.field, value);
4322 }
4323 }
4324 batch.commit()?;
4325 }
4326
4327 if let Some(returns) = stmt.returns {
4329 let var = stmt.var.as_deref().unwrap_or("_mn0");
4330 let q = Query {
4331 matches: vec![Pattern {
4332 start: NodePat {
4333 var: Some(var.to_string()),
4334 label: Some(stmt.label.clone()),
4335 props: vec![("id".to_string(), Operand::Lit(stmt.key_value.clone()))],
4336 },
4337 chain: vec![],
4338 shortest: false,
4339 }],
4340 optional_clauses: vec![],
4341 where_expr: None,
4342 unwinds: vec![],
4343 post_unwind_where: None,
4344 stages: vec![],
4345 returns,
4346 distinct: false,
4347 order_by: vec![],
4348 skip: None,
4349 limit: None,
4350 };
4351 let ops = plan(&q).map_err(|e| GraphError::QueryError {
4352 detail: format!("plan: {e}"),
4353 })?;
4354 return execute(&self.view(), &ops, &Params(params)).map_err(|e| {
4355 GraphError::QueryError {
4356 detail: format!("execute: {e}"),
4357 }
4358 });
4359 }
4360
4361 let mut rs = write_result_set();
4362 rs.push_row(vec![
4363 Some(Value::Int(created)),
4364 Some(Value::Int(0)),
4365 Some(Value::Int(0)),
4366 ]);
4367 Ok(rs)
4368 }
4369
4370 pub fn explain(&self, key_a: &str, key_b: &str) -> Result<Vec<Explanation>> {
4375 let id_a = self
4376 .ids
4377 .get(key_a)
4378 .ok_or_else(|| GraphError::KeyNotFound { key: key_a.into() })?;
4379 let id_b = self
4380 .ids
4381 .get(key_b)
4382 .ok_or_else(|| GraphError::KeyNotFound { key: key_b.into() })?;
4383
4384 let mut results = Vec::new();
4385
4386 let scan = if self.engine.provenance_touching_len(id_a)
4389 <= self.engine.provenance_touching_len(id_b)
4390 {
4391 id_a
4392 } else {
4393 id_b
4394 };
4395 for (rule_name, etype, src, dst) in self.engine.provenance_touching(scan) {
4396 if !((src == id_a && dst == id_b) || (src == id_b && dst == id_a)) {
4397 continue;
4398 }
4399 let Some(rule_def) = self.engine.rules().find(|r| r.name == rule_name) else {
4400 continue;
4401 };
4402 let edge_type = match self.syms.resolve(etype) {
4403 Some(s) => s.to_string(),
4404 None => continue,
4405 };
4406 let src_key = self
4407 .ids
4408 .key_of(src)
4409 .expect("provenance ids always resolvable")
4410 .to_string();
4411 let dst_key = self
4412 .ids
4413 .key_of(dst)
4414 .expect("provenance ids always resolvable")
4415 .to_string();
4416 let weight = rule_def.weight_prop.as_deref().and_then(|prop| {
4417 self.edge_props.get(etype, src, dst, prop).and_then(|v| {
4418 if let Value::Float(f) = v {
4419 Some(*f)
4420 } else {
4421 None
4422 }
4423 })
4424 });
4425 results.push(Explanation {
4426 rule: rule_name.to_string(),
4427 edge_type,
4428 src_key,
4429 dst_key,
4430 weight,
4431 predicate: PredicateSummary {
4432 approximate: rule_def.approximate,
4433 ..PredicateSummary::from(&rule_def.predicate)
4434 },
4435 });
4436 }
4437
4438 results.sort_by(|a, b| a.rule.cmp(&b.rule).then(a.edge_type.cmp(&b.edge_type)));
4439 Ok(results)
4440 }
4441
4442 pub fn neighbors(&self, key: &str, edge_type: &str, dir: Direction) -> Result<Vec<String>> {
4443 let id = self
4444 .ids
4445 .get(key)
4446 .ok_or_else(|| GraphError::KeyNotFound { key: key.into() })?;
4447 let Some(sym) = self.syms.get(edge_type) else {
4448 return Ok(Vec::new());
4449 };
4450 self.topo
4451 .neighbors(sym, dir, id)
4452 .iter()
4453 .map(|&n| {
4454 self.ids
4455 .key_of(n)
4456 .map(|k| k.to_string())
4457 .ok_or_else(|| GraphError::Corrupt {
4458 detail: format!("topology id {n} has no key"),
4459 })
4460 })
4461 .collect::<Result<Vec<_>>>()
4462 }
4463
4464 pub fn node_count(&self) -> usize {
4465 self.ids.len()
4466 }
4467
4468 pub fn node_history(&self, key: &str) -> Result<Vec<crate::history::HistoryEntry>> {
4494 use crate::history::{HistoryChange, HistoryEntry};
4495 use core_storage::wal::WalRecord;
4496
4497 let bytes = self.fs.read(FileId::Wal)?;
4498 let (frames, _) = decode_all(&bytes);
4499
4500 let mut out: Vec<HistoryEntry> = Vec::new();
4501
4502 for (commit, frame) in frames.iter().enumerate() {
4503 let commit = commit as u64;
4504 let records: &[WalRecord] = match frame {
4506 WalRecord::Batch(inner) => inner.as_slice(),
4507 single => std::slice::from_ref(single),
4508 };
4509
4510 for rec in records {
4511 let change = match rec {
4512 WalRecord::InsertNode { label, key: k, .. } if k == key => {
4513 Some(HistoryChange::NodeInserted {
4514 label: label.clone(),
4515 })
4516 }
4517 WalRecord::InsertNodeId { label, key: k, .. } if k == key => {
4518 let label_str = match self.syms.resolve(*label) {
4519 Some(s) => s.to_string(),
4520 None => continue,
4521 };
4522 Some(HistoryChange::NodeInserted { label: label_str })
4523 }
4524 WalRecord::SetProp {
4525 key: k,
4526 field,
4527 value,
4528 } if k == key => Some(HistoryChange::PropSet {
4529 field: field.clone(),
4530 value: value.clone(),
4531 }),
4532 WalRecord::SetPropId { id, field, value } => match self.ids.key_of(*id) {
4533 Some(resolved) if resolved == key => {
4534 let field_str = match self.syms.resolve(*field) {
4535 Some(s) => s.to_string(),
4536 None => continue,
4537 };
4538 Some(HistoryChange::PropSet {
4539 field: field_str,
4540 value: value.clone(),
4541 })
4542 }
4543 _ => None,
4544 },
4545 WalRecord::RemoveProp { key: k, field } if k == key => {
4546 Some(HistoryChange::PropRemoved {
4547 field: field.clone(),
4548 })
4549 }
4550 WalRecord::InsertEdge {
4551 edge_type,
4552 src_key,
4553 dst_key,
4554 } => {
4555 if src_key == key {
4556 Some(HistoryChange::EdgeAdded {
4557 edge_type: edge_type.clone(),
4558 other: dst_key.clone(),
4559 outgoing: true,
4560 })
4561 } else if dst_key == key {
4562 Some(HistoryChange::EdgeAdded {
4563 edge_type: edge_type.clone(),
4564 other: src_key.clone(),
4565 outgoing: false,
4566 })
4567 } else {
4568 None
4569 }
4570 }
4571 WalRecord::InsertEdgeId { etype, src, dst } => {
4572 let etype_str = match self.syms.resolve(*etype) {
4573 Some(s) => s.to_string(),
4574 None => continue,
4575 };
4576 let src_key = self.ids.key_of(*src);
4577 let dst_key = self.ids.key_of(*dst);
4578 if src_key == Some(key) {
4579 let other = match dst_key {
4580 Some(s) => s.to_string(),
4581 None => continue,
4582 };
4583 Some(HistoryChange::EdgeAdded {
4584 edge_type: etype_str,
4585 other,
4586 outgoing: true,
4587 })
4588 } else if dst_key == Some(key) {
4589 let other = match src_key {
4590 Some(s) => s.to_string(),
4591 None => continue,
4592 };
4593 Some(HistoryChange::EdgeAdded {
4594 edge_type: etype_str,
4595 other,
4596 outgoing: false,
4597 })
4598 } else {
4599 None
4600 }
4601 }
4602 WalRecord::DeleteEdge {
4603 edge_type,
4604 src_key,
4605 dst_key,
4606 } => {
4607 if src_key == key {
4608 Some(HistoryChange::EdgeRemoved {
4609 edge_type: edge_type.clone(),
4610 other: dst_key.clone(),
4611 outgoing: true,
4612 })
4613 } else if dst_key == key {
4614 Some(HistoryChange::EdgeRemoved {
4615 edge_type: edge_type.clone(),
4616 other: src_key.clone(),
4617 outgoing: false,
4618 })
4619 } else {
4620 None
4621 }
4622 }
4623 WalRecord::DeleteNode { key: k } if k == key => {
4624 Some(HistoryChange::NodeDeleted)
4625 }
4626 _ => None,
4628 };
4629
4630 if let Some(change) = change {
4631 out.push(HistoryEntry { commit, change });
4632 }
4633 }
4634 }
4635
4636 Ok(out)
4637 }
4638
4639 pub fn edge_count(&self) -> u64 {
4640 self.topo.edge_count()
4641 }
4642
4643 pub fn stats(&self) -> Stats {
4646 let rules: Vec<RuleStats> = self
4647 .engine
4648 .rules()
4649 .map(|r| RuleStats {
4650 name: r.name.clone(),
4651 edges: self
4652 .engine
4653 .provenance()
4654 .get(&r.name)
4655 .map(|s| s.len() as u64)
4656 .unwrap_or(0),
4657 tripped: self.engine.is_tripped(&r.name),
4658 fires: self.engine.fire_count(&r.name),
4659 approximate: r.approximate,
4660 })
4661 .collect();
4662 Stats {
4663 nodes_live: self.ids.live_len(),
4664 nodes_tombstoned: self.ids.len() - self.ids.live_len(),
4665 edges: self.topo.edge_count(),
4666 rules,
4667 }
4668 }
4669
4670 pub fn format_version() -> u16 {
4672 core_storage::snapshot::VERSION
4673 }
4674
4675 pub fn fs_total_appended(&self) -> usize
4677 where
4678 F: FsIntrospect,
4679 {
4680 self.fs.total_appended()
4681 }
4682
4683 pub fn fs_sync_count(&self) -> usize
4685 where
4686 F: FsIntrospect,
4687 {
4688 self.fs.sync_count()
4689 }
4690
4691 pub fn into_fs(self) -> F {
4693 self.fs
4694 }
4695
4696 pub fn snapshot(&mut self) -> Result<()> {
4697 self.snapshot_with(SnapshotOptions::default())
4698 }
4699
4700 pub fn snapshot_with(&mut self, opts: SnapshotOptions) -> Result<()> {
4721 if self.read_only {
4722 return Err(GraphError::ReadOnly);
4723 }
4724 let (rule_defs_typed, provenance, rule_tripped, rule_fires) = self.engine.to_persist();
4725 let rule_defs = rule_defs_typed
4726 .iter()
4727 .map(|r| bincode::serialize(r).expect("RuleDef serialize cannot fail"))
4728 .collect();
4729 let hnsw_state = self.engine.export_hnsw_state();
4731 let raw_ivf = self.engine.export_ivf_state();
4732 let ivf_state: BTreeMap<String, core_storage::snapshot::PerRuleIvfState> = raw_ivf
4733 .into_iter()
4734 .map(|(name, ((sc, sa, sd), (dc, da, dd)))| {
4735 (
4736 name,
4737 core_storage::snapshot::PerRuleIvfState {
4738 src: core_storage::snapshot::SideIvfState {
4739 centroids: sc,
4740 clusters: sa,
4741 drift: sd,
4742 },
4743 dst: core_storage::snapshot::SideIvfState {
4744 centroids: dc,
4745 clusters: da,
4746 drift: dd,
4747 },
4748 },
4749 )
4750 })
4751 .collect();
4752 let view_defs: Vec<Vec<u8>> = self
4753 .view_store
4754 .views()
4755 .map(|v| bincode::serialize(v).expect("ViewDef serialize cannot fail"))
4756 .collect();
4757 let state = core_storage::snapshot::SnapshotState {
4758 ids: self.ids.clone(),
4759 syms: self.syms.clone(),
4760 topo: self.topo.clone(),
4761 props: self.props.clone(),
4762 labels: self.labels.clone(),
4763 edge_props: self.edge_props.clone(),
4764 rule_defs,
4765 provenance,
4766 rule_tripped,
4767 rule_fires,
4768 ivf_state,
4769 hnsw_state,
4770 view_defs,
4771 wal_truncated: !opts.keep_wal,
4772 };
4773 self.fs
4774 .write_atomic(FileId::Snapshot, &core_storage::snapshot::encode(&state)?)?;
4775
4776 if opts.keep_wal {
4777 } else {
4783 let mut baseline_wal: Vec<u8> = Vec::new();
4792 for (label, field) in self.fulltext.enabled_pairs() {
4793 let rec = WalRecord::EnableFulltext {
4794 label: label.clone(),
4795 field: field.clone(),
4796 };
4797 baseline_wal.extend_from_slice(&encode_record(&rec));
4798 }
4799 self.fs.write_atomic(FileId::Wal, &baseline_wal)?;
4800 }
4801 Ok(())
4802 }
4803}
4804
4805enum BatchOp {
4807 InsertNode {
4808 label: String,
4809 key: String,
4810 props: Vec<(String, Value)>,
4811 },
4812 InsertEdge {
4813 edge_type: String,
4814 src_key: String,
4815 dst_key: String,
4816 },
4817 SetProp {
4818 key: String,
4819 field: String,
4820 value: Value,
4821 },
4822 RemoveProp {
4823 key: String,
4824 field: String,
4825 },
4826 DeleteEdge {
4827 edge_type: String,
4828 src_key: String,
4829 dst_key: String,
4830 },
4831 DeleteNode {
4832 key: String,
4833 },
4834 CreateRule(RuleDef),
4835 DeleteRule {
4836 name: String,
4837 },
4838}
4839
4840#[derive(Default)]
4843struct Overlay {
4844 extra_keys: BTreeSet<String>,
4845 deleted_keys: BTreeSet<String>,
4846 extra_props: BTreeMap<(String, String), Value>,
4847 removed_props: BTreeSet<(String, String)>,
4848 extra_edges: BTreeSet<(String, String, String)>,
4849 deleted_edges: BTreeSet<(String, String, String)>,
4850 extra_rules: BTreeSet<String>,
4851 deleted_rules: BTreeSet<String>,
4852}
4853
4854struct MutPreview<'a, F: Fs> {
4857 db: &'a GraphDb<F>,
4858 overlay: Overlay,
4859}
4860
4861impl<'a, F: Fs> MutPreview<'a, F> {
4862 fn new(db: &'a GraphDb<F>) -> Self {
4863 Self {
4864 db,
4865 overlay: Overlay::default(),
4866 }
4867 }
4868
4869 fn has_key(&self, key: &str) -> bool {
4870 if self.overlay.extra_keys.contains(key) {
4871 return true;
4872 }
4873 if self.overlay.deleted_keys.contains(key) {
4874 return false;
4875 }
4876 self.db.ids.get(key).is_some()
4877 }
4878
4879 fn has_prop(&self, key: &str, field: &str) -> bool {
4880 if !self.has_key(key) {
4881 return false;
4882 }
4883 let k = (key.to_string(), field.to_string());
4884 if self.overlay.removed_props.contains(&k) {
4885 return false;
4886 }
4887 if self.overlay.extra_props.contains_key(&k) {
4888 return true;
4889 }
4890 if self.overlay.extra_keys.contains(key) {
4893 return false;
4894 }
4895 self.db.get_prop(key, field).is_some()
4896 }
4897
4898 fn has_edge(&self, edge_type: &str, src_key: &str, dst_key: &str) -> bool {
4899 let k = (
4900 edge_type.to_string(),
4901 src_key.to_string(),
4902 dst_key.to_string(),
4903 );
4904 if self.overlay.deleted_edges.contains(&k) {
4905 return false;
4906 }
4907 if self.overlay.extra_edges.contains(&k) {
4908 return true;
4909 }
4910 if self.overlay.extra_keys.contains(src_key) || self.overlay.extra_keys.contains(dst_key) {
4912 return false;
4913 }
4914 if self.overlay.deleted_keys.contains(src_key)
4915 || self.overlay.deleted_keys.contains(dst_key)
4916 {
4917 return false;
4918 }
4919 let Some(src) = self.db.ids.get(src_key) else {
4920 return false;
4921 };
4922 let Some(dst) = self.db.ids.get(dst_key) else {
4923 return false;
4924 };
4925 let Some(sym) = self.db.syms.get(edge_type) else {
4926 return false;
4927 };
4928 self.db
4929 .topo
4930 .neighbors(sym, Direction::Out, src)
4931 .binary_search(&dst)
4932 .is_ok()
4933 }
4934
4935 fn has_rule(&self, name: &str) -> bool {
4936 if self.overlay.extra_rules.contains(name) {
4937 return true;
4938 }
4939 if self.overlay.deleted_rules.contains(name) {
4940 return false;
4941 }
4942 self.db.engine.rules().any(|r| r.name == name)
4943 }
4944
4945 fn is_rule_owned(&self, edge_type: &str, src_key: &str, dst_key: &str) -> bool {
4946 if self.overlay.extra_keys.contains(src_key) || self.overlay.extra_keys.contains(dst_key) {
4947 return false;
4948 }
4949 if self.overlay.deleted_keys.contains(src_key)
4950 || self.overlay.deleted_keys.contains(dst_key)
4951 {
4952 return false;
4953 }
4954 let Some(src) = self.db.ids.get(src_key) else {
4955 return false;
4956 };
4957 let Some(dst) = self.db.ids.get(dst_key) else {
4958 return false;
4959 };
4960 let Some(et) = self.db.syms.get(edge_type) else {
4961 return false;
4962 };
4963 if self.overlay.deleted_rules.is_empty() {
4967 return self.db.engine.is_owned(et, src, dst);
4968 }
4969 for (rule, triples) in self.db.engine.provenance() {
4970 if self.overlay.deleted_rules.contains(rule) {
4971 continue;
4972 }
4973 if triples.contains(&(et, src, dst)) {
4974 return true;
4975 }
4976 }
4977 false
4978 }
4979
4980 fn check_insert_node(&self, key: &str) -> Result<()> {
4981 if self.has_key(key) {
4982 Err(GraphError::DuplicateKey { key: key.into() })
4983 } else {
4984 Ok(())
4985 }
4986 }
4987
4988 fn check_live_key(&self, key: &str) -> Result<()> {
4989 if self.has_key(key) {
4990 Ok(())
4991 } else {
4992 Err(GraphError::KeyNotFound { key: key.into() })
4993 }
4994 }
4995
4996 fn prepare_insert_edge(&self, edge_type: &str, src_key: &str, dst_key: &str) -> Result<bool> {
4997 for k in [src_key, dst_key] {
4998 if !self.has_key(k) {
4999 return Err(GraphError::KeyNotFound { key: k.into() });
5000 }
5001 }
5002 if self.is_rule_owned(edge_type, src_key, dst_key) {
5003 return Err(GraphError::RuleOwned {
5004 detail: format!("edge {edge_type} {src_key}→{dst_key} is rule-owned"),
5005 });
5006 }
5007 Ok(!self.has_edge(edge_type, src_key, dst_key))
5008 }
5009
5010 fn prepare_remove_prop(&self, key: &str, field: &str) -> Result<bool> {
5011 self.check_live_key(key)?;
5012 Ok(self.has_prop(key, field))
5013 }
5014
5015 fn prepare_delete_edge(&self, edge_type: &str, src_key: &str, dst_key: &str) -> Result<bool> {
5016 for k in [src_key, dst_key] {
5017 if !self.has_key(k) {
5018 return Err(GraphError::KeyNotFound { key: k.into() });
5019 }
5020 }
5021 if self.is_rule_owned(edge_type, src_key, dst_key) {
5025 return Err(GraphError::RuleOwned {
5026 detail: format!(
5027 "edge {edge_type} {src_key}→{dst_key} is rule-owned; \
5028 delete or change the owning rule"
5029 ),
5030 });
5031 }
5032 if self.would_derive(edge_type, src_key, dst_key) {
5033 return Err(GraphError::RuleOwned {
5034 detail: format!(
5035 "edge {edge_type} {src_key}→{dst_key} is rule-owned; \
5036 delete or change the owning rule, or a live rule would re-derive it"
5037 ),
5038 });
5039 }
5040 Ok(self.has_edge(edge_type, src_key, dst_key))
5041 }
5042
5043 fn would_derive(&self, edge_type: &str, src_key: &str, dst_key: &str) -> bool {
5048 if src_key == dst_key {
5049 return false;
5050 }
5051 let Some(src_label) = self.label_of(src_key) else {
5052 return false;
5053 };
5054 let Some(dst_label) = self.label_of(dst_key) else {
5055 return false;
5056 };
5057 for rule in self.db.engine.rules() {
5058 if self.overlay.deleted_rules.contains(&rule.name) {
5059 continue;
5060 }
5061 if rule.edge_type != edge_type {
5062 continue;
5063 }
5064 if rule.src_label != src_label || rule.dst_label != dst_label {
5065 continue;
5066 }
5067 let src_props = |f: &str| self.prop_value(src_key, f);
5068 let dst_props = |f: &str| self.prop_value(dst_key, f);
5069 let src_view = NodeView {
5070 key: src_key,
5071 props: &src_props,
5072 };
5073 let dst_view = NodeView {
5074 key: dst_key,
5075 props: &dst_props,
5076 };
5077 if evaluate(&rule.predicate, &src_view, &dst_view).is_some() {
5078 return true;
5079 }
5080 }
5081 false
5082 }
5083
5084 fn label_of(&self, key: &str) -> Option<String> {
5085 if self.overlay.deleted_keys.contains(key) {
5086 return None;
5087 }
5088 let id = self.db.ids.get(key)?;
5091 let sym = self.db.labels.get(id as usize).copied()?;
5092 if sym == u32::MAX {
5093 return None;
5094 }
5095 self.db.syms.resolve(sym).map(str::to_string)
5096 }
5097
5098 fn prop_value(&self, key: &str, field: &str) -> Option<Value> {
5099 if !self.has_key(key) {
5100 return None;
5101 }
5102 let k = (key.to_string(), field.to_string());
5103 if self.overlay.removed_props.contains(&k) {
5104 return None;
5105 }
5106 if let Some(v) = self.overlay.extra_props.get(&k) {
5107 return Some(v.clone());
5108 }
5109 if self.overlay.extra_keys.contains(key) {
5110 return None;
5111 }
5112 self.db.get_prop(key, field).cloned()
5113 }
5114
5115 fn check_create_rule(&self, def: &RuleDef) -> Result<()> {
5116 def.validate()
5117 .map_err(|e| GraphError::RuleInvalid { detail: e })?;
5118 if self.has_rule(&def.name) {
5119 return Err(GraphError::RuleInvalid {
5120 detail: format!("rule {:?} already exists", def.name),
5121 });
5122 }
5123 Ok(())
5124 }
5125
5126 fn check_delete_rule(&self, name: &str) -> Result<()> {
5127 if self.has_rule(name) {
5128 Ok(())
5129 } else {
5130 Err(GraphError::RuleNotFound { name: name.into() })
5131 }
5132 }
5133
5134 fn note_insert_node(&mut self, key: &str, props: &[(String, Value)]) {
5135 self.overlay.deleted_keys.remove(key);
5136 self.overlay.extra_keys.insert(key.to_string());
5137 self.overlay.extra_props.retain(|(k, _), _| k != key);
5138 self.overlay.removed_props.retain(|(k, _)| k != key);
5139 for (field, value) in props {
5140 self.overlay
5141 .extra_props
5142 .insert((key.to_string(), field.clone()), value.clone());
5143 }
5144 }
5145
5146 fn note_insert_edge(&mut self, edge_type: &str, src_key: &str, dst_key: &str) {
5147 let k = (
5148 edge_type.to_string(),
5149 src_key.to_string(),
5150 dst_key.to_string(),
5151 );
5152 self.overlay.deleted_edges.remove(&k);
5153 self.overlay.extra_edges.insert(k);
5154 }
5155
5156 fn note_set_prop(&mut self, key: &str, field: &str, value: &Value) {
5157 let k = (key.to_string(), field.to_string());
5158 self.overlay.removed_props.remove(&k);
5159 self.overlay.extra_props.insert(k, value.clone());
5160 }
5161
5162 fn note_remove_prop(&mut self, key: &str, field: &str) {
5163 let k = (key.to_string(), field.to_string());
5164 self.overlay.extra_props.remove(&k);
5165 self.overlay.removed_props.insert(k);
5166 }
5167
5168 fn note_delete_edge(&mut self, edge_type: &str, src_key: &str, dst_key: &str) {
5169 let k = (
5170 edge_type.to_string(),
5171 src_key.to_string(),
5172 dst_key.to_string(),
5173 );
5174 self.overlay.extra_edges.remove(&k);
5175 self.overlay.deleted_edges.insert(k);
5176 }
5177
5178 fn note_delete_node(&mut self, key: &str) {
5179 self.overlay.extra_keys.remove(key);
5180 self.overlay.deleted_keys.insert(key.to_string());
5181 self.overlay.extra_props.retain(|(k, _), _| k != key);
5182 self.overlay.removed_props.retain(|(k, _)| k != key);
5183 self.overlay
5184 .extra_edges
5185 .retain(|(_, s, d)| s != key && d != key);
5186 self.overlay
5187 .deleted_edges
5188 .retain(|(_, s, d)| s != key && d != key);
5189 }
5190
5191 fn note_create_rule(&mut self, name: &str) {
5192 self.overlay.deleted_rules.remove(name);
5193 self.overlay.extra_rules.insert(name.to_string());
5194 }
5195
5196 fn note_delete_rule(&mut self, name: &str) {
5197 self.overlay.extra_rules.remove(name);
5198 self.overlay.deleted_rules.insert(name.to_string());
5199 if let Some(triples) = self.db.engine.provenance().get(name) {
5202 for &(et, s, d) in triples {
5203 let Some(etype) = self.db.syms.resolve(et) else {
5204 continue;
5205 };
5206 let Some(src) = self.db.ids.key_of(s) else {
5207 continue;
5208 };
5209 let Some(dst) = self.db.ids.key_of(d) else {
5210 continue;
5211 };
5212 let k = (etype.to_string(), src.to_string(), dst.to_string());
5213 self.overlay.extra_edges.remove(&k);
5214 self.overlay.deleted_edges.insert(k);
5215 }
5216 }
5217 }
5218}
5219
5220pub struct BatchBuilder<'a, F: Fs> {
5226 db: &'a mut GraphDb<F>,
5227 ops: Vec<BatchOp>,
5228}
5229
5230impl<'a, F: Fs> BatchBuilder<'a, F> {
5231 pub fn insert_node(
5232 &mut self,
5233 label: &str,
5234 key: &str,
5235 props: Vec<(String, Value)>,
5236 ) -> &mut Self {
5237 self.ops.push(BatchOp::InsertNode {
5238 label: label.into(),
5239 key: key.into(),
5240 props,
5241 });
5242 self
5243 }
5244
5245 pub fn insert_edge(&mut self, edge_type: &str, src_key: &str, dst_key: &str) -> &mut Self {
5246 self.ops.push(BatchOp::InsertEdge {
5247 edge_type: edge_type.into(),
5248 src_key: src_key.into(),
5249 dst_key: dst_key.into(),
5250 });
5251 self
5252 }
5253
5254 pub fn set_prop(&mut self, key: &str, field: &str, value: Value) -> &mut Self {
5255 self.ops.push(BatchOp::SetProp {
5256 key: key.into(),
5257 field: field.into(),
5258 value,
5259 });
5260 self
5261 }
5262
5263 pub fn remove_prop(&mut self, key: &str, field: &str) -> &mut Self {
5264 self.ops.push(BatchOp::RemoveProp {
5265 key: key.into(),
5266 field: field.into(),
5267 });
5268 self
5269 }
5270
5271 pub fn delete_edge(&mut self, edge_type: &str, src_key: &str, dst_key: &str) -> &mut Self {
5272 self.ops.push(BatchOp::DeleteEdge {
5273 edge_type: edge_type.into(),
5274 src_key: src_key.into(),
5275 dst_key: dst_key.into(),
5276 });
5277 self
5278 }
5279
5280 pub fn delete_node(&mut self, key: &str) -> &mut Self {
5281 self.ops.push(BatchOp::DeleteNode { key: key.into() });
5282 self
5283 }
5284
5285 pub fn create_rule(&mut self, def: RuleDef) -> &mut Self {
5286 self.ops.push(BatchOp::CreateRule(def));
5287 self
5288 }
5289
5290 pub fn delete_rule(&mut self, name: &str) -> &mut Self {
5291 self.ops.push(BatchOp::DeleteRule { name: name.into() });
5292 self
5293 }
5294
5295 pub fn commit(&mut self) -> Result<(usize, usize)> {
5315 let ops = std::mem::take(&mut self.ops);
5316 self.db.commit_batch(ops)
5317 }
5318
5319 pub(crate) fn commit_ingest(&mut self, label: &str, inserted: usize) -> Result<(usize, usize)> {
5322 let ops = std::mem::take(&mut self.ops);
5323 self.db
5324 .commit_logged_batch(ops, Some((label.to_string(), inserted)))
5325 }
5326}
5327
5328pub struct NodeRef<'a, F: Fs> {
5329 db: &'a GraphDb<F>,
5330 id: u32,
5331}
5332
5333impl<'a, F: Fs> NodeRef<'a, F> {
5334 pub fn key(&self) -> &str {
5335 self.db.ids.key_of(self.id).expect("dense ids")
5336 }
5337
5338 pub fn label(&self) -> &str {
5339 let sym = self
5340 .db
5341 .labels
5342 .get(self.id as usize)
5343 .copied()
5344 .filter(|&s| s != u32::MAX)
5345 .expect("real nodes always have a label; u32::MAX sentinel cannot occur");
5346 self.db.syms.resolve(sym).expect("interned label symbol")
5347 }
5348
5349 pub fn prop(&self, field: &str) -> Option<&Value> {
5350 self.db.props.get(self.id, field)
5351 }
5352
5353 pub fn props(&self) -> BTreeMap<String, Value> {
5355 let mut out = BTreeMap::new();
5356 for field in self.db.props.fields() {
5357 if let Some(v) = self.db.props.get(self.id, field) {
5358 out.insert(field.to_string(), v.clone());
5359 }
5360 }
5361 out
5362 }
5363
5364 pub fn neighborhood(&self, depth: u32, edge_types: Option<&[&str]>, dir: Dir) -> ResultSet {
5366 let view = self.db.view();
5367 let resolved: Option<Vec<u32>> = edge_types.map(|names| {
5368 names
5369 .iter()
5370 .filter_map(|name| view.syms.get(name))
5371 .collect()
5372 });
5373 let nb = neighborhood(&view, self.id, depth, resolved.as_deref(), dir);
5374 let mut rs = ResultSet::new(vec!["key".into(), "label".into(), "depth".into()]);
5375 for (nid, d) in nb.nodes {
5376 let key = view.key_of(nid);
5377 let label = view
5378 .label_of(nid)
5379 .expect("real nodes always have a label; u32::MAX sentinel cannot occur");
5380 rs.push_row(vec![
5381 Some(Value::Str(key.to_string())),
5382 Some(Value::Str(label.to_string())),
5383 Some(Value::Int(d as i64)),
5384 ]);
5385 }
5386 rs
5387 }
5388
5389 pub fn grouped_by_edge_type(&self) -> BTreeMap<String, Vec<String>> {
5391 let view = self.db.view();
5392 let mut groups: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
5393 for e in expand(&view, self.id, None, Dir::Both) {
5394 let etype = view
5395 .syms
5396 .resolve(e.etype)
5397 .expect("topology etype is interned")
5398 .to_string();
5399 let nbr = if e.src == self.id { e.dst } else { e.src };
5400 groups
5401 .entry(etype)
5402 .or_default()
5403 .insert(view.key_of(nbr).to_string());
5404 }
5405 groups
5406 .into_iter()
5407 .map(|(k, v)| (k, v.into_iter().collect()))
5408 .collect()
5409 }
5410}
5411
5412#[cfg(test)]
5413mod tests {
5414 use super::*;
5415 use core_rules::Predicate;
5416
5417 fn tmp_dir(name: &str) -> std::path::PathBuf {
5418 let d =
5419 std::env::temp_dir().join(format!("graphdb-db-unit-{}-{}", name, std::process::id()));
5420 let _ = std::fs::remove_dir_all(&d);
5421 d
5422 }
5423
5424 fn fk_rule() -> RuleDef {
5425 RuleDef {
5426 name: "works_at".into(),
5427 src_label: "Person".into(),
5428 dst_label: "Org".into(),
5429 predicate: Predicate::KeyMatch {
5430 field: "org_id".into(),
5431 },
5432 edge_type: "WORKS_AT".into(),
5433 weight_prop: None,
5434 max_edges: None,
5435 approximate: false,
5436 via_label: None,
5437 via_edge: None,
5438 via_dir: None,
5439 }
5440 }
5441
5442 #[test]
5449 fn no_delta_copy_when_no_views() {
5450 DELTA_COPY_COUNT.with(|c| c.set(0));
5451 let dir = tmp_dir("no-delta-copy");
5452 {
5453 let mut db = GraphDb::open(&dir).unwrap();
5454 for i in 0..50u32 {
5456 db.insert_node("Org", &format!("o{i}"), vec![]).unwrap();
5457 }
5458 for i in 0..50u32 {
5459 db.insert_node(
5460 "Person",
5461 &format!("p{i}"),
5462 vec![("org_id".into(), Value::Str(format!("o{i}")))],
5463 )
5464 .unwrap();
5465 }
5466 db.create_rule(fk_rule()).unwrap();
5468
5469 let copies = DELTA_COPY_COUNT.with(|c| c.get());
5471 assert_eq!(
5472 copies, 0,
5473 "pending_deltas_since().to_vec() called despite no views"
5474 );
5475
5476 let nbrs = db.neighbors("p0", "WORKS_AT", Direction::Out).unwrap();
5479 assert_eq!(
5480 nbrs,
5481 vec!["o0"],
5482 "rule must derive edges even with no views"
5483 );
5484 }
5485 let _ = std::fs::remove_dir_all(&dir);
5486 }
5487
5488 #[test]
5491 fn subscribe_after_backfill_no_stale_events() {
5492 let dir = tmp_dir("sub-after-backfill");
5493 {
5494 let mut db = GraphDb::open(&dir).unwrap();
5495 for i in 0..10u32 {
5496 db.insert_node("Org", &format!("o{i}"), vec![]).unwrap();
5497 db.insert_node(
5498 "Person",
5499 &format!("p{i}"),
5500 vec![("org_id".into(), Value::Str(format!("o{i}")))],
5501 )
5502 .unwrap();
5503 }
5504 db.create_rule(fk_rule()).unwrap();
5506
5507 let sub = db.subscribe_all_rules().unwrap();
5509 assert!(
5511 sub.try_recv().is_none(),
5512 "subscribe after backfill must see no stale events"
5513 );
5514
5515 db.insert_node("Org", "o_new", vec![]).unwrap();
5517 db.insert_node(
5518 "Person",
5519 "p_new",
5520 vec![("org_id".into(), Value::Str("o_new".into()))],
5521 )
5522 .unwrap();
5523 let ev = sub.recv_timeout(std::time::Duration::from_millis(200));
5524 assert!(
5525 ev.is_some(),
5526 "edge-fire event must arrive after subscribe (emit_deltas=true)"
5527 );
5528 }
5529 let _ = std::fs::remove_dir_all(&dir);
5530 }
5531
5532 #[test]
5534 fn subscribe_before_backfill_events_flow() {
5535 let dir = tmp_dir("sub-before-backfill");
5536 {
5537 let mut db = GraphDb::open(&dir).unwrap();
5538 let sub = db.subscribe_all_rules().unwrap();
5540
5541 for i in 0..5u32 {
5542 db.insert_node("Org", &format!("o{i}"), vec![]).unwrap();
5543 db.insert_node(
5544 "Person",
5545 &format!("p{i}"),
5546 vec![("org_id".into(), Value::Str(format!("o{i}")))],
5547 )
5548 .unwrap();
5549 }
5550 db.create_rule(fk_rule()).unwrap();
5552
5553 let mut received = 0usize;
5555 while sub.try_recv().is_some() {
5556 received += 1;
5557 }
5558 assert!(
5559 received > 0,
5560 "subscribe before backfill must receive edge-fire events (got 0)"
5561 );
5562 }
5563 let _ = std::fs::remove_dir_all(&dir);
5564 }
5565
5566 #[test]
5568 fn delta_copy_fires_when_view_exists() {
5569 use core_rules::ViewSource;
5570 DELTA_COPY_COUNT.with(|c| c.set(0));
5571 let dir = tmp_dir("delta-copy-with-view");
5572 {
5573 let mut db = GraphDb::open(&dir).unwrap();
5574 db.insert_node("Org", "o1", vec![]).unwrap();
5575 db.insert_node(
5576 "Person",
5577 "p1",
5578 vec![("org_id".into(), Value::Str("o1".into()))],
5579 )
5580 .unwrap();
5581 db.create_view(ViewDef {
5583 name: "degree_out".into(),
5584 label: "Person".into(),
5585 view_prop: "degree_out".into(),
5586 source: ViewSource::Degree {
5587 edge_type: "WORKS_AT".into(),
5588 direction: Direction::Out,
5589 },
5590 })
5591 .unwrap();
5592 db.create_rule(fk_rule()).unwrap();
5593
5594 let copies = DELTA_COPY_COUNT.with(|c| c.get());
5596 assert!(
5597 copies > 0,
5598 "expected delta copy to fire when a view is defined"
5599 );
5600
5601 let info = db.node_info("p1").unwrap();
5603 let degree = info.props.get("degree_out");
5604 assert!(
5605 degree.is_some(),
5606 "view prop should be written to node props"
5607 );
5608 }
5609 let _ = std::fs::remove_dir_all(&dir);
5610 }
5611
5612 #[test]
5627 fn open_at_derived_edge_view_values_correct() {
5628 use core_rules::ViewSource;
5629 let dir = tmp_dir("open-at-view-rebuild");
5630 {
5631 let mut db = GraphDb::open(&dir).unwrap();
5632 db.insert_node("Org", "o1", vec![]).unwrap();
5634 db.create_view(ViewDef {
5636 name: "employee_count".into(),
5637 label: "Org".into(),
5638 view_prop: "emp".into(),
5639 source: ViewSource::Degree {
5640 edge_type: "WORKS_AT".into(),
5641 direction: Direction::In,
5642 },
5643 })
5644 .unwrap();
5645 db.create_rule(fk_rule()).unwrap();
5647 db.insert_node(
5649 "Person",
5650 "p1",
5651 vec![("org_id".into(), Value::Str("o1".into()))],
5652 )
5653 .unwrap();
5654 db.insert_node(
5656 "Person",
5657 "p2",
5658 vec![("org_id".into(), Value::Str("o1".into()))],
5659 )
5660 .unwrap();
5661 db.insert_node(
5663 "Person",
5664 "p3",
5665 vec![("org_id".into(), Value::Str("o1".into()))],
5666 )
5667 .unwrap();
5668 assert_eq!(
5670 db.get_view_prop("o1", "emp").cloned(),
5671 Some(Value::Int(3)),
5672 "normal db must show degree 3 after 3 derived edges"
5673 );
5674 } let normal_db = GraphDb::open(&dir).unwrap();
5678 let normal_emp = normal_db.get_view_prop("o1", "emp").cloned();
5679 assert_eq!(
5680 normal_emp,
5681 Some(Value::Int(3)),
5682 "re-opened normal db must show degree 3"
5683 );
5684
5685 let aof_latest = GraphDb::open_at(&dir, 5).unwrap();
5687 assert_eq!(
5688 aof_latest.get_view_prop("o1", "emp").cloned(),
5689 normal_emp,
5690 "open_at latest: derived-edge view must equal normal open (rebuild_all required)"
5691 );
5692
5693 let aof_mid = GraphDb::open_at(&dir, 3).unwrap();
5695 assert_eq!(
5696 aof_mid.get_view_prop("o1", "emp").cloned(),
5697 Some(Value::Int(1)),
5698 "open_at mid-history: only p1 exists at frame 3, degree must be 1"
5699 );
5700
5701 let _ = std::fs::remove_dir_all(&dir);
5702 }
5703
5704 #[test]
5708 fn subscribe_on_as_of_returns_read_only_error() {
5709 let dir = tmp_dir("sub-as-of-read-only");
5710 {
5711 let mut db = GraphDb::open(&dir).unwrap();
5712 db.insert_node("Org", "o1", vec![]).unwrap();
5713 db.create_rule(fk_rule()).unwrap();
5714 }
5715 let mut aof = GraphDb::open_at(&dir, 0).unwrap();
5716
5717 assert!(
5718 matches!(
5719 aof.subscribe_all_rules(),
5720 Err(core_storage::GraphError::ReadOnly)
5721 ),
5722 "subscribe_all_rules on as-of must return ReadOnly"
5723 );
5724 assert!(
5725 matches!(
5726 aof.subscribe_writes(),
5727 Err(core_storage::GraphError::ReadOnly)
5728 ),
5729 "subscribe_writes on as-of must return ReadOnly"
5730 );
5731 assert!(
5732 matches!(
5733 aof.subscribe_rule("works_at"),
5734 Err(core_storage::GraphError::ReadOnly)
5735 ),
5736 "subscribe_rule on as-of must return ReadOnly"
5737 );
5738 let _ = std::fs::remove_dir_all(&dir);
5739 }
5740
5741 #[test]
5746 fn dense_rewrite_error_rolls_back_speculative_interns() {
5747 let dir = tmp_dir("dense-rewrite-rollback");
5748 {
5749 let mut db = GraphDb::open(&dir).unwrap();
5750 db.insert_node("Person", "a", vec![]).unwrap();
5751
5752 let err = db.rewrite_wal_dense(vec![WalRecord::InsertEdge {
5756 edge_type: "ORPHAN_TYPE".into(),
5757 src_key: "missing".into(),
5758 dst_key: "a".into(),
5759 }]);
5760 assert!(err.is_err(), "rewrite of a missing src key must fail");
5761 assert_eq!(
5762 db.syms.get("ORPHAN_TYPE"),
5763 None,
5764 "failed rewrite must roll back speculative interns"
5765 );
5766
5767 db.set_prop("a", "later_field", Value::Int(2)).unwrap();
5769 }
5770 let db = GraphDb::open(&dir).expect("WAL must replay after failed rewrite");
5771 assert_eq!(db.get_prop("a", "later_field"), Some(&Value::Int(2)));
5772 let _ = std::fs::remove_dir_all(&dir);
5773 }
5774}