1#[cfg(any(feature = "apoc-create", feature = "apoc-refactor"))]
2use crate::error::Error;
3use crate::error::Result;
4use crate::reader::GraphReader;
5use crate::value::Value;
6#[cfg(any(feature = "apoc-create", feature = "apoc-refactor"))]
7use crate::writer::GraphWriter;
8#[cfg(any(feature = "apoc-create", feature = "apoc-refactor"))]
9use meshdb_core::Edge;
10#[cfg(feature = "apoc-create")]
11use meshdb_core::Node;
12use meshdb_core::Property;
13use std::collections::{BTreeSet, HashMap};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ProcType {
22 String,
23 Integer,
24 Float,
25 Number,
26 Boolean,
27 Any,
28}
29
30impl ProcType {
31 pub fn parse(s: &str) -> Self {
32 let trimmed = s.trim().trim_end_matches('?').trim();
33 match trimmed.to_ascii_uppercase().as_str() {
34 "STRING" => ProcType::String,
35 "INTEGER" | "INT" => ProcType::Integer,
36 "FLOAT" => ProcType::Float,
37 "NUMBER" | "NUMERIC" => ProcType::Number,
38 "BOOLEAN" | "BOOL" => ProcType::Boolean,
39 _ => ProcType::Any,
40 }
41 }
42
43 pub fn accepts(&self, value: &Value) -> bool {
48 if matches!(value, Value::Null) {
49 return true;
50 }
51 match (self, value) {
52 (ProcType::Any, _) => true,
53 (ProcType::String, Value::Property(Property::String(_))) => true,
54 (ProcType::Integer, Value::Property(Property::Int64(_))) => true,
55 (ProcType::Float, Value::Property(Property::Float64(_))) => true,
56 (ProcType::Float, Value::Property(Property::Int64(_))) => true,
57 (ProcType::Number, Value::Property(Property::Int64(_))) => true,
58 (ProcType::Number, Value::Property(Property::Float64(_))) => true,
59 (ProcType::Boolean, Value::Property(Property::Bool(_))) => true,
60 _ => false,
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
66pub struct ProcArgSpec {
67 pub name: String,
68 pub ty: ProcType,
69}
70
71#[derive(Debug, Clone)]
72pub struct ProcOutSpec {
73 pub name: String,
74 pub ty: ProcType,
75}
76
77#[derive(Debug, Clone)]
89pub struct Procedure {
90 pub qualified_name: Vec<String>,
91 pub inputs: Vec<ProcArgSpec>,
92 pub outputs: Vec<ProcOutSpec>,
93 pub rows: Vec<ProcRow>,
94 pub builtin: Option<BuiltinProc>,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum BuiltinProc {
103 DbLabels,
105 DbRelationshipTypes,
107 DbPropertyKeys,
110 DbConstraints,
114 #[cfg(feature = "apoc-create")]
121 ApocCreateNode,
122 #[cfg(feature = "apoc-create")]
129 ApocCreateRelationship,
130 #[cfg(feature = "apoc-create")]
136 ApocCreateAddLabels,
137 #[cfg(feature = "apoc-create")]
141 ApocCreateRemoveLabels,
142 #[cfg(feature = "apoc-create")]
146 ApocCreateSetLabels,
147 #[cfg(feature = "apoc-create")]
153 ApocCreateSetProperty,
154 #[cfg(feature = "apoc-create")]
158 ApocCreateSetRelProperty,
159 #[cfg(feature = "apoc-refactor")]
165 ApocRefactorSetType,
166 #[cfg(feature = "apoc-meta")]
175 ApocMetaSchema,
176}
177
178pub type ProcRow = HashMap<String, Value>;
183
184impl Procedure {
185 pub fn row_matches(&self, row: &ProcRow, args: &[Value]) -> bool {
192 for (spec, arg) in self.inputs.iter().zip(args.iter()) {
193 let cell = row.get(&spec.name).unwrap_or(&Value::Null);
194 if !values_equal_for_procedure(cell, arg) {
195 return false;
196 }
197 }
198 true
199 }
200
201 pub fn is_write_builtin(&self) -> bool {
207 match self.builtin {
208 #[cfg(feature = "apoc-create")]
209 Some(
210 BuiltinProc::ApocCreateNode
211 | BuiltinProc::ApocCreateRelationship
212 | BuiltinProc::ApocCreateAddLabels
213 | BuiltinProc::ApocCreateRemoveLabels
214 | BuiltinProc::ApocCreateSetLabels
215 | BuiltinProc::ApocCreateSetProperty
216 | BuiltinProc::ApocCreateSetRelProperty,
217 ) => true,
218 #[cfg(feature = "apoc-refactor")]
219 Some(BuiltinProc::ApocRefactorSetType) => true,
220 _ => false,
221 }
222 }
223
224 pub fn resolve_rows(&self, reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
228 match self.builtin {
229 None => Ok(self.rows.clone()),
230 Some(BuiltinProc::DbLabels) => builtin_db_labels(reader),
231 Some(BuiltinProc::DbRelationshipTypes) => builtin_db_relationship_types(reader),
232 Some(BuiltinProc::DbPropertyKeys) => builtin_db_property_keys(reader),
233 Some(BuiltinProc::DbConstraints) => builtin_db_constraints(reader),
234 #[cfg(feature = "apoc-meta")]
235 Some(BuiltinProc::ApocMetaSchema) => builtin_apoc_meta_schema(reader),
236 #[cfg(feature = "apoc-create")]
237 Some(BuiltinProc::ApocCreateNode) => Err(Error::Procedure(
238 "apoc.create.node is a write procedure — call via resolve_write_rows".into(),
239 )),
240 #[cfg(feature = "apoc-create")]
241 Some(BuiltinProc::ApocCreateRelationship) => Err(Error::Procedure(
242 "apoc.create.relationship is a write procedure — call via resolve_write_rows"
243 .into(),
244 )),
245 #[cfg(feature = "apoc-create")]
246 Some(
247 BuiltinProc::ApocCreateAddLabels
248 | BuiltinProc::ApocCreateRemoveLabels
249 | BuiltinProc::ApocCreateSetLabels
250 | BuiltinProc::ApocCreateSetProperty
251 | BuiltinProc::ApocCreateSetRelProperty,
252 ) => Err(Error::Procedure(
253 "apoc.create.* mutator is a write procedure — call via resolve_write_rows".into(),
254 )),
255 #[cfg(feature = "apoc-refactor")]
256 Some(BuiltinProc::ApocRefactorSetType) => Err(Error::Procedure(
257 "apoc.refactor.setType is a write procedure — call via resolve_write_rows".into(),
258 )),
259 }
260 }
261
262 #[cfg(any(feature = "apoc-create", feature = "apoc-refactor"))]
267 pub fn resolve_write_rows(
268 &self,
269 reader: &dyn GraphReader,
270 writer: &dyn GraphWriter,
271 args: &[Value],
272 ) -> Result<Vec<ProcRow>> {
273 match self.builtin {
274 #[cfg(feature = "apoc-create")]
275 Some(BuiltinProc::ApocCreateNode) => apoc_create_node(writer, args),
276 #[cfg(feature = "apoc-create")]
277 Some(BuiltinProc::ApocCreateRelationship) => apoc_create_relationship(writer, args),
278 #[cfg(feature = "apoc-create")]
279 Some(BuiltinProc::ApocCreateAddLabels) => {
280 apoc_label_mutator(reader, writer, args, LabelMode::Add)
281 }
282 #[cfg(feature = "apoc-create")]
283 Some(BuiltinProc::ApocCreateRemoveLabels) => {
284 apoc_label_mutator(reader, writer, args, LabelMode::Remove)
285 }
286 #[cfg(feature = "apoc-create")]
287 Some(BuiltinProc::ApocCreateSetLabels) => {
288 apoc_label_mutator(reader, writer, args, LabelMode::Set)
289 }
290 #[cfg(feature = "apoc-create")]
291 Some(BuiltinProc::ApocCreateSetProperty) => {
292 apoc_set_node_property(reader, writer, args)
293 }
294 #[cfg(feature = "apoc-create")]
295 Some(BuiltinProc::ApocCreateSetRelProperty) => {
296 apoc_set_rel_property(reader, writer, args)
297 }
298 #[cfg(feature = "apoc-refactor")]
299 Some(BuiltinProc::ApocRefactorSetType) => apoc_refactor_set_type(reader, writer, args),
300 _ => Err(Error::Procedure("procedure is not a write builtin".into())),
301 }
302 }
303}
304
305fn str_row(column: &str, value: String) -> ProcRow {
306 let mut row = HashMap::new();
307 row.insert(column.to_string(), Value::Property(Property::String(value)));
308 row
309}
310
311fn builtin_db_labels(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
312 let mut labels: BTreeSet<String> = BTreeSet::new();
313 for id in reader.all_node_ids()? {
314 if let Some(n) = reader.get_node(id)? {
315 for l in n.labels {
316 labels.insert(l);
317 }
318 }
319 }
320 Ok(labels.into_iter().map(|l| str_row("label", l)).collect())
321}
322
323fn builtin_db_relationship_types(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
324 let mut types: BTreeSet<String> = BTreeSet::new();
325 for id in reader.all_node_ids()? {
326 for (edge_id, _) in reader.outgoing(id)? {
327 if let Some(e) = reader.get_edge(edge_id)? {
328 types.insert(e.edge_type);
329 }
330 }
331 }
332 Ok(types
333 .into_iter()
334 .map(|t| str_row("relationshipType", t))
335 .collect())
336}
337
338fn builtin_db_constraints(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
339 let specs = reader.list_property_constraints()?;
340 Ok(specs
341 .into_iter()
342 .map(|spec| {
343 let mut row: ProcRow = HashMap::new();
344 row.insert("name".into(), Value::Property(Property::String(spec.name)));
345 let (scope_tag, target) = match spec.scope {
346 meshdb_storage::ConstraintScope::Node(l) => ("NODE", l),
347 meshdb_storage::ConstraintScope::Relationship(t) => ("RELATIONSHIP", t),
348 };
349 row.insert(
350 "scope".into(),
351 Value::Property(Property::String(scope_tag.into())),
352 );
353 row.insert("label".into(), Value::Property(Property::String(target)));
354 let props: Vec<Property> = spec.properties.into_iter().map(Property::String).collect();
355 row.insert("properties".into(), Value::Property(Property::List(props)));
356 row.insert(
357 "type".into(),
358 Value::Property(Property::String(spec.kind.as_string())),
359 );
360 row
361 })
362 .collect())
363}
364
365fn builtin_db_property_keys(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
366 let mut keys: BTreeSet<String> = BTreeSet::new();
367 for id in reader.all_node_ids()? {
368 if let Some(n) = reader.get_node(id)? {
369 for k in n.properties.keys() {
370 keys.insert(k.clone());
371 }
372 for (edge_id, _) in reader.outgoing(id)? {
373 if let Some(e) = reader.get_edge(edge_id)? {
374 for k in e.properties.keys() {
375 keys.insert(k.clone());
376 }
377 }
378 }
379 }
380 }
381 Ok(keys
382 .into_iter()
383 .map(|k| str_row("propertyKey", k))
384 .collect())
385}
386
387#[cfg(feature = "apoc-meta")]
417fn builtin_apoc_meta_schema(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
418 use std::collections::HashSet;
419 let mut label_count: HashMap<String, i64> = HashMap::new();
421 let mut label_props: HashMap<String, HashMap<String, HashSet<&'static str>>> = HashMap::new();
422 let mut edge_count: HashMap<String, i64> = HashMap::new();
423 let mut edge_props: HashMap<String, HashMap<String, HashSet<&'static str>>> = HashMap::new();
424
425 for id in reader.all_node_ids()? {
426 let node = match reader.get_node(id)? {
427 Some(n) => n,
428 None => continue,
429 };
430 for label in &node.labels {
431 *label_count.entry(label.clone()).or_insert(0) += 1;
432 let per_label = label_props.entry(label.clone()).or_default();
433 for (k, v) in &node.properties {
434 per_label
435 .entry(k.clone())
436 .or_default()
437 .insert(apoc_schema_type(v));
438 }
439 }
440 for (edge_id, _) in reader.outgoing(id)? {
441 let edge = match reader.get_edge(edge_id)? {
442 Some(e) => e,
443 None => continue,
444 };
445 *edge_count.entry(edge.edge_type.clone()).or_insert(0) += 1;
446 let per_type = edge_props.entry(edge.edge_type.clone()).or_default();
447 for (k, v) in &edge.properties {
448 per_type
449 .entry(k.clone())
450 .or_default()
451 .insert(apoc_schema_type(v));
452 }
453 }
454 }
455
456 let mut node_indexed: HashMap<String, HashSet<String>> = HashMap::new();
462 for (label, props) in reader.list_property_indexes()? {
463 let entry = node_indexed.entry(label).or_default();
464 for p in props {
465 entry.insert(p);
466 }
467 }
468 let mut edge_indexed: HashMap<String, HashSet<String>> = HashMap::new();
469 for (edge_type, props) in reader.list_edge_property_indexes()? {
470 let entry = edge_indexed.entry(edge_type).or_default();
471 for p in props {
472 entry.insert(p);
473 }
474 }
475
476 let mut schema: HashMap<String, Property> = HashMap::new();
477 for (label, count) in label_count {
478 let props = label_props.remove(&label).unwrap_or_default();
479 let indexed = node_indexed.remove(&label).unwrap_or_default();
480 schema.insert(
481 label,
482 Property::Map(schema_entry(count, "node", props, Some(&indexed))),
483 );
484 }
485 for (edge_type, count) in edge_count {
486 let props = edge_props.remove(&edge_type).unwrap_or_default();
487 let indexed = edge_indexed.remove(&edge_type).unwrap_or_default();
488 schema.insert(
489 edge_type,
490 Property::Map(schema_entry(count, "relationship", props, Some(&indexed))),
491 );
492 }
493
494 let mut row = HashMap::new();
495 row.insert("value".to_string(), Value::Property(Property::Map(schema)));
496 Ok(vec![row])
497}
498
499#[cfg(feature = "apoc-meta")]
505fn schema_entry(
506 count: i64,
507 type_tag: &str,
508 props: HashMap<String, std::collections::HashSet<&'static str>>,
509 indexed: Option<&std::collections::HashSet<String>>,
510) -> HashMap<String, Property> {
511 let mut out: HashMap<String, Property> = HashMap::new();
512 out.insert("type".into(), Property::String(type_tag.into()));
513 out.insert("count".into(), Property::Int64(count));
514 let mut properties: HashMap<String, Property> = HashMap::new();
515 for (k, types) in props {
516 let mut sorted: Vec<&'static str> = types.into_iter().collect();
517 sorted.sort_unstable();
518 let ty = sorted.join("|");
519 let mut info: HashMap<String, Property> = HashMap::new();
520 info.insert("type".into(), Property::String(ty));
521 if let Some(idx) = indexed {
522 info.insert("indexed".into(), Property::Bool(idx.contains(&k)));
523 }
524 properties.insert(k, Property::Map(info));
525 }
526 out.insert("properties".into(), Property::Map(properties));
527 out
528}
529
530#[cfg(feature = "apoc-meta")]
536fn apoc_schema_type(p: &Property) -> &'static str {
537 match p {
538 Property::Null => "NULL",
539 Property::String(_) => "STRING",
540 Property::Int64(_) => "INTEGER",
541 Property::Float64(_) => "FLOAT",
542 Property::Bool(_) => "BOOLEAN",
543 Property::List(_) => "LIST",
544 Property::Map(_) => "MAP",
545 Property::DateTime { .. } => "DATE_TIME",
546 Property::LocalDateTime(_) => "LOCAL_DATE_TIME",
547 Property::Date(_) => "DATE",
548 Property::Time { .. } => "TIME",
549 Property::Duration(_) => "DURATION",
550 Property::Point(_) => "POINT",
551 }
552}
553
554#[cfg(feature = "apoc-create")]
562fn apoc_create_node(writer: &dyn GraphWriter, args: &[Value]) -> Result<Vec<ProcRow>> {
563 let labels = match &args[0] {
564 Value::Null | Value::Property(Property::Null) => Vec::new(),
565 Value::List(items) => items
566 .iter()
567 .map(|v| match v {
568 Value::Property(Property::String(s)) => Ok(s.clone()),
569 other => Err(Error::Procedure(format!(
570 "apoc.create.node: labels must be strings, got {other:?}"
571 ))),
572 })
573 .collect::<Result<Vec<_>>>()?,
574 Value::Property(Property::List(items)) => items
575 .iter()
576 .map(|p| match p {
577 Property::String(s) => Ok(s.clone()),
578 other => Err(Error::Procedure(format!(
579 "apoc.create.node: labels must be strings, got {other:?}"
580 ))),
581 })
582 .collect::<Result<Vec<_>>>()?,
583 other => {
584 return Err(Error::Procedure(format!(
585 "apoc.create.node: first argument must be a list of strings, got {other:?}"
586 )));
587 }
588 };
589 let props: HashMap<String, Property> = match &args[1] {
590 Value::Null | Value::Property(Property::Null) => HashMap::new(),
591 Value::Map(pairs) => pairs
592 .iter()
593 .map(|(k, v)| Ok((k.clone(), value_to_storable_property(v)?)))
594 .collect::<Result<HashMap<_, _>>>()?,
595 Value::Property(Property::Map(entries)) => entries
596 .iter()
597 .map(|(k, p)| (k.clone(), p.clone()))
598 .collect(),
599 other => {
600 return Err(Error::Procedure(format!(
601 "apoc.create.node: second argument must be a map, got {other:?}"
602 )));
603 }
604 };
605 let mut node = Node::new();
606 node.labels = labels;
607 for (k, p) in props {
610 if !matches!(p, Property::Null) {
611 node.properties.insert(k, p);
612 }
613 }
614 writer.put_node(&node)?;
615 let mut row = HashMap::new();
616 row.insert("node".to_string(), Value::Node(node));
617 Ok(vec![row])
618}
619
620#[cfg(feature = "apoc-create")]
627fn apoc_create_relationship(writer: &dyn GraphWriter, args: &[Value]) -> Result<Vec<ProcRow>> {
628 let from = expect_node_id(&args[0], "first argument (from)")?;
629 let rel_type = match &args[1] {
630 Value::Property(Property::String(s)) => s.clone(),
631 Value::Null | Value::Property(Property::Null) => {
632 return Err(Error::Procedure(
633 "apoc.create.relationship: relationship type must not be null".into(),
634 ));
635 }
636 other => {
637 return Err(Error::Procedure(format!(
638 "apoc.create.relationship: relationship type must be a string, got {other:?}"
639 )));
640 }
641 };
642 let props: HashMap<String, Property> = match &args[2] {
643 Value::Null | Value::Property(Property::Null) => HashMap::new(),
644 Value::Map(pairs) => pairs
645 .iter()
646 .map(|(k, v)| Ok((k.clone(), value_to_storable_property(v)?)))
647 .collect::<Result<HashMap<_, _>>>()?,
648 Value::Property(Property::Map(entries)) => entries
649 .iter()
650 .map(|(k, p)| (k.clone(), p.clone()))
651 .collect(),
652 other => {
653 return Err(Error::Procedure(format!(
654 "apoc.create.relationship: third argument must be a map, got {other:?}"
655 )));
656 }
657 };
658 let to = expect_node_id(&args[3], "fourth argument (to)")?;
659 let mut edge = Edge::new(rel_type, from, to);
660 for (k, p) in props {
661 if !matches!(p, Property::Null) {
662 edge.properties.insert(k, p);
663 }
664 }
665 writer.put_edge(&edge)?;
666 let mut row = HashMap::new();
667 row.insert("rel".to_string(), Value::Edge(edge));
668 Ok(vec![row])
669}
670
671#[cfg(feature = "apoc-refactor")]
678fn apoc_refactor_set_type(
679 reader: &dyn GraphReader,
680 writer: &dyn GraphWriter,
681 args: &[Value],
682) -> Result<Vec<ProcRow>> {
683 let old_id = match &args[0] {
684 Value::Edge(e) => e.id,
685 Value::Null | Value::Property(Property::Null) => {
686 return Err(Error::Procedure(
687 "apoc.refactor.setType: relationship argument must not be null".into(),
688 ));
689 }
690 other => {
691 return Err(Error::Procedure(format!(
692 "apoc.refactor.setType: first argument must be a relationship, got {other:?}"
693 )));
694 }
695 };
696 let new_type = match &args[1] {
697 Value::Property(Property::String(s)) => s.clone(),
698 Value::Null | Value::Property(Property::Null) => {
699 return Err(Error::Procedure(
700 "apoc.refactor.setType: new type must not be null".into(),
701 ));
702 }
703 other => {
704 return Err(Error::Procedure(format!(
705 "apoc.refactor.setType: new type must be a string, got {other:?}"
706 )));
707 }
708 };
709 let old = reader
710 .get_edge(old_id)?
711 .ok_or_else(|| Error::Procedure(format!("edge {old_id:?} no longer exists")))?;
712 if old.edge_type == new_type {
715 let mut row = HashMap::new();
716 row.insert("rel".to_string(), Value::Edge(old));
717 return Ok(vec![row]);
718 }
719 let mut new_edge = Edge::new(new_type, old.source, old.target);
720 new_edge.properties = old.properties.clone();
721 writer.delete_edge(old_id)?;
722 writer.put_edge(&new_edge)?;
723 let mut row = HashMap::new();
724 row.insert("rel".to_string(), Value::Edge(new_edge));
725 Ok(vec![row])
726}
727
728#[cfg(feature = "apoc-create")]
733#[derive(Debug, Clone, Copy)]
734enum LabelMode {
735 Add,
736 Remove,
737 Set,
738}
739
740#[cfg(feature = "apoc-create")]
749fn apoc_label_mutator(
750 reader: &dyn GraphReader,
751 writer: &dyn GraphWriter,
752 args: &[Value],
753 mode: LabelMode,
754) -> Result<Vec<ProcRow>> {
755 let node_ids = expect_node_or_node_list(&args[0], "first argument")?;
756 let labels = expect_string_list(&args[1], "second argument (labels)")?;
757 let mut out: Vec<ProcRow> = Vec::with_capacity(node_ids.len());
758 for nid in node_ids {
759 let mut node = reader
760 .get_node(nid)?
761 .ok_or_else(|| Error::Procedure(format!("node {nid:?} no longer exists")))?;
762 match mode {
763 LabelMode::Add => {
764 for l in &labels {
765 if !node.labels.iter().any(|existing| existing == l) {
766 node.labels.push(l.clone());
767 }
768 }
769 }
770 LabelMode::Remove => {
771 node.labels
772 .retain(|existing| !labels.iter().any(|l| l == existing));
773 }
774 LabelMode::Set => {
775 node.labels = labels.clone();
776 }
777 }
778 writer.put_node(&node)?;
779 let mut row = HashMap::new();
780 row.insert("node".to_string(), Value::Node(node));
781 out.push(row);
782 }
783 Ok(out)
784}
785
786#[cfg(feature = "apoc-create")]
793fn apoc_set_node_property(
794 reader: &dyn GraphReader,
795 writer: &dyn GraphWriter,
796 args: &[Value],
797) -> Result<Vec<ProcRow>> {
798 let node_ids = expect_node_or_node_list(&args[0], "first argument")?;
799 let key = expect_string(&args[1], "second argument (key)")?;
800 let value = value_to_storable_property(&args[2])?;
801 let mut out: Vec<ProcRow> = Vec::with_capacity(node_ids.len());
802 for nid in node_ids {
803 let mut node = reader
804 .get_node(nid)?
805 .ok_or_else(|| Error::Procedure(format!("node {nid:?} no longer exists")))?;
806 if matches!(value, Property::Null) {
807 node.properties.remove(&key);
808 } else {
809 node.properties.insert(key.clone(), value.clone());
810 }
811 writer.put_node(&node)?;
812 let mut row = HashMap::new();
813 row.insert("node".to_string(), Value::Node(node));
814 out.push(row);
815 }
816 Ok(out)
817}
818
819#[cfg(feature = "apoc-create")]
821fn apoc_set_rel_property(
822 reader: &dyn GraphReader,
823 writer: &dyn GraphWriter,
824 args: &[Value],
825) -> Result<Vec<ProcRow>> {
826 let edge_ids = expect_edge_or_edge_list(&args[0], "first argument")?;
827 let key = expect_string(&args[1], "second argument (key)")?;
828 let value = value_to_storable_property(&args[2])?;
829 let mut out: Vec<ProcRow> = Vec::with_capacity(edge_ids.len());
830 for eid in edge_ids {
831 let mut edge = reader
832 .get_edge(eid)?
833 .ok_or_else(|| Error::Procedure(format!("edge {eid:?} no longer exists")))?;
834 if matches!(value, Property::Null) {
835 edge.properties.remove(&key);
836 } else {
837 edge.properties.insert(key.clone(), value.clone());
838 }
839 writer.put_edge(&edge)?;
840 let mut row = HashMap::new();
841 row.insert("rel".to_string(), Value::Edge(edge));
842 out.push(row);
843 }
844 Ok(out)
845}
846
847#[cfg(feature = "apoc-create")]
852fn expect_node_or_node_list(v: &Value, position: &str) -> Result<Vec<meshdb_core::NodeId>> {
853 let mut ids: Vec<meshdb_core::NodeId> = Vec::new();
854 collect_node_ids(v, position, &mut ids)?;
855 if ids.is_empty() {
856 return Err(Error::Procedure(format!(
857 "apoc.create.*: {position} resolved to zero nodes"
858 )));
859 }
860 Ok(ids)
861}
862
863#[cfg(feature = "apoc-create")]
864fn collect_node_ids(v: &Value, position: &str, out: &mut Vec<meshdb_core::NodeId>) -> Result<()> {
865 match v {
866 Value::Node(n) => {
867 out.push(n.id);
868 Ok(())
869 }
870 Value::List(items) => {
871 for item in items {
872 collect_node_ids(item, position, out)?;
873 }
874 Ok(())
875 }
876 Value::Null | Value::Property(Property::Null) => Ok(()),
877 other => Err(Error::Procedure(format!(
878 "apoc.create.*: {position} must be a node or list of nodes, got {other:?}"
879 ))),
880 }
881}
882
883#[cfg(feature = "apoc-create")]
885fn expect_edge_or_edge_list(v: &Value, position: &str) -> Result<Vec<meshdb_core::EdgeId>> {
886 let mut ids: Vec<meshdb_core::EdgeId> = Vec::new();
887 collect_edge_ids(v, position, &mut ids)?;
888 if ids.is_empty() {
889 return Err(Error::Procedure(format!(
890 "apoc.create.*: {position} resolved to zero relationships"
891 )));
892 }
893 Ok(ids)
894}
895
896#[cfg(feature = "apoc-create")]
897fn collect_edge_ids(v: &Value, position: &str, out: &mut Vec<meshdb_core::EdgeId>) -> Result<()> {
898 match v {
899 Value::Edge(e) => {
900 out.push(e.id);
901 Ok(())
902 }
903 Value::List(items) => {
904 for item in items {
905 collect_edge_ids(item, position, out)?;
906 }
907 Ok(())
908 }
909 Value::Null | Value::Property(Property::Null) => Ok(()),
910 other => Err(Error::Procedure(format!(
911 "apoc.create.*: {position} must be a relationship or list of relationships, got {other:?}"
912 ))),
913 }
914}
915
916#[cfg(feature = "apoc-create")]
917fn expect_string_list(v: &Value, position: &str) -> Result<Vec<String>> {
918 match v {
919 Value::Null | Value::Property(Property::Null) => Ok(Vec::new()),
920 Value::List(items) => items
921 .iter()
922 .map(|item| match item {
923 Value::Property(Property::String(s)) => Ok(s.clone()),
924 other => Err(Error::Procedure(format!(
925 "apoc.create.*: {position} must contain strings, got {other:?}"
926 ))),
927 })
928 .collect(),
929 Value::Property(Property::List(items)) => items
930 .iter()
931 .map(|p| match p {
932 Property::String(s) => Ok(s.clone()),
933 other => Err(Error::Procedure(format!(
934 "apoc.create.*: {position} must contain strings, got {other:?}"
935 ))),
936 })
937 .collect(),
938 other => Err(Error::Procedure(format!(
939 "apoc.create.*: {position} must be a list of strings, got {other:?}"
940 ))),
941 }
942}
943
944#[cfg(feature = "apoc-create")]
945fn expect_string(v: &Value, position: &str) -> Result<String> {
946 match v {
947 Value::Property(Property::String(s)) => Ok(s.clone()),
948 other => Err(Error::Procedure(format!(
949 "apoc.create.*: {position} must be a string, got {other:?}"
950 ))),
951 }
952}
953
954#[cfg(feature = "apoc-create")]
959fn expect_node_id(v: &Value, position: &str) -> Result<meshdb_core::NodeId> {
960 match v {
961 Value::Node(n) => Ok(n.id),
962 Value::Null | Value::Property(Property::Null) => Err(Error::Procedure(format!(
963 "apoc.create.relationship: {position} must be a node, got null"
964 ))),
965 other => Err(Error::Procedure(format!(
966 "apoc.create.relationship: {position} must be a node, got {other:?}"
967 ))),
968 }
969}
970
971#[cfg(feature = "apoc-create")]
977fn value_to_storable_property(v: &Value) -> Result<Property> {
978 match v {
979 Value::Property(p) => Ok(p.clone()),
980 Value::Null => Ok(Property::Null),
981 Value::List(items) => {
982 let props = items
983 .iter()
984 .map(value_to_storable_property)
985 .collect::<Result<Vec<_>>>()?;
986 Ok(Property::List(props))
987 }
988 Value::Map(_) | Value::Node(_) | Value::Edge(_) | Value::Path { .. } => {
989 Err(Error::Procedure(
990 "apoc.create.node: property values can't be graph elements or graph-aware maps"
991 .into(),
992 ))
993 }
994 }
995}
996
997fn values_equal_for_procedure(a: &Value, b: &Value) -> bool {
998 match (a, b) {
999 (Value::Null, Value::Null) => true,
1000 (Value::Null, _) | (_, Value::Null) => false,
1001 (Value::Property(Property::Int64(x)), Value::Property(Property::Int64(y))) => x == y,
1002 (Value::Property(Property::Float64(x)), Value::Property(Property::Float64(y))) => x == y,
1003 (Value::Property(Property::Int64(i)), Value::Property(Property::Float64(f)))
1004 | (Value::Property(Property::Float64(f)), Value::Property(Property::Int64(i))) => {
1005 *f == (*i as f64)
1006 }
1007 (Value::Property(Property::String(x)), Value::Property(Property::String(y))) => x == y,
1008 (Value::Property(Property::Bool(x)), Value::Property(Property::Bool(y))) => x == y,
1009 _ => a == b,
1010 }
1011}
1012
1013#[derive(Debug, Clone, Default)]
1020pub struct ProcedureRegistry {
1021 procs: HashMap<String, Procedure>,
1022}
1023
1024impl ProcedureRegistry {
1025 pub fn new() -> Self {
1026 Self::default()
1027 }
1028
1029 pub fn register(&mut self, proc: Procedure) {
1030 let key = proc.qualified_name.join(".");
1031 self.procs.insert(key, proc);
1032 }
1033
1034 pub fn get(&self, qualified_name: &[String]) -> Option<&Procedure> {
1035 self.procs.get(&qualified_name.join("."))
1036 }
1037
1038 pub fn register_defaults(&mut self) {
1044 self.register(Procedure {
1045 qualified_name: vec!["db".into(), "labels".into()],
1046 inputs: Vec::new(),
1047 outputs: vec![ProcOutSpec {
1048 name: "label".into(),
1049 ty: ProcType::String,
1050 }],
1051 rows: Vec::new(),
1052 builtin: Some(BuiltinProc::DbLabels),
1053 });
1054 self.register(Procedure {
1055 qualified_name: vec!["db".into(), "relationshipTypes".into()],
1056 inputs: Vec::new(),
1057 outputs: vec![ProcOutSpec {
1058 name: "relationshipType".into(),
1059 ty: ProcType::String,
1060 }],
1061 rows: Vec::new(),
1062 builtin: Some(BuiltinProc::DbRelationshipTypes),
1063 });
1064 self.register(Procedure {
1065 qualified_name: vec!["db".into(), "propertyKeys".into()],
1066 inputs: Vec::new(),
1067 outputs: vec![ProcOutSpec {
1068 name: "propertyKey".into(),
1069 ty: ProcType::String,
1070 }],
1071 rows: Vec::new(),
1072 builtin: Some(BuiltinProc::DbPropertyKeys),
1073 });
1074 self.register(Procedure {
1075 qualified_name: vec!["db".into(), "constraints".into()],
1076 inputs: Vec::new(),
1077 outputs: vec![
1078 ProcOutSpec {
1079 name: "name".into(),
1080 ty: ProcType::String,
1081 },
1082 ProcOutSpec {
1083 name: "scope".into(),
1084 ty: ProcType::String,
1085 },
1086 ProcOutSpec {
1087 name: "label".into(),
1088 ty: ProcType::String,
1089 },
1090 ProcOutSpec {
1091 name: "properties".into(),
1092 ty: ProcType::Any,
1097 },
1098 ProcOutSpec {
1099 name: "type".into(),
1100 ty: ProcType::String,
1101 },
1102 ],
1103 rows: Vec::new(),
1104 builtin: Some(BuiltinProc::DbConstraints),
1105 });
1106 #[cfg(feature = "apoc-create")]
1107 self.register(Procedure {
1108 qualified_name: vec!["apoc".into(), "create".into(), "node".into()],
1109 inputs: vec![
1110 ProcArgSpec {
1111 name: "labels".into(),
1112 ty: ProcType::Any,
1116 },
1117 ProcArgSpec {
1118 name: "props".into(),
1119 ty: ProcType::Any,
1120 },
1121 ],
1122 outputs: vec![ProcOutSpec {
1123 name: "node".into(),
1124 ty: ProcType::Any,
1125 }],
1126 rows: Vec::new(),
1127 builtin: Some(BuiltinProc::ApocCreateNode),
1128 });
1129 #[cfg(feature = "apoc-create")]
1130 self.register(Procedure {
1131 qualified_name: vec!["apoc".into(), "create".into(), "relationship".into()],
1132 inputs: vec![
1133 ProcArgSpec {
1138 name: "from".into(),
1139 ty: ProcType::Any,
1140 },
1141 ProcArgSpec {
1142 name: "relType".into(),
1143 ty: ProcType::String,
1144 },
1145 ProcArgSpec {
1146 name: "props".into(),
1147 ty: ProcType::Any,
1148 },
1149 ProcArgSpec {
1150 name: "to".into(),
1151 ty: ProcType::Any,
1152 },
1153 ],
1154 outputs: vec![ProcOutSpec {
1155 name: "rel".into(),
1156 ty: ProcType::Any,
1157 }],
1158 rows: Vec::new(),
1159 builtin: Some(BuiltinProc::ApocCreateRelationship),
1160 });
1161 #[cfg(feature = "apoc-create")]
1162 for (name, builtin) in [
1163 ("addLabels", BuiltinProc::ApocCreateAddLabels),
1164 ("removeLabels", BuiltinProc::ApocCreateRemoveLabels),
1165 ("setLabels", BuiltinProc::ApocCreateSetLabels),
1166 ] {
1167 self.register(Procedure {
1168 qualified_name: vec!["apoc".into(), "create".into(), name.into()],
1169 inputs: vec![
1170 ProcArgSpec {
1173 name: "nodes".into(),
1174 ty: ProcType::Any,
1175 },
1176 ProcArgSpec {
1177 name: "labels".into(),
1178 ty: ProcType::Any,
1179 },
1180 ],
1181 outputs: vec![ProcOutSpec {
1182 name: "node".into(),
1183 ty: ProcType::Any,
1184 }],
1185 rows: Vec::new(),
1186 builtin: Some(builtin),
1187 });
1188 }
1189 #[cfg(feature = "apoc-create")]
1190 self.register(Procedure {
1191 qualified_name: vec!["apoc".into(), "create".into(), "setProperty".into()],
1192 inputs: vec![
1193 ProcArgSpec {
1194 name: "nodes".into(),
1195 ty: ProcType::Any,
1196 },
1197 ProcArgSpec {
1198 name: "key".into(),
1199 ty: ProcType::String,
1200 },
1201 ProcArgSpec {
1202 name: "value".into(),
1203 ty: ProcType::Any,
1204 },
1205 ],
1206 outputs: vec![ProcOutSpec {
1207 name: "node".into(),
1208 ty: ProcType::Any,
1209 }],
1210 rows: Vec::new(),
1211 builtin: Some(BuiltinProc::ApocCreateSetProperty),
1212 });
1213 #[cfg(feature = "apoc-create")]
1214 self.register(Procedure {
1215 qualified_name: vec!["apoc".into(), "create".into(), "setRelProperty".into()],
1216 inputs: vec![
1217 ProcArgSpec {
1218 name: "rels".into(),
1219 ty: ProcType::Any,
1220 },
1221 ProcArgSpec {
1222 name: "key".into(),
1223 ty: ProcType::String,
1224 },
1225 ProcArgSpec {
1226 name: "value".into(),
1227 ty: ProcType::Any,
1228 },
1229 ],
1230 outputs: vec![ProcOutSpec {
1231 name: "rel".into(),
1232 ty: ProcType::Any,
1233 }],
1234 rows: Vec::new(),
1235 builtin: Some(BuiltinProc::ApocCreateSetRelProperty),
1236 });
1237 #[cfg(feature = "apoc-meta")]
1238 self.register(Procedure {
1239 qualified_name: vec!["apoc".into(), "meta".into(), "schema".into()],
1240 inputs: Vec::new(),
1241 outputs: vec![ProcOutSpec {
1242 name: "value".into(),
1243 ty: ProcType::Any,
1244 }],
1245 rows: Vec::new(),
1246 builtin: Some(BuiltinProc::ApocMetaSchema),
1247 });
1248 #[cfg(feature = "apoc-refactor")]
1249 self.register(Procedure {
1250 qualified_name: vec!["apoc".into(), "refactor".into(), "setType".into()],
1251 inputs: vec![
1252 ProcArgSpec {
1253 name: "rel".into(),
1254 ty: ProcType::Any,
1255 },
1256 ProcArgSpec {
1257 name: "newType".into(),
1258 ty: ProcType::String,
1259 },
1260 ],
1261 outputs: vec![ProcOutSpec {
1262 name: "rel".into(),
1263 ty: ProcType::Any,
1264 }],
1265 rows: Vec::new(),
1266 builtin: Some(BuiltinProc::ApocRefactorSetType),
1267 });
1268 }
1269}