Skip to main content

meshdb_executor/
procedures.rs

1use crate::value::Value;
2use meshdb_core::Property;
3use std::collections::HashMap;
4
5/// Declared argument / output type for a procedure signature.
6/// Mirrors the openCypher type names the TCK uses (`STRING?`,
7/// `INTEGER?`, `FLOAT?`, `NUMBER?`, `BOOLEAN?`, `ANY?`). Nullability
8/// is not tracked separately — every TCK type in practice is nullable
9/// (`?`) and the match logic treats nulls uniformly.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ProcType {
12    String,
13    Integer,
14    Float,
15    Number,
16    Boolean,
17    Any,
18}
19
20impl ProcType {
21    pub fn parse(s: &str) -> Self {
22        let trimmed = s.trim().trim_end_matches('?').trim();
23        match trimmed.to_ascii_uppercase().as_str() {
24            "STRING" => ProcType::String,
25            "INTEGER" | "INT" => ProcType::Integer,
26            "FLOAT" => ProcType::Float,
27            "NUMBER" | "NUMERIC" => ProcType::Number,
28            "BOOLEAN" | "BOOL" => ProcType::Boolean,
29            _ => ProcType::Any,
30        }
31    }
32
33    /// True when `value` is acceptable as a procedure argument of
34    /// this declared type. Follows Neo4j's assignable-type rules:
35    /// `FLOAT` accepts integers (coerced), `NUMBER` accepts both
36    /// numeric kinds, `ANY` accepts everything.
37    pub fn accepts(&self, value: &Value) -> bool {
38        if matches!(value, Value::Null) {
39            return true;
40        }
41        match (self, value) {
42            (ProcType::Any, _) => true,
43            (ProcType::String, Value::Property(Property::String(_))) => true,
44            (ProcType::Integer, Value::Property(Property::Int64(_))) => true,
45            (ProcType::Float, Value::Property(Property::Float64(_))) => true,
46            (ProcType::Float, Value::Property(Property::Int64(_))) => true,
47            (ProcType::Number, Value::Property(Property::Int64(_))) => true,
48            (ProcType::Number, Value::Property(Property::Float64(_))) => true,
49            (ProcType::Boolean, Value::Property(Property::Bool(_))) => true,
50            _ => false,
51        }
52    }
53}
54
55#[derive(Debug, Clone)]
56pub struct ProcArgSpec {
57    pub name: String,
58    pub ty: ProcType,
59}
60
61#[derive(Debug, Clone)]
62pub struct ProcOutSpec {
63    pub name: String,
64    pub ty: ProcType,
65}
66
67/// A procedure registered with a [`ProcedureRegistry`]. The TCK
68/// harness builds one per `And there exists a procedure ...` step by
69/// collating the signature and the gherkin data table: each data row
70/// contributes one entry to `rows` where the leading cells are the
71/// input-column values (matched against call arguments) and the
72/// trailing cells are the output-column values (projected by
73/// YIELD).
74#[derive(Debug, Clone)]
75pub struct Procedure {
76    pub qualified_name: Vec<String>,
77    pub inputs: Vec<ProcArgSpec>,
78    pub outputs: Vec<ProcOutSpec>,
79    pub rows: Vec<ProcRow>,
80}
81
82/// One data-table row. Columns are keyed by declared column name
83/// so the registry can look up either the input side (for arg
84/// matching) or the output side (for YIELD projection) without
85/// recomputing offsets.
86pub type ProcRow = HashMap<String, Value>;
87
88impl Procedure {
89    /// True when the call arguments match this row's input columns.
90    /// Applied per row during execution — rows whose input cells
91    /// differ from the supplied arg values are filtered out.
92    /// Argument-type coercion (`FLOAT` accepts an integer, etc.) is
93    /// handled by the caller converting the call arg to the declared
94    /// type before comparing here.
95    pub fn row_matches(&self, row: &ProcRow, args: &[Value]) -> bool {
96        for (spec, arg) in self.inputs.iter().zip(args.iter()) {
97            let cell = row.get(&spec.name).unwrap_or(&Value::Null);
98            if !values_equal_for_procedure(cell, arg) {
99                return false;
100            }
101        }
102        true
103    }
104}
105
106fn values_equal_for_procedure(a: &Value, b: &Value) -> bool {
107    match (a, b) {
108        (Value::Null, Value::Null) => true,
109        (Value::Null, _) | (_, Value::Null) => false,
110        (Value::Property(Property::Int64(x)), Value::Property(Property::Int64(y))) => x == y,
111        (Value::Property(Property::Float64(x)), Value::Property(Property::Float64(y))) => x == y,
112        (Value::Property(Property::Int64(i)), Value::Property(Property::Float64(f)))
113        | (Value::Property(Property::Float64(f)), Value::Property(Property::Int64(i))) => {
114            *f == (*i as f64)
115        }
116        (Value::Property(Property::String(x)), Value::Property(Property::String(y))) => x == y,
117        (Value::Property(Property::Bool(x)), Value::Property(Property::Bool(y))) => x == y,
118        _ => a == b,
119    }
120}
121
122/// Lookup table for registered procedures, keyed by fully qualified
123/// name (`test.my.proc`). The executor consults this at run time;
124/// callers (TCK harness, server startup) build an instance and pass
125/// it to [`crate::execute_with_reader_and_procs`]. An empty registry
126/// is the default, meaning no procedures are known and any CALL
127/// raises `ProcedureNotFound`.
128#[derive(Debug, Clone, Default)]
129pub struct ProcedureRegistry {
130    procs: HashMap<String, Procedure>,
131}
132
133impl ProcedureRegistry {
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    pub fn register(&mut self, proc: Procedure) {
139        let key = proc.qualified_name.join(".");
140        self.procs.insert(key, proc);
141    }
142
143    pub fn get(&self, qualified_name: &[String]) -> Option<&Procedure> {
144        self.procs.get(&qualified_name.join("."))
145    }
146}