Skip to main content

uqa_graph/cypher/
writer.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Mutating Cypher executor: extends the read-only [`CypherExecutor`]
8//! pipeline with `CREATE`, `SET`, `MERGE`, `DELETE` / `DETACH DELETE`,
9//! and `UNWIND` clauses. Reads delegate to a transient
10//! [`CypherExecutor`] view that borrows the store immutably.
11
12use std::collections::BTreeMap;
13
14use uqa_core::{Edge, EdgeId, Value, Vertex, VertexId};
15
16use crate::cypher::ast::{
17    CreateClause, CypherClause, CypherExpr, CypherQuery, DeleteClause, MergeClause, NodePattern,
18    PathElement, PathPattern, PropertyAccess, RelDirection, RelPattern, SetClause, SetItem,
19    SetOperator, UnwindClause, Variable,
20};
21use crate::cypher::executor::{Binding, BindingRow, CypherError, CypherExecutor, ResultRow};
22use crate::store::GraphStore;
23
24/// Mutating Cypher executor. Holds a unique borrow of the graph store
25/// and a copy of the parameter map; reads are routed through a
26/// transient read-only view.
27pub struct CypherWriter<'a, G: GraphStore> {
28    pub store: &'a mut G,
29    pub graph: String,
30    pub params: BTreeMap<String, Value>,
31}
32
33impl<'a, G: GraphStore> CypherWriter<'a, G> {
34    pub fn new(store: &'a mut G, graph: impl Into<String>) -> Self {
35        Self {
36            store,
37            graph: graph.into(),
38            params: BTreeMap::new(),
39        }
40    }
41
42    pub fn with_params(mut self, params: BTreeMap<String, Value>) -> Self {
43        self.params = params;
44        self
45    }
46
47    fn reader(&self) -> CypherExecutor<'_, G> {
48        let mut exec = CypherExecutor::new(&*self.store, &self.graph);
49        exec.params = self.params.clone();
50        exec
51    }
52
53    pub fn execute(
54        &mut self,
55        query: &CypherQuery,
56    ) -> Result<(Vec<String>, Vec<ResultRow>), CypherError> {
57        let mut bindings: Vec<BindingRow> = vec![BTreeMap::new()];
58        let mut columns: Vec<String> = Vec::new();
59        let mut rows: Vec<ResultRow> = Vec::new();
60        for clause in &query.clauses {
61            match clause {
62                CypherClause::Match(m) => {
63                    bindings = self.reader().exec_match(m, &bindings)?;
64                }
65                CypherClause::Create(c) => {
66                    bindings = self.exec_create(c, bindings)?;
67                }
68                CypherClause::Merge(m) => {
69                    bindings = self.exec_merge(m, bindings)?;
70                }
71                CypherClause::Set(s) => {
72                    bindings = self.exec_set(s, bindings)?;
73                }
74                CypherClause::Delete(d) => {
75                    bindings = self.exec_delete(d, bindings)?;
76                }
77                CypherClause::Unwind(u) => {
78                    bindings = self.exec_unwind(u, bindings)?;
79                }
80                CypherClause::With(w) => {
81                    let (cols, projected) = self.reader().exec_return_like(
82                        &w.items,
83                        w.distinct,
84                        w.order_by.as_deref(),
85                        w.skip.as_ref(),
86                        w.limit.as_ref(),
87                        &bindings,
88                    )?;
89                    let reader = self.reader();
90                    let mut next = Vec::with_capacity(projected.len());
91                    for row in projected {
92                        if let Some(filter) = &w.r#where {
93                            if !reader.where_passes(filter, &row)? {
94                                continue;
95                            }
96                        }
97                        next.push(CypherExecutor::<G>::row_to_bindings(&cols, &row));
98                    }
99                    drop(reader);
100                    bindings = next;
101                }
102                CypherClause::Return(r) => {
103                    let (cols, ret_rows) = self.reader().exec_return_like(
104                        &r.items,
105                        r.distinct,
106                        r.order_by.as_deref(),
107                        r.skip.as_ref(),
108                        r.limit.as_ref(),
109                        &bindings,
110                    )?;
111                    columns = cols;
112                    rows = ret_rows;
113                }
114            }
115        }
116        Ok((columns, rows))
117    }
118
119    // -----------------------------------------------------------------
120    // CREATE
121    // -----------------------------------------------------------------
122
123    fn exec_create(
124        &mut self,
125        clause: &CreateClause,
126        bindings: Vec<BindingRow>,
127    ) -> Result<Vec<BindingRow>, CypherError> {
128        let mut next = Vec::with_capacity(bindings.len());
129        for mut row in bindings {
130            for pattern in &clause.patterns {
131                self.create_path(pattern, &mut row)?;
132            }
133            next.push(row);
134        }
135        Ok(next)
136    }
137
138    fn create_path(
139        &mut self,
140        pattern: &PathPattern,
141        row: &mut BindingRow,
142    ) -> Result<(), CypherError> {
143        let elements = &pattern.elements;
144        // The vertex id the path currently stands on. Anonymous nodes
145        // participate positionally without a variable binding.
146        let mut position: Option<VertexId> = None;
147        let mut idx = 0;
148        while idx < elements.len() {
149            match &elements[idx] {
150                PathElement::Node(np) => {
151                    position = Some(self.resolve_or_create_vertex(np, row)?);
152                    idx += 1;
153                }
154                PathElement::Rel(rp) => {
155                    let Some(PathElement::Node(next_np)) = elements.get(idx + 1) else {
156                        return Err(CypherError::Unsupported("path must end on a node".into()));
157                    };
158                    let src_id = position.ok_or_else(|| {
159                        CypherError::Unsupported("relationship without prior node".into())
160                    })?;
161                    let tgt_id = self.resolve_or_create_vertex(next_np, row)?;
162                    let edge = self.create_edge(rp, row, src_id, tgt_id)?;
163                    if let Some(var) = &rp.variable {
164                        row.insert(var.clone(), Binding::Edge(edge));
165                    }
166                    position = Some(tgt_id);
167                    idx += 2;
168                }
169            }
170        }
171        Ok(())
172    }
173
174    /// Vertex id for a CREATE node pattern: reuse the bound vertex when
175    /// the variable already resolves to one, otherwise create a fresh
176    /// vertex (binding it when a variable is present).
177    fn resolve_or_create_vertex(
178        &mut self,
179        np: &NodePattern,
180        row: &mut BindingRow,
181    ) -> Result<VertexId, CypherError> {
182        if let Some(var) = &np.variable {
183            match row.get(var) {
184                Some(Binding::Vertex(v)) => return Ok(v.vertex_id),
185                Some(_) => {
186                    return Err(CypherError::TypeError(format!("{var:?} is not a vertex")));
187                }
188                None => {}
189            }
190        }
191        let vertex = self.create_vertex(np, row)?;
192        let id = vertex.vertex_id;
193        if let Some(var) = &np.variable {
194            row.insert(var.clone(), Binding::Vertex(vertex));
195        }
196        Ok(id)
197    }
198
199    fn create_vertex(
200        &mut self,
201        pat: &NodePattern,
202        row: &BindingRow,
203    ) -> Result<Vertex, CypherError> {
204        let label = pat.labels.first().cloned().unwrap_or_default();
205        let mut props: BTreeMap<String, Value> = BTreeMap::new();
206        if let Some(map) = &pat.properties {
207            let reader = self.reader();
208            for (k, expr) in map {
209                props.insert(k.clone(), reader.eval(expr, row)?);
210            }
211        }
212        // AGE graphid allocation: (label_id << 48) | per-label sequence.
213        let vid = self
214            .store
215            .allocate_vertex_id(&label, &self.graph)
216            .map_err(|error| CypherError::Storage(error.to_string()))?;
217        let vertex = Vertex {
218            vertex_id: vid,
219            label,
220            properties: props,
221        };
222        self.store.add_vertex(vertex.clone(), &self.graph)?;
223        Ok(vertex)
224    }
225
226    fn create_edge(
227        &mut self,
228        pat: &RelPattern,
229        row: &BindingRow,
230        mut src_id: VertexId,
231        mut tgt_id: VertexId,
232    ) -> Result<Edge, CypherError> {
233        if pat.direction == RelDirection::Left {
234            std::mem::swap(&mut src_id, &mut tgt_id);
235        }
236        let label = pat.types.first().cloned().unwrap_or_default();
237        let mut props: BTreeMap<String, Value> = BTreeMap::new();
238        if let Some(map) = &pat.properties {
239            let reader = self.reader();
240            for (k, expr) in map {
241                props.insert(k.clone(), reader.eval(expr, row)?);
242            }
243        }
244        // AGE graphid allocation: (label_id << 48) | per-label sequence.
245        let eid = self
246            .store
247            .allocate_edge_id(&label, &self.graph)
248            .map_err(|error| CypherError::Storage(error.to_string()))?;
249        let edge = Edge {
250            edge_id: eid,
251            source_id: src_id,
252            target_id: tgt_id,
253            label,
254            properties: props,
255        };
256        self.store.add_edge(edge.clone(), &self.graph)?;
257        Ok(edge)
258    }
259
260    // -----------------------------------------------------------------
261    // SET
262    // -----------------------------------------------------------------
263
264    fn exec_set(
265        &mut self,
266        clause: &SetClause,
267        bindings: Vec<BindingRow>,
268    ) -> Result<Vec<BindingRow>, CypherError> {
269        let mut next = Vec::with_capacity(bindings.len());
270        for mut row in bindings {
271            for item in &clause.items {
272                self.apply_set_item(item, &mut row)?;
273            }
274            next.push(row);
275        }
276        Ok(next)
277    }
278
279    fn apply_set_item(&mut self, item: &SetItem, row: &mut BindingRow) -> Result<(), CypherError> {
280        let value = self.reader().eval(&item.value, row)?;
281        match &item.target {
282            CypherExpr::PropertyAccess(PropertyAccess { variable, keys }) => {
283                let Some(binding) = row.get(variable).cloned() else {
284                    return Err(CypherError::UndefinedVariable(variable.clone()));
285                };
286                match binding {
287                    Binding::Vertex(vertex) => {
288                        let mut new_props = vertex.properties.clone();
289                        apply_property_update(&mut new_props, keys, &value, item.operator)?;
290                        let updated = Vertex {
291                            vertex_id: vertex.vertex_id,
292                            label: vertex.label.clone(),
293                            properties: new_props,
294                        };
295                        self.store.add_vertex(updated.clone(), &self.graph)?;
296                        row.insert(variable.clone(), Binding::Vertex(updated));
297                    }
298                    Binding::Edge(edge) => {
299                        let mut new_props = edge.properties.clone();
300                        apply_property_update(&mut new_props, keys, &value, item.operator)?;
301                        let updated = Edge {
302                            edge_id: edge.edge_id,
303                            source_id: edge.source_id,
304                            target_id: edge.target_id,
305                            label: edge.label.clone(),
306                            properties: new_props,
307                        };
308                        self.store.add_edge(updated.clone(), &self.graph)?;
309                        row.insert(variable.clone(), Binding::Edge(updated));
310                    }
311                    _ => {
312                        return Err(CypherError::TypeError(format!(
313                            "SET target {variable:?} is not a vertex or edge"
314                        )));
315                    }
316                }
317            }
318            CypherExpr::Variable(Variable { name }) => {
319                // `SET n = {props}` or `SET n += {props}`.
320                let Value::Map(replacement) = value else {
321                    return Err(CypherError::TypeError(
322                        "SET <var> = <expr> requires a map RHS".into(),
323                    ));
324                };
325                let Some(binding) = row.get(name).cloned() else {
326                    return Err(CypherError::UndefinedVariable(name.clone()));
327                };
328                if let Binding::Vertex(vertex) = binding {
329                    let mut new_props = match item.operator {
330                        SetOperator::Assign => BTreeMap::new(),
331                        SetOperator::Update => vertex.properties.clone(),
332                    };
333                    new_props.extend(replacement);
334                    let updated = Vertex {
335                        vertex_id: vertex.vertex_id,
336                        label: vertex.label.clone(),
337                        properties: new_props,
338                    };
339                    self.store.add_vertex(updated.clone(), &self.graph)?;
340                    row.insert(name.clone(), Binding::Vertex(updated));
341                } else {
342                    return Err(CypherError::TypeError(format!(
343                        "SET {name:?} target must be a vertex"
344                    )));
345                }
346            }
347            other => {
348                return Err(CypherError::Unsupported(format!("SET target {other:?}")));
349            }
350        }
351        Ok(())
352    }
353
354    // -----------------------------------------------------------------
355    // DELETE / DETACH DELETE
356    // -----------------------------------------------------------------
357
358    fn exec_delete(
359        &mut self,
360        clause: &DeleteClause,
361        bindings: Vec<BindingRow>,
362    ) -> Result<Vec<BindingRow>, CypherError> {
363        let mut to_delete_vertices: Vec<VertexId> = Vec::new();
364        let mut to_delete_edges: Vec<EdgeId> = Vec::new();
365        for row in &bindings {
366            for expr in &clause.expressions {
367                let CypherExpr::Variable(Variable { name }) = expr else {
368                    return Err(CypherError::Unsupported(
369                        "DELETE only supports bare variable references".into(),
370                    ));
371                };
372                let Some(binding) = row.get(name) else {
373                    return Err(CypherError::UndefinedVariable(name.clone()));
374                };
375                match binding {
376                    Binding::Vertex(v) => to_delete_vertices.push(v.vertex_id),
377                    Binding::Edge(e) => to_delete_edges.push(e.edge_id),
378                    _ => {
379                        return Err(CypherError::TypeError(format!(
380                            "DELETE {name:?} is not a vertex or edge"
381                        )));
382                    }
383                }
384            }
385        }
386        // Edges first to avoid double-delete via vertex DETACH paths.
387        to_delete_edges.sort_unstable();
388        to_delete_edges.dedup();
389        for eid in &to_delete_edges {
390            self.store.remove_edge(*eid, &self.graph)?;
391        }
392        to_delete_vertices.sort_unstable();
393        to_delete_vertices.dedup();
394        for vid in &to_delete_vertices {
395            if !clause.detach {
396                let has_out = !self.store.out_edge_ids(*vid, &self.graph)?.is_empty();
397                let has_in = !self.store.in_edge_ids(*vid, &self.graph)?.is_empty();
398                if has_out || has_in {
399                    return Err(CypherError::TypeError(format!(
400                        "cannot delete vertex {vid}: has incident edges, use DETACH DELETE"
401                    )));
402                }
403            }
404            self.store.remove_vertex(*vid, &self.graph)?;
405        }
406        Ok(bindings)
407    }
408
409    // -----------------------------------------------------------------
410    // MERGE
411    // -----------------------------------------------------------------
412
413    fn exec_merge(
414        &mut self,
415        clause: &MergeClause,
416        bindings: Vec<BindingRow>,
417    ) -> Result<Vec<BindingRow>, CypherError> {
418        let mut next: Vec<BindingRow> = Vec::new();
419        for row in bindings {
420            let matches = self.reader().match_path_pattern(&clause.pattern, &row)?;
421            if matches.is_empty() {
422                let mut row = row;
423                self.create_path(&clause.pattern, &mut row)?;
424                if let Some(items) = &clause.on_create_set {
425                    for item in items {
426                        self.apply_set_item(item, &mut row)?;
427                    }
428                }
429                next.push(row);
430            } else {
431                for mut matched in matches {
432                    if let Some(items) = &clause.on_match_set {
433                        for item in items {
434                            self.apply_set_item(item, &mut matched)?;
435                        }
436                    }
437                    next.push(matched);
438                }
439            }
440        }
441        Ok(next)
442    }
443
444    // -----------------------------------------------------------------
445    // UNWIND
446    // -----------------------------------------------------------------
447
448    fn exec_unwind(
449        &mut self,
450        clause: &UnwindClause,
451        bindings: Vec<BindingRow>,
452    ) -> Result<Vec<BindingRow>, CypherError> {
453        let mut next = Vec::new();
454        for row in bindings {
455            let value = self.reader().eval(&clause.expr, &row)?;
456            // AGE semantics: lists spread one row per element, null
457            // yields no rows, any other scalar passes through as a
458            // single row.
459            let items = match value {
460                Value::List(items) => items,
461                Value::Null => continue,
462                other => vec![other],
463            };
464            for item in items {
465                let mut new_row = row.clone();
466                new_row.insert(clause.variable.clone(), Binding::Value(item));
467                next.push(new_row);
468            }
469        }
470        Ok(next)
471    }
472}
473
474fn apply_property_update(
475    props: &mut BTreeMap<String, Value>,
476    keys: &[String],
477    value: &Value,
478    op: SetOperator,
479) -> Result<(), CypherError> {
480    if keys.is_empty() {
481        return Err(CypherError::TypeError(
482            "SET property path must contain at least one key".to_string(),
483        ));
484    }
485    if keys.len() == 1 {
486        let key = keys[0].clone();
487        match (op, value) {
488            (SetOperator::Update, Value::Map(rhs)) => {
489                let mut existing = match props.remove(&key) {
490                    Some(Value::Map(m)) => m,
491                    _ => BTreeMap::new(),
492                };
493                existing.extend(rhs.clone());
494                props.insert(key, Value::Map(existing));
495            }
496            _ => {
497                props.insert(key, value.clone());
498            }
499        }
500        return Ok(());
501    }
502    // Nested path: descend, creating maps as needed.
503    let mut cursor = props;
504    for key in &keys[..keys.len() - 1] {
505        let entry = cursor
506            .entry(key.clone())
507            .or_insert_with(|| Value::Map(BTreeMap::new()));
508        if !matches!(entry, Value::Map(_)) {
509            *entry = Value::Map(BTreeMap::new());
510        }
511        let Value::Map(inner) = entry else {
512            return Err(CypherError::TypeError(format!(
513                "SET property path component {key:?} is not a map"
514            )));
515        };
516        cursor = inner;
517    }
518    let Some(leaf) = keys.last().cloned() else {
519        return Err(CypherError::TypeError(
520            "SET property path must contain at least one key".to_string(),
521        ));
522    };
523    cursor.insert(leaf, value.clone());
524    Ok(())
525}