1use std::collections::{BTreeMap, HashSet};
17use std::sync::{Arc, OnceLock};
18
19use core_query::cypher::{execute, is_write_tokens, lex, parse, plan, Params};
20use core_query::{expand, neighborhood, Dir, GraphView, ResultSet};
21use core_storage::fulltext::FulltextIndex;
22use core_storage::v8::seam::{ColumnsView, TopologyView};
23use core_storage::v8::MappedBase;
24use core_storage::wal::WalRecord;
25use core_storage::{
26 ColumnStore, Direction, EdgeProps, EdgePropsView, GraphError, IdMap, Interner, Result,
27 Topology, Value,
28};
29
30use crate::db::{EdgeInfo, NodeInfo};
31use crate::mask::NodeMask;
32use crate::roles::RoleDef;
33
34pub const FOLD_EVERY_K: usize = 64;
37
38pub struct CommitDelta {
40 pub records: Vec<WalRecord>,
42 pub derived_inserts: Vec<(u32, u32, u32)>,
44 pub derived_deletes: Vec<(u32, u32, u32)>,
46}
47
48#[derive(Clone)]
52pub struct FrozenOverlay {
53 pub ids: IdMap,
54 pub syms: Interner,
55 pub topo: Topology,
56 pub props: ColumnStore,
57 pub labels: Vec<u32>,
58 pub edge_props: EdgeProps,
59 pub roles: Option<Vec<RoleDef>>,
60 pub fulltext: FulltextIndex,
61}
62
63pub struct ReaderSnapshot {
72 pub frozen: Arc<FrozenOverlay>,
74 pub base: Option<Arc<MappedBase>>,
76 pub deltas: Vec<Arc<CommitDelta>>,
78 cache: OnceLock<std::result::Result<FrozenOverlay, String>>,
86}
87
88fn build_tv<'a>(topo: &'a Topology, base: &'a Option<Arc<MappedBase>>) -> TopologyView<'a> {
91 match base {
92 None => TopologyView::owned(topo),
93 Some(b) => {
94 let csr = b.topology().expect("base CSR CRC already verified at open");
95 TopologyView::with_base(topo, csr)
96 }
97 }
98}
99
100fn build_cv<'a>(props: &'a ColumnStore, base: &'a Option<Arc<MappedBase>>) -> ColumnsView<'a> {
101 match base {
102 None => ColumnsView::owned(props),
103 Some(b) => {
104 let cols = b
105 .columns()
106 .expect("base columns CRC already verified at open");
107 ColumnsView::with_base(props, cols)
108 }
109 }
110}
111
112fn build_epv<'a>(
113 edge_props: &'a EdgeProps,
114 base: &'a Option<Arc<MappedBase>>,
115) -> EdgePropsView<'a> {
116 match base {
117 None => EdgePropsView::owned(edge_props),
118 Some(b) => {
119 let archived = b
120 .edge_props_section()
121 .expect("base edge_props CRC already verified at open");
122 EdgePropsView::with_base(edge_props, archived)
123 }
124 }
125}
126
127fn make_view<'a>(
128 state: &'a FrozenOverlay,
129 base: &'a Option<Arc<MappedBase>>,
130 mask: Option<&'a HashSet<u32>>,
131) -> GraphView<'a> {
132 GraphView {
133 ids: &state.ids,
134 syms: &state.syms,
135 labels: &state.labels,
136 props: build_cv(&state.props, base),
137 topo: build_tv(&state.topo, base),
138 edge_props: build_epv(&state.edge_props, base),
139 mask,
140 prop_index: None,
143 }
144}
145
146#[allow(clippy::too_many_arguments)]
155fn apply_one(
156 ids: &mut IdMap,
157 syms: &mut Interner,
158 topo: &mut Topology,
159 props: &mut ColumnStore,
160 edge_props: &mut EdgeProps,
161 labels: &mut Vec<u32>,
162 fulltext: &mut FulltextIndex,
163 rec: &WalRecord,
164) -> Result<()> {
165 match rec {
166 WalRecord::Intern { id, text } => {
167 let got = syms.intern(text);
168 if got != *id {
169 return Err(GraphError::Corrupt {
170 detail: format!(
171 "mvcc delta intern mismatch for {text:?}: expected {id}, got {got}"
172 ),
173 });
174 }
175 }
176
177 WalRecord::InsertNodeId {
178 label,
179 key,
180 props: node_props,
181 } => {
182 let node_id = ids.try_insert(key)?;
183 if labels.len() <= node_id as usize {
184 labels.resize(node_id as usize + 1, u32::MAX);
185 }
186 labels[node_id as usize] = *label;
187 let label_str = syms
188 .resolve(*label)
189 .ok_or_else(|| GraphError::Corrupt {
190 detail: format!("mvcc delta: unknown label sym {label}"),
191 })?
192 .to_string();
193 for (field_sym, value) in node_props {
194 let field = syms
195 .resolve(*field_sym)
196 .ok_or_else(|| GraphError::Corrupt {
197 detail: format!("mvcc delta: unknown field sym {field_sym}"),
198 })?
199 .to_string();
200 props.set(node_id, &field, value.clone());
201 if fulltext.is_enabled(&label_str, &field) {
202 fulltext.add_tokens(node_id, &field, value);
203 }
204 }
205 }
206
207 WalRecord::InsertNode {
208 label,
209 key,
210 props: node_props,
211 } => {
212 let label_sym = syms.intern(label);
213 let node_id = ids.try_insert(key)?;
214 if labels.len() <= node_id as usize {
215 labels.resize(node_id as usize + 1, u32::MAX);
216 }
217 labels[node_id as usize] = label_sym;
218 for (field, value) in node_props {
219 props.set(node_id, field, value.clone());
220 if fulltext.is_enabled(label, field) {
221 fulltext.add_tokens(node_id, field, value);
222 }
223 }
224 }
225
226 WalRecord::SetPropId { id, field, value } => {
227 if let Some(field_str) = syms.resolve(*field).map(str::to_string) {
228 props.set(*id, &field_str, value.clone());
229 if let Some(&label_sym) = labels.get(*id as usize) {
230 if let Some(label_str) = syms.resolve(label_sym) {
231 if fulltext.is_enabled(label_str, &field_str) {
232 fulltext.add_tokens(*id, &field_str, value);
233 }
234 }
235 }
236 }
237 }
238
239 WalRecord::SetProp { key, field, value } => {
240 if let Some(node_id) = ids.get(key) {
241 props.set(node_id, field, value.clone());
242 if let Some(&label_sym) = labels.get(node_id as usize) {
243 if let Some(label_str) = syms.resolve(label_sym) {
244 if fulltext.is_enabled(label_str, field) {
245 fulltext.add_tokens(node_id, field, value);
246 }
247 }
248 }
249 }
250 }
251
252 WalRecord::RemoveProp { key, field } => {
253 if let Some(node_id) = ids.get(key) {
254 props.remove(node_id, field);
255 props.record_prop_tombstone(node_id, field);
260 fulltext.remove_node_field(node_id, field);
261 }
262 }
263
264 WalRecord::DeleteNode { key } => {
265 if let Some(node_id) = ids.delete(key) {
266 props.remove_all(node_id);
267 fulltext.remove_node(node_id);
268 if let Some(slot) = labels.get_mut(node_id as usize) {
270 *slot = u32::MAX;
271 }
272 let etypes: Vec<u32> = topo.etypes().collect();
276 let mut doomed = Vec::new();
277 for et in &etypes {
278 for &dst in topo.neighbors(*et, Direction::Out, node_id).as_ref() {
279 doomed.push((*et, node_id, dst));
280 }
281 for &src in topo.neighbors(*et, Direction::In, node_id).as_ref() {
282 doomed.push((*et, src, node_id));
283 }
284 }
285 for (et, s, d) in doomed {
286 topo.remove_edge(et, s, d);
287 edge_props.remove_edge(et, s, d);
288 }
289 }
290 }
291
292 WalRecord::InsertEdgeId { etype, src, dst } => {
293 topo.add_edge(*etype, *src, *dst);
294 }
295
296 WalRecord::InsertEdge {
297 edge_type,
298 src_key,
299 dst_key,
300 } => {
301 let etype = syms.intern(edge_type);
302 if let (Some(src), Some(dst)) = (ids.get(src_key), ids.get(dst_key)) {
303 topo.add_edge(etype, src, dst);
304 }
305 }
306
307 WalRecord::DeleteEdge {
308 edge_type,
309 src_key,
310 dst_key,
311 } => {
312 if let Some(etype) = syms.get(edge_type) {
313 if let (Some(src), Some(dst)) = (ids.get(src_key), ids.get(dst_key)) {
314 topo.remove_edge(etype, src, dst);
315 }
316 }
317 }
318
319 WalRecord::EnableFulltext { label, field } => {
320 fulltext.enable(label, field);
321 }
322
323 WalRecord::DisableFulltext { label, field } => {
324 fulltext.disable(label, field);
325 }
326
327 WalRecord::Batch(inner) => {
328 for r in inner {
329 apply_one(ids, syms, topo, props, edge_props, labels, fulltext, r)?;
330 }
331 }
332
333 WalRecord::CreateRule { .. }
336 | WalRecord::DeleteRule { .. }
337 | WalRecord::RebuildRule { .. }
338 | WalRecord::CreateView { .. }
339 | WalRecord::DeleteView { .. }
340 | WalRecord::EnableIndex { .. }
344 | WalRecord::DisableIndex { .. }
345 | WalRecord::DerivedEdgeAdded { .. }
349 | WalRecord::DerivedEdgeRetracted { .. } => {}
350
351 WalRecord::RenameNode { old_key, new_key } => {
352 if ids.get(old_key).is_some() {
355 ids.rename(old_key, new_key).map_err(|_| GraphError::Corrupt {
356 detail: format!("mvcc delta RenameNode {old_key}→{new_key} failed"),
357 })?;
358 }
359 }
360 }
361 Ok(())
362}
363
364fn mask_for_role_from(state: &FrozenOverlay, role: &str) -> Result<NodeMask> {
367 let roles = state.roles.as_ref().ok_or_else(|| GraphError::Corrupt {
368 detail: "roles.json was corrupt at open; fix the file and re-open".into(),
369 })?;
370 let def = roles
371 .iter()
372 .find(|r| r.name == role)
373 .ok_or_else(|| GraphError::KeyNotFound {
374 key: format!("role:{role}"),
375 })?;
376 let mut visible = HashSet::new();
377 for key in &def.keys {
378 if let Some(id) = state.ids.get(key) {
379 visible.insert(id);
380 }
381 }
382 for label_name in &def.labels {
383 if let Some(sym) = state.syms.get(label_name) {
384 for (i, &s) in state.labels.iter().enumerate() {
385 if s == sym {
386 visible.insert(i as u32);
387 }
388 }
389 }
390 }
391 Ok(NodeMask::from_ids(visible))
392}
393
394impl ReaderSnapshot {
395 fn materialize(&self) -> Result<FrozenOverlay> {
400 let mut w = (*self.frozen).clone();
401 for delta in &self.deltas {
402 for rec in &delta.records {
403 apply_one(
404 &mut w.ids,
405 &mut w.syms,
406 &mut w.topo,
407 &mut w.props,
408 &mut w.edge_props,
409 &mut w.labels,
410 &mut w.fulltext,
411 rec,
412 )?;
413 }
414 for &(etype, src, dst) in &delta.derived_inserts {
415 w.topo.add_edge(etype, src, dst);
416 }
417 for &(etype, src, dst) in &delta.derived_deletes {
418 w.topo.remove_edge(etype, src, dst);
419 }
420 }
421 if !self.deltas.is_empty() {
422 let cv = build_cv(&w.props, &self.base);
425 w.fulltext.rebuild_all(&w.ids, &w.labels, &w.syms, cv);
426 }
427 Ok(w)
428 }
429
430 pub(crate) fn new(
435 frozen: Arc<FrozenOverlay>,
436 base: Option<Arc<MappedBase>>,
437 deltas: Vec<Arc<CommitDelta>>,
438 ) -> Self {
439 Self {
440 frozen,
441 base,
442 deltas,
443 cache: OnceLock::new(),
444 }
445 }
446
447 fn effective(&self) -> Result<&FrozenOverlay> {
457 if self.deltas.is_empty() {
458 return Ok(&self.frozen);
459 }
460 let cached = self
461 .cache
462 .get_or_init(|| self.materialize().map_err(|e| e.to_string()));
463 cached
464 .as_ref()
465 .map_err(|e| GraphError::Corrupt { detail: e.clone() })
466 }
467
468 pub fn mask_for_role(&self, role: &str) -> Result<NodeMask> {
476 mask_for_role_from(self.effective()?, role)
477 }
478
479 pub fn resolve_key(&self, key: &str) -> Option<u32> {
484 self.effective().ok()?.ids.get(key)
485 }
486
487 pub fn query(&self, cypher: &str, params: &BTreeMap<String, Value>) -> Result<ResultSet> {
489 let tokens = lex(cypher).map_err(|e| GraphError::QueryError {
490 detail: format!("lex: {e}"),
491 })?;
492 let ast = parse(&tokens).map_err(|e| GraphError::QueryError {
493 detail: format!("parse: {e}"),
494 })?;
495 let ops = plan(&ast).map_err(|e| GraphError::QueryError {
496 detail: format!("plan: {e}"),
497 })?;
498 let state = self.effective()?;
499 let view = make_view(state, &self.base, None);
500 execute(&view, &ops, &Params(params)).map_err(|e| GraphError::QueryError {
501 detail: format!("execute: {e}"),
502 })
503 }
504
505 pub fn query_masked(
509 &self,
510 cypher: &str,
511 params: &BTreeMap<String, Value>,
512 mask: &NodeMask,
513 ) -> Result<ResultSet> {
514 let tokens = lex(cypher).map_err(|e| GraphError::QueryError {
515 detail: format!("lex: {e}"),
516 })?;
517 if is_write_tokens(&tokens) {
518 return Err(GraphError::QueryError {
519 detail: "masked queries are read-only".into(),
520 });
521 }
522 let ast = parse(&tokens).map_err(|e| GraphError::QueryError {
523 detail: format!("parse: {e}"),
524 })?;
525 let ops = plan(&ast).map_err(|e| GraphError::QueryError {
526 detail: format!("plan: {e}"),
527 })?;
528 let state = self.effective()?;
529 let view = make_view(state, &self.base, Some(&mask.visible));
530 execute(&view, &ops, &Params(params)).map_err(|e| GraphError::QueryError {
531 detail: format!("execute: {e}"),
532 })
533 }
534
535 pub fn node_info(&self, key: &str) -> Option<NodeInfo> {
537 node_info_from(key, self.effective().ok()?, &self.base)
538 }
539
540 pub fn node_edges(&self, key: &str) -> Result<Vec<EdgeInfo>> {
545 node_edges_from(key, self.effective()?, &self.base)
546 }
547
548 pub fn neighborhood_masked(
553 &self,
554 key: &str,
555 depth: u32,
556 edge_types: Option<&[&str]>,
557 dir: Dir,
558 mask: &NodeMask,
559 ) -> Option<ResultSet> {
560 neighborhood_masked_from(
561 key,
562 self.effective().ok()?,
563 &self.base,
564 depth,
565 edge_types,
566 dir,
567 mask,
568 )
569 }
570}
571
572fn node_info_from(
575 key: &str,
576 state: &FrozenOverlay,
577 base: &Option<Arc<MappedBase>>,
578) -> Option<NodeInfo> {
579 let id = state.ids.get(key)?;
580 let label_sym = *state.labels.get(id as usize)?;
581 if label_sym == u32::MAX {
582 return None;
583 }
584 let label = state.syms.resolve(label_sym)?.to_string();
585 let cv = build_cv(&state.props, base);
586 let mut props = BTreeMap::new();
587 for field in cv.field_names() {
588 if let Some(vr) = cv.get(id, &field) {
589 props.insert(field, vr.into_value());
590 }
591 }
592 Some(NodeInfo {
593 key: key.to_string(),
594 label,
595 props,
596 })
597}
598
599fn node_edges_from(
600 key: &str,
601 state: &FrozenOverlay,
602 base: &Option<Arc<MappedBase>>,
603) -> Result<Vec<EdgeInfo>> {
604 let id = state
605 .ids
606 .get(key)
607 .ok_or_else(|| GraphError::KeyNotFound { key: key.into() })?;
608 let tv = build_tv(&state.topo, base);
609 let mut edges = Vec::new();
610 for etype in tv.etypes() {
611 let edge_type = state
612 .syms
613 .resolve(etype)
614 .ok_or_else(|| GraphError::Corrupt {
615 detail: format!("reader: topology etype {etype} not in interner"),
616 })?
617 .to_string();
618 for dir in [Direction::Out, Direction::In] {
619 for &nbr in tv.neighbors(etype, dir, id).as_ref() {
620 let (src_key, dst_key) = match dir {
621 Direction::Out => (
622 key.to_string(),
623 state
624 .ids
625 .key_of(nbr)
626 .ok_or_else(|| GraphError::Corrupt {
627 detail: format!("topology id {nbr} has no key"),
628 })?
629 .to_string(),
630 ),
631 Direction::In => (
632 state
633 .ids
634 .key_of(nbr)
635 .ok_or_else(|| GraphError::Corrupt {
636 detail: format!("topology id {nbr} has no key"),
637 })?
638 .to_string(),
639 key.to_string(),
640 ),
641 };
642 edges.push(EdgeInfo {
643 edge_type: edge_type.clone(),
644 src_key,
645 dst_key,
646 derived: false,
647 });
648 }
649 }
650 }
651 edges.sort_by(|a, b| {
652 a.edge_type
653 .cmp(&b.edge_type)
654 .then(a.src_key.cmp(&b.src_key))
655 .then(a.dst_key.cmp(&b.dst_key))
656 });
657 edges.dedup();
658 Ok(edges)
659}
660
661fn neighborhood_masked_from(
662 key: &str,
663 state: &FrozenOverlay,
664 base: &Option<Arc<MappedBase>>,
665 depth: u32,
666 edge_types: Option<&[&str]>,
667 dir: Dir,
668 mask: &NodeMask,
669) -> Option<ResultSet> {
670 let start_id = state.ids.get(key)?;
671 let view = make_view(state, base, Some(&mask.visible));
672 let resolved: Option<Vec<u32>> = edge_types.map(|names| {
673 names
674 .iter()
675 .filter_map(|name| view.syms.get(name))
676 .collect()
677 });
678 let nb = neighborhood(&view, start_id, depth, resolved.as_deref(), dir);
679 let mut rs = ResultSet::new(vec!["key".into(), "label".into(), "depth".into()]);
680 let mut visited: Vec<(u32, u32)> = Vec::with_capacity(nb.nodes.len() + 1);
682 visited.push((start_id, 0));
683 for (nid, d) in &nb.nodes {
684 let k = view.key_of(*nid);
685 let lbl = view
686 .label_of(*nid)
687 .expect("real nodes always have a label; u32::MAX sentinel cannot occur");
688 rs.push_row(vec![
689 Some(Value::Str(k.to_string())),
690 Some(Value::Str(lbl.to_string())),
691 Some(Value::Int(*d as i64)),
692 ]);
693 visited.push((*nid, *d));
694 }
695 if mask.mode() == crate::mask::MaskMode::Stub {
703 let raw_view = make_view(state, base, None);
704 let mut seen: HashSet<u32> = visited.iter().map(|(id, _)| *id).collect();
705 for (node_id, node_depth) in &visited {
706 if *node_depth >= depth {
707 continue;
708 }
709 for e in expand(&raw_view, *node_id, resolved.as_deref(), dir) {
710 let nbr = if e.src == *node_id { e.dst } else { e.src };
711 if !mask.contains_id(nbr) && seen.insert(nbr) {
712 if let Some(k) = state.ids.key_of(nbr) {
713 rs.push_row(vec![
714 Some(Value::Str(k.to_string())),
715 None,
716 Some(Value::Int((*node_depth + 1) as i64)),
717 ]);
718 }
719 }
720 }
721 }
722 }
723 Some(rs)
724}