1use ahash::{AHashMap, AHashSet};
12
13use dr_strange_core::{
14 Dir, LogicalPlan, NodeId, PlaneHandle, PropDesc, PropValue, Properties, WriteTxn,
15};
16
17use crate::ast::*;
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct WriteSummary {
23 pub nodes_created: u64,
24 pub edges_created: u64,
25 pub props_set: u64,
26 pub labels_set: u64,
27 pub nodes_deleted: u64,
28 pub edges_deleted: u64,
29}
30
31#[derive(Debug)]
34pub struct WriteStatement {
35 binding: Option<(String, LogicalPlan)>,
37 ops: Vec<WriteOp>,
38 has_label_ops: bool,
40 params: crate::Params,
42}
43
44impl WriteStatement {
45 pub fn apply(&self, plane: &PlaneHandle<'_>) -> Result<WriteSummary, String> {
47 execute(plane, self)
48 }
49}
50
51fn terminal_var(p: &Pattern) -> Option<&str> {
53 match p.rest.last() {
54 Some((_, n)) => n.var.as_deref(),
55 None => p.first.var.as_deref(),
56 }
57}
58
59fn set_item_var(it: &SetItem) -> &str {
60 match it {
61 SetItem::Prop { var, .. } | SetItem::Label { var, .. } | SetItem::Merge { var, .. } => var,
62 }
63}
64
65fn remove_item_var(it: &RemoveItem) -> &str {
66 match it {
67 RemoveItem::Prop { var, .. } | RemoveItem::Label { var, .. } => var,
68 }
69}
70
71fn is_label_op(op: &WriteOp) -> bool {
72 match op {
73 WriteOp::Set(items) => items.iter().any(|i| matches!(i, SetItem::Label { .. })),
74 WriteOp::Remove(items) => items.iter().any(|i| matches!(i, RemoveItem::Label { .. })),
75 _ => false,
76 }
77}
78
79pub fn compile(mut ast: WriteAst, params: crate::Params) -> Result<WriteStatement, String> {
81 let has_label_ops = ast.ops.iter().any(is_label_op);
82 resolve_keys(&mut ast.ops, ¶ms)?;
83
84 let binding = match ast.match_clause {
85 None => {
87 for op in &ast.ops {
88 match op {
89 WriteOp::Create(_) => {}
90 WriteOp::Merge(m) => validate_merge(m)?,
91 _ => {
92 return Err(
93 "SET / REMOVE / DELETE require a MATCH to select nodes".to_string()
94 );
95 }
96 }
97 }
98 None
99 }
100 Some(m) => {
102 let var = terminal_var(&m.pattern)
103 .ok_or("MATCH … SET/REMOVE/DELETE needs the last node to have a variable")?
104 .to_string();
105 for op in &ast.ops {
106 match op {
107 WriteOp::Create(_) => {}
111 WriteOp::Merge(m) => validate_merge_after_match(m, &var)?,
112 WriteOp::Set(items) => check_var(items.iter().map(set_item_var), &var)?,
113 WriteOp::Remove(items) => check_var(items.iter().map(remove_item_var), &var)?,
114 WriteOp::Delete { vars, .. } => {
115 check_var(vars.iter().map(String::as_str), &var)?
116 }
117 }
118 }
119 let query = Query {
121 source: QuerySource {
122 kind: SourceKind::Match,
123 first: m.pattern.first,
124 rest: m.pattern.rest,
125 },
126 beams: Vec::new(),
127 where_clause: m.where_clause,
128 ret: Return {
134 distinct: true,
135 items: vec![ReturnItem::Star],
136 },
137 order_by: Vec::new(),
138 skip: None,
139 limit: None,
140 as_of: None,
142 };
143 let plan = crate::compile::compile(query, None, ¶ms)?;
144 Some((var, plan))
145 }
146 };
147
148 Ok(WriteStatement {
149 binding,
150 ops: ast.ops,
151 has_label_ops,
152 params,
153 })
154}
155
156fn resolve_keys(ops: &mut [WriteOp], params: &crate::Params) -> Result<(), String> {
160 fn resolve_node(cn: &mut CreateNode, params: &crate::Params) -> Result<(), String> {
161 if let Some(Val::Param(name)) = &cn.key {
162 match crate::resolve_param(params, name)? {
163 s @ PropValue::Str(_) => cn.key = Some(Val::Lit(s)),
164 other => {
165 return Err(format!(
166 "`key:` must be a string to serve as the external key; `${name}` is {other:?}"
167 ));
168 }
169 }
170 }
171 Ok(())
172 }
173 for op in ops {
174 let paths: Vec<&mut CreatePath> = match op {
175 WriteOp::Create(paths) => paths.iter_mut().collect(),
176 WriteOp::Merge(m) => vec![&mut m.path],
177 _ => continue,
178 };
179 for path in paths {
180 resolve_node(&mut path.first, params)?;
181 for (_, node) in &mut path.rest {
182 resolve_node(node, params)?;
183 }
184 }
185 }
186 Ok(())
187}
188
189fn literal_key(cn: &CreateNode) -> Option<&str> {
191 match &cn.key {
192 Some(Val::Lit(PropValue::Str(s))) => Some(s),
193 _ => None,
194 }
195}
196
197fn validate_merge(m: &MergeClause) -> Result<(), String> {
202 let nodes = std::iter::once(&m.path.first).chain(m.path.rest.iter().map(|(_, n)| n));
203 let mut bound: AHashSet<&str> = AHashSet::new();
204 for n in nodes {
205 let rebound = n.var.as_deref().is_some_and(|v| !bound.insert(v));
206 if n.key.is_none() && !rebound {
207 return Err(
208 "MERGE needs a `key:` on every node to upsert on, e.g. MERGE (n:Label {key:\"…\"})"
209 .to_string(),
210 );
211 }
212 }
213 if !m.path.rest.is_empty() && (!m.on_create.is_empty() || !m.on_match.is_empty()) {
214 return Err(
215 "ON CREATE / ON MATCH SET is only supported for a single-node MERGE".to_string(),
216 );
217 }
218 let var = m.path.first.var.as_deref();
219 for it in m.on_create.iter().chain(&m.on_match) {
220 match var {
221 Some(v) if set_item_var(it) == v => {}
222 _ => {
223 return Err(
224 "ON CREATE / ON MATCH SET must reference the MERGE variable".to_string()
225 );
226 }
227 }
228 }
229 Ok(())
230}
231
232fn validate_merge_after_match(m: &MergeClause, terminal: &str) -> Result<(), String> {
236 if m.path.first.var.as_deref() != Some(terminal) {
237 return Err(format!(
238 "MERGE after MATCH must start from the matched variable `{terminal}`, \
239 e.g. MATCH ({terminal}) MERGE ({terminal})-[:R]->(b {{key:\"…\"}})"
240 ));
241 }
242 if m.path.rest.is_empty() {
243 return Err(
244 "MERGE after MATCH must extend the matched node with a relationship".to_string(),
245 );
246 }
247 for (_, n) in &m.path.rest {
248 if n.key.is_none() {
249 return Err("MERGE needs a `key:` on every non-anchor node".to_string());
250 }
251 }
252 if !m.on_create.is_empty() || !m.on_match.is_empty() {
253 return Err("ON CREATE / ON MATCH SET isn't supported for a MERGE after MATCH".to_string());
254 }
255 Ok(())
256}
257
258fn check_var<'a>(vars: impl Iterator<Item = &'a str>, bound: &str) -> Result<(), String> {
259 for v in vars {
260 if v != bound {
261 return Err(format!(
262 "`{v}` is not the matched variable `{bound}`; a mutation may reference \
263 only the pattern's terminal variable"
264 ));
265 }
266 }
267 Ok(())
268}
269
270fn resolve_val(v: &Val, params: &crate::Params) -> Result<PropValue, String> {
271 match v {
272 Val::Lit(p) => Ok(p.clone()),
273 Val::Param(name) => crate::resolve_param(params, name),
274 }
275}
276
277fn props_of(entries: &[(String, Val)], params: &crate::Params) -> Result<Properties, String> {
278 entries
279 .iter()
280 .map(|(k, v)| Ok((k.clone(), PropDesc::new(resolve_val(v, params)?))))
281 .collect()
282}
283
284fn execute(plane: &PlaneHandle<'_>, stmt: &WriteStatement) -> Result<WriteSummary, String> {
285 let mut staged = Staged::new(&stmt.params);
286
287 let ids: Vec<NodeId> = match &stmt.binding {
289 Some((_, plan)) => plane
290 .query_from_plan(plan.clone())
291 .ids()
292 .map_err(|e| e.to_string())?,
293 None => Vec::new(),
294 };
295
296 if stmt.has_label_ops {
299 for id in &ids {
300 if let Some(n) = plane.node(*id).map_err(|e| e.to_string())? {
301 staged.labels.insert(id.0, n.labels);
302 }
303 }
304 }
305
306 let rows = Rows { stmt, ids: &ids };
307 let mut txn = plane.write().map_err(|e| e.to_string())?;
308 for op in &stmt.ops {
309 match op {
310 WriteOp::Create(paths) => {
311 run_create(&mut txn, paths, rows, &mut staged)?;
312 }
313 WriteOp::Merge(m) => {
314 run_merge(plane, &mut txn, m, rows, &mut staged)?;
315 }
316 WriteOp::Set(items) => {
317 for id in &ids {
318 for it in items {
319 apply_set(&mut txn, *id, it, &mut staged)?;
320 }
321 }
322 }
323 WriteOp::Remove(items) => {
324 for id in &ids {
325 for it in items {
326 apply_remove(&mut txn, *id, it, &mut staged)?;
327 }
328 }
329 }
330 WriteOp::Delete { detach, .. } => {
331 run_delete(plane, &mut txn, rows, *detach, &mut staged)?;
332 }
333 }
334 }
335
336 txn.commit().map_err(|e| e.to_string())?;
337 Ok(staged.summary)
338}
339
340fn run_create<'a>(
346 txn: &mut WriteTxn<'_>,
347 paths: &'a [CreatePath],
348 rows: Rows<'a>,
349 staged: &mut Staged<'a>,
350) -> Result<(), String> {
351 match &rows.stmt.binding {
352 None => {
353 let mut vars: AHashMap<&'a str, NodeId> = AHashMap::new();
354 for path in paths {
355 create_path(txn, path, &mut vars, staged)?;
356 }
357 }
358 Some((bound, _)) => {
359 for id in rows.ids {
360 let mut vars: AHashMap<&'a str, NodeId> = AHashMap::new();
361 vars.insert(bound.as_str(), *id);
362 for path in paths {
363 create_path(txn, path, &mut vars, staged)?;
364 }
365 }
366 }
367 }
368 Ok(())
369}
370
371fn run_merge<'a>(
376 plane: &PlaneHandle<'_>,
377 txn: &mut WriteTxn<'_>,
378 m: &'a MergeClause,
379 rows: Rows<'a>,
380 staged: &mut Staged<'a>,
381) -> Result<(), String> {
382 match &rows.stmt.binding {
383 None => {
384 let mut vars: AHashMap<&'a str, NodeId> = AHashMap::new();
385 merge_path(plane, txn, m, &mut vars, staged)?;
386 }
387 Some((bound, _)) => {
388 for id in rows.ids {
389 let mut vars: AHashMap<&'a str, NodeId> = AHashMap::new();
390 vars.insert(bound.as_str(), *id);
391 merge_path(plane, txn, m, &mut vars, staged)?;
392 }
393 }
394 }
395 Ok(())
396}
397
398fn run_delete(
404 plane: &PlaneHandle<'_>,
405 txn: &mut WriteTxn<'_>,
406 rows: Rows<'_>,
407 detach: bool,
408 staged: &mut Staged<'_>,
409) -> Result<(), String> {
410 for id in rows.ids {
411 if !detach
412 && !plane
413 .neighbors(*id, Dir::Both, None)
414 .map_err(|e| e.to_string())?
415 .is_empty()
416 {
417 return Err(format!(
418 "cannot DELETE node {} — it still has relationships; use DETACH DELETE",
419 id.0
420 ));
421 }
422 txn.delete_node(*id).map_err(|e| e.to_string())?;
423 staged.summary.nodes_deleted += 1;
424 staged.labels.remove(&id.0);
425 }
426 Ok(())
427}
428
429fn apply_set(
430 txn: &mut WriteTxn<'_>,
431 id: NodeId,
432 it: &SetItem,
433 staged: &mut Staged<'_>,
434) -> Result<(), String> {
435 match it {
436 SetItem::Prop { key, value, .. } => {
437 txn.set_prop(id, key, PropDesc::new(resolve_val(value, staged.params)?))
438 .map_err(|e| e.to_string())?;
439 staged.summary.props_set += 1;
440 }
441 SetItem::Merge { props, .. } => {
442 for (k, v) in props {
443 txn.set_prop(id, k, PropDesc::new(resolve_val(v, staged.params)?))
444 .map_err(|e| e.to_string())?;
445 staged.summary.props_set += 1;
446 }
447 }
448 SetItem::Label { label, .. } => {
449 let set = staged.labels.entry(id.0).or_default();
450 if !set.iter().any(|l| l == label) {
451 set.push(label.clone());
452 let refs: Vec<&str> = set.iter().map(String::as_str).collect();
453 txn.set_labels(id, &refs).map_err(|e| e.to_string())?;
454 staged.summary.labels_set += 1;
455 }
456 }
457 }
458 Ok(())
459}
460
461fn apply_remove(
462 txn: &mut WriteTxn<'_>,
463 id: NodeId,
464 it: &RemoveItem,
465 staged: &mut Staged<'_>,
466) -> Result<(), String> {
467 match it {
468 RemoveItem::Prop { key, .. } => {
469 txn.remove_prop(id, key).map_err(|e| e.to_string())?;
470 staged.summary.props_set += 1;
471 }
472 RemoveItem::Label { label, .. } => {
473 let set = staged.labels.entry(id.0).or_default();
474 let before = set.len();
475 set.retain(|l| l != label);
476 if set.len() != before {
477 let refs: Vec<&str> = set.iter().map(String::as_str).collect();
478 txn.set_labels(id, &refs).map_err(|e| e.to_string())?;
479 staged.summary.labels_set += 1;
480 }
481 }
482 }
483 Ok(())
484}
485
486fn merge_path<'a>(
491 plane: &PlaneHandle<'_>,
492 txn: &mut WriteTxn<'_>,
493 m: &'a MergeClause,
494 vars: &mut AHashMap<&'a str, NodeId>,
495 staged: &mut Staged<'a>,
496) -> Result<(), String> {
497 let (first_id, created) = upsert_merge_node(plane, txn, &m.path.first, vars, staged)?;
498 let items = if created { &m.on_create } else { &m.on_match };
500 for it in items {
501 apply_set(txn, first_id, it, staged)?;
502 }
503 let mut prev = first_id;
504 for (rel, node) in &m.path.rest {
505 let (cur, _) = upsert_merge_node(plane, txn, node, vars, staged)?;
506 ensure_edge(plane, txn, (prev, cur), rel, staged)?;
507 prev = cur;
508 }
509 Ok(())
510}
511
512fn upsert_merge_node<'a>(
519 plane: &PlaneHandle<'_>,
520 txn: &mut WriteTxn<'_>,
521 cn: &'a CreateNode,
522 vars: &mut AHashMap<&'a str, NodeId>,
523 staged: &mut Staged<'a>,
524) -> Result<(NodeId, bool), String> {
525 if let Some(v) = &cn.var
526 && let Some(&id) = vars.get(v.as_str())
527 {
528 return Ok((id, false));
529 }
530 let key = literal_key(cn).ok_or("MERGE node needs a `key:` to upsert on")?;
531
532 if let Some(&id) = staged.keyed.get(key) {
533 if let Some(v) = &cn.var {
534 vars.insert(v.as_str(), id);
535 }
536 return Ok((id, false));
537 }
538
539 let (id, created) = match plane.node_by_key(key).map_err(|e| e.to_string())? {
540 Some(found) => {
541 let id = found.id;
542 staged.labels.entry(id.0).or_insert(found.labels);
543 (id, false)
544 }
545 None => {
546 let label_refs: Vec<&str> = cn.label.as_deref().into_iter().collect();
547 let id = txn
548 .create_node_with_key(key, &label_refs, props_of(&cn.props, staged.params)?)
549 .map_err(|e| e.to_string())?;
550 staged.summary.nodes_created += 1;
551 staged
552 .labels
553 .entry(id.0)
554 .or_insert_with(|| cn.label.clone().into_iter().collect());
555 (id, true)
556 }
557 };
558 staged.keyed.insert(key, id);
559 if let Some(v) = &cn.var {
560 vars.insert(v.as_str(), id);
561 }
562 Ok((id, created))
563}
564
565struct Staged<'a> {
571 params: &'a crate::Params,
573 keyed: AHashMap<&'a str, NodeId>,
576 edges: AHashSet<(NodeId, NodeId, String)>,
579 labels: AHashMap<u64, Vec<String>>,
582 summary: WriteSummary,
584}
585
586impl<'a> Staged<'a> {
587 fn new(params: &'a crate::Params) -> Self {
589 Self {
590 params,
591 keyed: AHashMap::new(),
592 edges: AHashSet::new(),
593 labels: AHashMap::new(),
594 summary: WriteSummary::default(),
595 }
596 }
597}
598
599#[derive(Clone, Copy)]
605struct Rows<'a> {
606 stmt: &'a WriteStatement,
607 ids: &'a [NodeId],
608}
609
610fn ensure_edge(
614 plane: &PlaneHandle<'_>,
615 txn: &mut WriteTxn<'_>,
616 (prev, cur): (NodeId, NodeId),
617 rel: &CreateRel,
618 staged: &mut Staged<'_>,
619) -> Result<(), String> {
620 let (src, dst) = match rel.dir {
622 Dir::In => (cur, prev),
623 _ => (prev, cur),
624 };
625 if staged.edges.contains(&(src, dst, rel.ty.clone())) {
626 return Ok(());
627 }
628 let existing = plane
629 .neighbors(src, Dir::Out, Some(&rel.ty))
630 .map_err(|e| e.to_string())?;
631 if existing.iter().any(|n| n.node == dst) {
632 return Ok(());
633 }
634 txn.create_edge(src, dst, &rel.ty, props_of(&rel.props, staged.params)?)
635 .map_err(|e| e.to_string())?;
636 staged.edges.insert((src, dst, rel.ty.clone()));
637 staged.summary.edges_created += 1;
638 Ok(())
639}
640
641fn get_or_create<'a>(
644 txn: &mut WriteTxn<'_>,
645 cn: &'a CreateNode,
646 vars: &mut AHashMap<&'a str, NodeId>,
647 staged: &mut Staged<'a>,
648) -> Result<NodeId, String> {
649 if let Some(v) = &cn.var
650 && let Some(&id) = vars.get(v.as_str())
651 {
652 return Ok(id); }
654 let labels: Vec<&str> = cn.label.as_deref().into_iter().collect();
655 let props = props_of(&cn.props, staged.params)?;
656 let id = match literal_key(cn) {
657 Some(k) => {
658 let id = txn
659 .create_node_with_key(k, &labels, props)
660 .map_err(|e| e.to_string())?;
661 staged.keyed.insert(k, id);
664 id
665 }
666 None => txn.create_node(&labels, props).map_err(|e| e.to_string())?,
667 };
668 staged.summary.nodes_created += 1;
669 if let Some(v) = &cn.var {
670 vars.insert(v.as_str(), id);
671 }
672 Ok(id)
673}
674
675fn create_path<'a>(
676 txn: &mut WriteTxn<'_>,
677 path: &'a CreatePath,
678 vars: &mut AHashMap<&'a str, NodeId>,
679 staged: &mut Staged<'a>,
680) -> Result<(), String> {
681 let mut prev = get_or_create(txn, &path.first, vars, staged)?;
682 for (rel, node) in &path.rest {
683 let cur = get_or_create(txn, node, vars, staged)?;
684 let (src, dst) = match rel.dir {
686 Dir::In => (cur, prev),
687 _ => (prev, cur),
688 };
689 txn.create_edge(src, dst, &rel.ty, props_of(&rel.props, staged.params)?)
690 .map_err(|e| e.to_string())?;
691 staged.edges.insert((src, dst, rel.ty.clone()));
694 staged.summary.edges_created += 1;
695 prev = cur;
696 }
697 Ok(())
698}