Skip to main content

meshdb_executor/
procedures.rs

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/// Declared argument / output type for a procedure signature.
16/// Mirrors the openCypher type names the TCK uses (`STRING?`,
17/// `INTEGER?`, `FLOAT?`, `NUMBER?`, `BOOLEAN?`, `ANY?`). Nullability
18/// is not tracked separately — every TCK type in practice is nullable
19/// (`?`) and the match logic treats nulls uniformly.
20#[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    /// True when `value` is acceptable as a procedure argument of
44    /// this declared type. Follows Neo4j's assignable-type rules:
45    /// `FLOAT` accepts integers (coerced), `NUMBER` accepts both
46    /// numeric kinds, `ANY` accepts everything.
47    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/// A procedure registered with a [`ProcedureRegistry`]. The TCK
78/// harness builds one per `And there exists a procedure ...` step by
79/// collating the signature and the gherkin data table: each data row
80/// contributes one entry to `rows` where the leading cells are the
81/// input-column values (matched against call arguments) and the
82/// trailing cells are the output-column values (projected by
83/// YIELD).
84///
85/// Built-in procedures — `db.labels()` and friends — leave `rows`
86/// empty and set `builtin` so the executor materialises the row set
87/// live from the current graph via [`Procedure::resolve_rows`].
88#[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/// Identifies a procedure whose rows are derived from the live graph
98/// at call time rather than pre-populated in [`Procedure::rows`].
99/// Keeps the procedure surface uniform — the executor still iterates
100/// `ProcRow`s; the only difference is who produced them.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum BuiltinProc {
103    /// `db.labels()` — yields one row per distinct node label.
104    DbLabels,
105    /// `db.relationshipTypes()` — yields one row per distinct edge type.
106    DbRelationshipTypes,
107    /// `db.propertyKeys()` — yields one row per distinct property key
108    /// observed on any node or edge.
109    DbPropertyKeys,
110    /// `db.constraints()` — yields one row per registered constraint,
111    /// carrying `name`, `label`, `property`, and `type` columns.
112    /// Mirrors the `SHOW CONSTRAINTS` surface.
113    DbConstraints,
114    /// `apoc.create.node(labels, props)` — write builtin that
115    /// creates a node with the given labels and properties and
116    /// yields it as `node`. The first builtin that mutates the
117    /// store, so it goes through [`Procedure::resolve_write_rows`]
118    /// rather than [`Procedure::resolve_rows`] — the args drive
119    /// the row directly, no candidate-row filtering needed.
120    #[cfg(feature = "apoc-create")]
121    ApocCreateNode,
122    /// `apoc.create.relationship(from, relType, props, to)` —
123    /// write builtin that creates an edge between two existing
124    /// nodes and yields it as `rel`. Argument order matches
125    /// Neo4j's APOC (relType + props between the two endpoints,
126    /// not at the end). Both endpoints must already exist; this
127    /// procedure does not auto-create missing nodes.
128    #[cfg(feature = "apoc-create")]
129    ApocCreateRelationship,
130    /// `apoc.create.addLabels(node|nodes, labels)` — adds the
131    /// given labels to the supplied nodes (no-op for labels
132    /// already present) and yields each updated node back. The
133    /// first arg accepts either a single Node or a list of Nodes
134    /// to match Neo4j APOC's variadic input convention.
135    #[cfg(feature = "apoc-create")]
136    ApocCreateAddLabels,
137    /// `apoc.create.removeLabels(node|nodes, labels)` — removes
138    /// the given labels from the supplied nodes (no-op for
139    /// labels not present) and yields each updated node back.
140    #[cfg(feature = "apoc-create")]
141    ApocCreateRemoveLabels,
142    /// `apoc.create.setLabels(node|nodes, labels)` — replaces
143    /// the entire label set on each supplied node with the given
144    /// list and yields the result back.
145    #[cfg(feature = "apoc-create")]
146    ApocCreateSetLabels,
147    /// `apoc.create.setProperty(node|nodes, key, value)` — sets
148    /// (or, if value is null, clears) a single property on each
149    /// supplied node and yields the result. Mirrors Neo4j APOC's
150    /// signature; for relationships use
151    /// [`Self::ApocCreateSetRelProperty`].
152    #[cfg(feature = "apoc-create")]
153    ApocCreateSetProperty,
154    /// `apoc.create.setRelProperty(rel|rels, key, value)` —
155    /// relationship-scope counterpart to
156    /// [`Self::ApocCreateSetProperty`].
157    #[cfg(feature = "apoc-create")]
158    ApocCreateSetRelProperty,
159    /// `apoc.refactor.setType(rel, newType)` — change a
160    /// relationship's type. Implementation deletes the old edge
161    /// and creates a new one with the supplied type, preserving
162    /// endpoints and properties; the new edge has a fresh
163    /// EdgeId. Yields the new edge as `rel`.
164    #[cfg(feature = "apoc-refactor")]
165    ApocRefactorSetType,
166    /// `apoc.meta.schema()` — read-only introspection that walks
167    /// the live graph and produces a single-row Map keyed by
168    /// label and relationship-type names. Each entry carries a
169    /// `count`, a `type` discriminator (`"node"` or
170    /// `"relationship"`), and a `properties` map describing each
171    /// observed property's APOC type and (for nodes) whether
172    /// it's covered by a property index. O(N) in the graph size,
173    /// same complexity class as Neo4j APOC's equivalent.
174    #[cfg(feature = "apoc-meta")]
175    ApocMetaSchema,
176}
177
178/// One data-table row. Columns are keyed by declared column name
179/// so the registry can look up either the input side (for arg
180/// matching) or the output side (for YIELD projection) without
181/// recomputing offsets.
182pub type ProcRow = HashMap<String, Value>;
183
184impl Procedure {
185    /// True when the call arguments match this row's input columns.
186    /// Applied per row during execution — rows whose input cells
187    /// differ from the supplied arg values are filtered out.
188    /// Argument-type coercion (`FLOAT` accepts an integer, etc.) is
189    /// handled by the caller converting the call arg to the declared
190    /// type before comparing here.
191    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    /// True when this procedure mutates the store and therefore
202    /// needs to be dispatched through [`Self::resolve_write_rows`]
203    /// (which receives the writer and the already-evaluated args)
204    /// rather than [`Self::resolve_rows`]. Read-only built-ins and
205    /// pre-populated rows return `false`.
206    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    /// Produce the row set the executor should iterate. Static
225    /// procedures simply hand back their pre-populated `rows`;
226    /// built-ins derive their rows from the live graph via `reader`.
227    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    /// Write-procedure dispatch path. The args are already
263    /// evaluated and type-checked; the row is produced as a side
264    /// effect of the mutation (e.g. the newly-created node) so
265    /// `row_matches` is skipped for these procedures.
266    #[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/// Implementation of `apoc.meta.schema()` — one full graph walk,
388/// collecting per-label and per-relationship-type statistics and
389/// merging them with the registered index set into a single Map
390/// keyed by label / type name. Matches Neo4j's APOC shape closely
391/// enough that ported dashboards rendering the `value` column
392/// keep working:
393///
394/// ```text
395/// {
396///   <label>: {
397///     type: "node",
398///     count: <int>,
399///     properties: {
400///       <key>: { type: <APOC type name>, indexed: <bool> }
401///     }
402///   },
403///   <edgeType>: {
404///     type: "relationship",
405///     count: <int>,
406///     properties: {
407///       <key>: { type: <APOC type name> }
408///     }
409///   }
410/// }
411/// ```
412///
413/// Emits a single row under the `value` column (standard APOC
414/// convention). Unlabeled nodes are omitted — Neo4j APOC does the
415/// same, since there's no natural key for that bucket.
416#[cfg(feature = "apoc-meta")]
417fn builtin_apoc_meta_schema(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
418    use std::collections::HashSet;
419    // Per-label accumulator: count + observed (key → type set).
420    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    // Index lookup: a property is marked `indexed: true` if any
457    // registered index mentions it (composite indexes count for
458    // each of their columns). Only node-scope indexes feed the
459    // per-label view; relationship property indexes follow the
460    // same pattern but under the edge type.
461    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/// Build one label-or-type entry in the schema map. `type_tag`
500/// is `"node"` or `"relationship"`. If a property was observed
501/// with several value kinds across rows, its `type` cell collapses
502/// those into a sorted `"STRING|INTEGER"` string — that's the
503/// convention Neo4j APOC uses for heterogeneous columns.
504#[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/// Upper-snake-case type name for a [`Property`], matching the
531/// spelling used by `apoc.meta.type` (e.g. `"STRING"`,
532/// `"DATE_TIME"`). Duplicates the scalar-side table in
533/// `meshdb_apoc::meta` because procedures.rs deliberately stays
534/// decoupled from meshdb-apoc.
535#[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/// Implementation of the `apoc.create.node(labels, props)` write
555/// builtin. Constructs a [`Node`] from the args, persists it via
556/// `writer.put_node`, and yields a single row with the new node
557/// under the `node` column. Both args may be Null (Neo4j allows
558/// `apoc.create.node(null, null)` — yields an empty unlabeled
559/// node), but a non-null first arg must be a list of strings and
560/// a non-null second arg must be a map.
561#[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    // Skip null property values — matches the openCypher rule used
608    // by `CREATE (n {k: null})`: the key is treated as absent.
609    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/// Implementation of the `apoc.create.relationship(from, type,
621/// props, to)` write builtin. Argument order matches Neo4j's
622/// APOC: from + type + props + to. Both endpoint args must
623/// resolve to a [`Value::Node`]; the procedure does not
624/// auto-create missing endpoints (use `apoc.create.node` first
625/// or merge through CREATE).
626#[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/// Implementation of `apoc.refactor.setType(rel, newType)`.
672/// Edges in Mesh carry their type as immutable storage state, so
673/// changing it requires a delete + recreate. The new edge keeps
674/// the source / target / properties of the old but receives a
675/// fresh [`EdgeId`] — mirrors what Neo4j APOC does (the
676/// procedure documents the ID change explicitly).
677#[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    // Same-type case is a no-op: hand the existing edge back
713    // unchanged so the caller's EdgeId stays valid.
714    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/// Three-way switch shared by the label mutators
729/// (`apoc.create.addLabels` / `removeLabels` / `setLabels`). The
730/// per-mode logic is identical except for how the existing label
731/// list is combined with the supplied list.
732#[cfg(feature = "apoc-create")]
733#[derive(Debug, Clone, Copy)]
734enum LabelMode {
735    Add,
736    Remove,
737    Set,
738}
739
740/// Implementation of `apoc.create.addLabels` / `removeLabels` /
741/// `setLabels`. The first arg is either a single Node or a list
742/// of Nodes (matching APOC's variadic input). The second arg is
743/// a list of label strings. Each input node is reloaded fresh
744/// from the reader so we apply the mutation to the latest state
745/// (the Node value in the row may have been captured earlier in
746/// the query). The updated node is written back via the writer
747/// and yielded under the `node` column.
748#[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/// Implementation of `apoc.create.setProperty(nodes, key,
787/// value)`. Sets the property on each supplied node; passing a
788/// null value clears the property (matches APOC, and matches the
789/// `SET n.k = null` openCypher rule). The node is reloaded from
790/// the reader before mutation so concurrent writes earlier in
791/// the query are picked up.
792#[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/// Relationship-scope counterpart to [`apoc_set_node_property`].
820#[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/// Coerce the variadic first argument used by the label / set-
848/// property mutators into a flat list of [`NodeId`]s. Accepts a
849/// single Node or a (possibly nested) list of Nodes — anything
850/// else is a type error.
851#[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/// Edge-scope counterpart to [`expect_node_or_node_list`].
884#[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/// Resolve an endpoint argument to its [`NodeId`]. Both
955/// [`Value::Node`] (a fully-materialised node) and a node-id
956/// integer are accepted; everything else (including null) is a
957/// type error since a relationship needs concrete endpoints.
958#[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/// Convert a [`Value`] supplied as a procedure arg into a
972/// storable [`Property`]. Mirrors the `value_to_property` helper
973/// in `ops.rs` but lives here so procedures.rs can stay
974/// self-contained — graph elements (Node/Edge/Path) and
975/// graph-aware Maps aren't valid as stored properties.
976#[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/// Lookup table for registered procedures, keyed by fully qualified
1014/// name (`test.my.proc`). The executor consults this at run time;
1015/// callers (TCK harness, server startup) build an instance and pass
1016/// it to [`crate::execute_with_reader_and_procs`]. An empty registry
1017/// is the default, meaning no procedures are known and any CALL
1018/// raises `ProcedureNotFound`.
1019#[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    /// Register the built-in `db.labels`, `db.relationshipTypes`, and
1039    /// `db.propertyKeys` procedures. Call once at server startup —
1040    /// the executor materialises each call's row set from the live
1041    /// graph, so no data needs to be recomputed here when new
1042    /// labels / types / keys appear.
1043    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                    // Returned as a list of strings; the type lattice
1093                    // doesn't have a dedicated `List<String>` variant
1094                    // so `ANY` is the closest fit — the executor
1095                    // preserves the actual List value at call time.
1096                    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                    // List<String> declared as ANY because the type
1113                    // lattice doesn't carry container parameters; the
1114                    // builtin validates structure at call time.
1115                    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                // Argument order matches Neo4j's APOC: from, type,
1134                // props, to. Endpoints accept Value::Node directly;
1135                // ProcType::Any is the broadest match that lets a
1136                // node value flow through without coercion.
1137                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                    // First arg accepts a Node or a list of Nodes;
1171                    // ProcType::Any covers both without coercion.
1172                    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}