Skip to main content

meshdb_executor/
procedures.rs

1use crate::error::Result;
2use crate::reader::GraphReader;
3use crate::value::Value;
4use meshdb_core::Property;
5use std::collections::{BTreeSet, HashMap};
6
7/// Declared argument / output type for a procedure signature.
8/// Mirrors the openCypher type names the TCK uses (`STRING?`,
9/// `INTEGER?`, `FLOAT?`, `NUMBER?`, `BOOLEAN?`, `ANY?`). Nullability
10/// is not tracked separately — every TCK type in practice is nullable
11/// (`?`) and the match logic treats nulls uniformly.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ProcType {
14    String,
15    Integer,
16    Float,
17    Number,
18    Boolean,
19    Any,
20}
21
22impl ProcType {
23    pub fn parse(s: &str) -> Self {
24        let trimmed = s.trim().trim_end_matches('?').trim();
25        match trimmed.to_ascii_uppercase().as_str() {
26            "STRING" => ProcType::String,
27            "INTEGER" | "INT" => ProcType::Integer,
28            "FLOAT" => ProcType::Float,
29            "NUMBER" | "NUMERIC" => ProcType::Number,
30            "BOOLEAN" | "BOOL" => ProcType::Boolean,
31            _ => ProcType::Any,
32        }
33    }
34
35    /// True when `value` is acceptable as a procedure argument of
36    /// this declared type. Follows Neo4j's assignable-type rules:
37    /// `FLOAT` accepts integers (coerced), `NUMBER` accepts both
38    /// numeric kinds, `ANY` accepts everything.
39    pub fn accepts(&self, value: &Value) -> bool {
40        if matches!(value, Value::Null) {
41            return true;
42        }
43        match (self, value) {
44            (ProcType::Any, _) => true,
45            (ProcType::String, Value::Property(Property::String(_))) => true,
46            (ProcType::Integer, Value::Property(Property::Int64(_))) => true,
47            (ProcType::Float, Value::Property(Property::Float64(_))) => true,
48            (ProcType::Float, Value::Property(Property::Int64(_))) => true,
49            (ProcType::Number, Value::Property(Property::Int64(_))) => true,
50            (ProcType::Number, Value::Property(Property::Float64(_))) => true,
51            (ProcType::Boolean, Value::Property(Property::Bool(_))) => true,
52            _ => false,
53        }
54    }
55}
56
57#[derive(Debug, Clone)]
58pub struct ProcArgSpec {
59    pub name: String,
60    pub ty: ProcType,
61}
62
63#[derive(Debug, Clone)]
64pub struct ProcOutSpec {
65    pub name: String,
66    pub ty: ProcType,
67}
68
69/// A procedure registered with a [`ProcedureRegistry`]. The TCK
70/// harness builds one per `And there exists a procedure ...` step by
71/// collating the signature and the gherkin data table: each data row
72/// contributes one entry to `rows` where the leading cells are the
73/// input-column values (matched against call arguments) and the
74/// trailing cells are the output-column values (projected by
75/// YIELD).
76///
77/// Built-in procedures — `db.labels()` and friends — leave `rows`
78/// empty and set `builtin` so the executor materialises the row set
79/// live from the current graph via [`Procedure::resolve_rows`].
80#[derive(Debug, Clone)]
81pub struct Procedure {
82    pub qualified_name: Vec<String>,
83    pub inputs: Vec<ProcArgSpec>,
84    pub outputs: Vec<ProcOutSpec>,
85    pub rows: Vec<ProcRow>,
86    pub builtin: Option<BuiltinProc>,
87}
88
89/// Identifies a procedure whose rows are derived from the live graph
90/// at call time rather than pre-populated in [`Procedure::rows`].
91/// Keeps the procedure surface uniform — the executor still iterates
92/// `ProcRow`s; the only difference is who produced them.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum BuiltinProc {
95    /// `db.labels()` — yields one row per distinct node label.
96    DbLabels,
97    /// `db.relationshipTypes()` — yields one row per distinct edge type.
98    DbRelationshipTypes,
99    /// `db.propertyKeys()` — yields one row per distinct property key
100    /// observed on any node or edge.
101    DbPropertyKeys,
102    /// `db.constraints()` — yields one row per registered constraint,
103    /// carrying `name`, `label`, `property`, and `type` columns.
104    /// Mirrors the `SHOW CONSTRAINTS` surface.
105    DbConstraints,
106}
107
108/// One data-table row. Columns are keyed by declared column name
109/// so the registry can look up either the input side (for arg
110/// matching) or the output side (for YIELD projection) without
111/// recomputing offsets.
112pub type ProcRow = HashMap<String, Value>;
113
114impl Procedure {
115    /// True when the call arguments match this row's input columns.
116    /// Applied per row during execution — rows whose input cells
117    /// differ from the supplied arg values are filtered out.
118    /// Argument-type coercion (`FLOAT` accepts an integer, etc.) is
119    /// handled by the caller converting the call arg to the declared
120    /// type before comparing here.
121    pub fn row_matches(&self, row: &ProcRow, args: &[Value]) -> bool {
122        for (spec, arg) in self.inputs.iter().zip(args.iter()) {
123            let cell = row.get(&spec.name).unwrap_or(&Value::Null);
124            if !values_equal_for_procedure(cell, arg) {
125                return false;
126            }
127        }
128        true
129    }
130
131    /// Produce the row set the executor should iterate. Static
132    /// procedures simply hand back their pre-populated `rows`;
133    /// built-ins derive their rows from the live graph via `reader`.
134    pub fn resolve_rows(&self, reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
135        match self.builtin {
136            None => Ok(self.rows.clone()),
137            Some(BuiltinProc::DbLabels) => builtin_db_labels(reader),
138            Some(BuiltinProc::DbRelationshipTypes) => builtin_db_relationship_types(reader),
139            Some(BuiltinProc::DbPropertyKeys) => builtin_db_property_keys(reader),
140            Some(BuiltinProc::DbConstraints) => builtin_db_constraints(reader),
141        }
142    }
143}
144
145fn str_row(column: &str, value: String) -> ProcRow {
146    let mut row = HashMap::new();
147    row.insert(column.to_string(), Value::Property(Property::String(value)));
148    row
149}
150
151fn builtin_db_labels(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
152    let mut labels: BTreeSet<String> = BTreeSet::new();
153    for id in reader.all_node_ids()? {
154        if let Some(n) = reader.get_node(id)? {
155            for l in n.labels {
156                labels.insert(l);
157            }
158        }
159    }
160    Ok(labels.into_iter().map(|l| str_row("label", l)).collect())
161}
162
163fn builtin_db_relationship_types(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
164    let mut types: BTreeSet<String> = BTreeSet::new();
165    for id in reader.all_node_ids()? {
166        for (edge_id, _) in reader.outgoing(id)? {
167            if let Some(e) = reader.get_edge(edge_id)? {
168                types.insert(e.edge_type);
169            }
170        }
171    }
172    Ok(types
173        .into_iter()
174        .map(|t| str_row("relationshipType", t))
175        .collect())
176}
177
178fn builtin_db_constraints(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
179    let specs = reader.list_property_constraints()?;
180    Ok(specs
181        .into_iter()
182        .map(|spec| {
183            let mut row: ProcRow = HashMap::new();
184            row.insert("name".into(), Value::Property(Property::String(spec.name)));
185            let (scope_tag, target) = match spec.scope {
186                meshdb_storage::ConstraintScope::Node(l) => ("NODE", l),
187                meshdb_storage::ConstraintScope::Relationship(t) => ("RELATIONSHIP", t),
188            };
189            row.insert(
190                "scope".into(),
191                Value::Property(Property::String(scope_tag.into())),
192            );
193            row.insert("label".into(), Value::Property(Property::String(target)));
194            let props: Vec<Property> = spec.properties.into_iter().map(Property::String).collect();
195            row.insert("properties".into(), Value::Property(Property::List(props)));
196            row.insert(
197                "type".into(),
198                Value::Property(Property::String(spec.kind.as_string())),
199            );
200            row
201        })
202        .collect())
203}
204
205fn builtin_db_property_keys(reader: &dyn GraphReader) -> Result<Vec<ProcRow>> {
206    let mut keys: BTreeSet<String> = BTreeSet::new();
207    for id in reader.all_node_ids()? {
208        if let Some(n) = reader.get_node(id)? {
209            for k in n.properties.keys() {
210                keys.insert(k.clone());
211            }
212            for (edge_id, _) in reader.outgoing(id)? {
213                if let Some(e) = reader.get_edge(edge_id)? {
214                    for k in e.properties.keys() {
215                        keys.insert(k.clone());
216                    }
217                }
218            }
219        }
220    }
221    Ok(keys
222        .into_iter()
223        .map(|k| str_row("propertyKey", k))
224        .collect())
225}
226
227fn values_equal_for_procedure(a: &Value, b: &Value) -> bool {
228    match (a, b) {
229        (Value::Null, Value::Null) => true,
230        (Value::Null, _) | (_, Value::Null) => false,
231        (Value::Property(Property::Int64(x)), Value::Property(Property::Int64(y))) => x == y,
232        (Value::Property(Property::Float64(x)), Value::Property(Property::Float64(y))) => x == y,
233        (Value::Property(Property::Int64(i)), Value::Property(Property::Float64(f)))
234        | (Value::Property(Property::Float64(f)), Value::Property(Property::Int64(i))) => {
235            *f == (*i as f64)
236        }
237        (Value::Property(Property::String(x)), Value::Property(Property::String(y))) => x == y,
238        (Value::Property(Property::Bool(x)), Value::Property(Property::Bool(y))) => x == y,
239        _ => a == b,
240    }
241}
242
243/// Lookup table for registered procedures, keyed by fully qualified
244/// name (`test.my.proc`). The executor consults this at run time;
245/// callers (TCK harness, server startup) build an instance and pass
246/// it to [`crate::execute_with_reader_and_procs`]. An empty registry
247/// is the default, meaning no procedures are known and any CALL
248/// raises `ProcedureNotFound`.
249#[derive(Debug, Clone, Default)]
250pub struct ProcedureRegistry {
251    procs: HashMap<String, Procedure>,
252}
253
254impl ProcedureRegistry {
255    pub fn new() -> Self {
256        Self::default()
257    }
258
259    pub fn register(&mut self, proc: Procedure) {
260        let key = proc.qualified_name.join(".");
261        self.procs.insert(key, proc);
262    }
263
264    pub fn get(&self, qualified_name: &[String]) -> Option<&Procedure> {
265        self.procs.get(&qualified_name.join("."))
266    }
267
268    /// Register the built-in `db.labels`, `db.relationshipTypes`, and
269    /// `db.propertyKeys` procedures. Call once at server startup —
270    /// the executor materialises each call's row set from the live
271    /// graph, so no data needs to be recomputed here when new
272    /// labels / types / keys appear.
273    pub fn register_defaults(&mut self) {
274        self.register(Procedure {
275            qualified_name: vec!["db".into(), "labels".into()],
276            inputs: Vec::new(),
277            outputs: vec![ProcOutSpec {
278                name: "label".into(),
279                ty: ProcType::String,
280            }],
281            rows: Vec::new(),
282            builtin: Some(BuiltinProc::DbLabels),
283        });
284        self.register(Procedure {
285            qualified_name: vec!["db".into(), "relationshipTypes".into()],
286            inputs: Vec::new(),
287            outputs: vec![ProcOutSpec {
288                name: "relationshipType".into(),
289                ty: ProcType::String,
290            }],
291            rows: Vec::new(),
292            builtin: Some(BuiltinProc::DbRelationshipTypes),
293        });
294        self.register(Procedure {
295            qualified_name: vec!["db".into(), "propertyKeys".into()],
296            inputs: Vec::new(),
297            outputs: vec![ProcOutSpec {
298                name: "propertyKey".into(),
299                ty: ProcType::String,
300            }],
301            rows: Vec::new(),
302            builtin: Some(BuiltinProc::DbPropertyKeys),
303        });
304        self.register(Procedure {
305            qualified_name: vec!["db".into(), "constraints".into()],
306            inputs: Vec::new(),
307            outputs: vec![
308                ProcOutSpec {
309                    name: "name".into(),
310                    ty: ProcType::String,
311                },
312                ProcOutSpec {
313                    name: "scope".into(),
314                    ty: ProcType::String,
315                },
316                ProcOutSpec {
317                    name: "label".into(),
318                    ty: ProcType::String,
319                },
320                ProcOutSpec {
321                    name: "properties".into(),
322                    // Returned as a list of strings; the type lattice
323                    // doesn't have a dedicated `List<String>` variant
324                    // so `ANY` is the closest fit — the executor
325                    // preserves the actual List value at call time.
326                    ty: ProcType::Any,
327                },
328                ProcOutSpec {
329                    name: "type".into(),
330                    ty: ProcType::String,
331                },
332            ],
333            rows: Vec::new(),
334            builtin: Some(BuiltinProc::DbConstraints),
335        });
336    }
337}