Skip to main content

marsdb_query/
params.rs

1use std::collections::HashMap;
2
3use marsdb_graph::PropertyValue;
4
5use crate::ast::{
6    CallClause, CallYield, Expr, Literal, MergeClause, NodePattern, Pattern, QueryClause,
7    QueryPart, ReturnExpr, ReturnTail, SetItem, Statement, Tail, UnwindClause, WithClause,
8    WithExpr,
9};
10use crate::error::QueryError;
11
12/// Resolves every `$name` placeholder in `stmt` to a concrete `Literal`
13/// using `params`, in place. Called before execution so the executor never
14/// sees `Literal::Param` — see the `unreachable!` in
15/// `executor::literal_to_value`.
16pub fn substitute_params(
17    stmt: &mut Statement,
18    params: &HashMap<String, PropertyValue>,
19) -> Result<(), QueryError> {
20    match stmt {
21        // Bare keywords, nothing to substitute into.
22        Statement::Begin | Statement::Commit | Statement::Rollback => {}
23        Statement::Create(patterns) => {
24            for pattern in patterns {
25                substitute_pattern(pattern, params)?;
26            }
27        }
28        // No `$param`-able position -- label/prop are identifiers, not
29        // expressions.
30        Statement::CreateIndex { .. } => {}
31        Statement::Explain(inner) => substitute_params(inner, params)?,
32        Statement::Match {
33            clauses,
34            tail,
35            order_by,
36            skip,
37            limit,
38        } => {
39            for clause in clauses {
40                substitute_query_clause(clause, params)?;
41            }
42            if let Some(tail) = tail {
43                substitute_tail(tail, params)?;
44            }
45            if let Some(items) = order_by {
46                for (expr, _) in items {
47                    substitute_return_expr(expr, params)?;
48                }
49            }
50            if let Some(expr) = skip {
51                substitute_return_expr(expr, params)?;
52            }
53            if let Some(expr) = limit {
54                substitute_return_expr(expr, params)?;
55            }
56        }
57        Statement::Union { parts, .. } => {
58            for part in parts {
59                substitute_params(part, params)?;
60            }
61        }
62        Statement::StandaloneCall(call) => substitute_call_clause(call, params)?,
63    }
64    Ok(())
65}
66
67/// `CallClause::args: None` (the implicit-argument form, `CALL proc` with
68/// no parens) has nothing to substitute here -- each declared input
69/// resolves from a same-named `$param` at execution time instead (see
70/// `ExecutionOptions::params`'s own docs for why that can't happen this
71/// early, before the procedure's signature is even known).
72fn substitute_call_clause(
73    call: &mut CallClause,
74    params: &HashMap<String, PropertyValue>,
75) -> Result<(), QueryError> {
76    if let Some(args) = &mut call.args {
77        for arg in args {
78            substitute_return_expr(arg, params)?;
79        }
80    }
81    if let Some(CallYield::Items(_, Some(where_expr))) = &mut call.yield_items {
82        substitute_expr(where_expr, params)?;
83    }
84    Ok(())
85}
86
87fn substitute_query_clause(
88    clause: &mut QueryClause,
89    params: &HashMap<String, PropertyValue>,
90) -> Result<(), QueryError> {
91    match clause {
92        QueryClause::Match(part) => substitute_query_part(part, params),
93        QueryClause::Unwind(u) => substitute_unwind_clause(u, params),
94        QueryClause::Merge(m) => substitute_merge_clause(m, params),
95        QueryClause::With(with) => substitute_with_clause(with, params),
96        QueryClause::Set(items) => {
97            for item in items {
98                substitute_set_item(item, params)?;
99            }
100            Ok(())
101        }
102        QueryClause::Delete { items, detach: _ } => {
103            for expr in items {
104                substitute_return_expr(expr, params)?;
105            }
106            Ok(())
107        }
108        // No `$param`-able position -- `RemoveItem` is a bare prop/label
109        // path, not a value expression.
110        QueryClause::Remove(_) => Ok(()),
111        QueryClause::Create(patterns) => {
112            for pattern in patterns {
113                substitute_pattern(pattern, params)?;
114            }
115            Ok(())
116        }
117        QueryClause::Call(call) => substitute_call_clause(call, params),
118    }
119}
120
121/// Shared by every `SetItem` list this file substitutes into (`SET`'s own
122/// `QueryClause`/`Tail` forms, and `MERGE`'s `ON CREATE`/`ON MATCH SET`)
123/// -- `Labels` has no `$param`-able position (a label name is always a
124/// bare identifier), `Prop`/`MapAssign` both carry exactly one
125/// `ReturnExpr` value to recurse into.
126fn substitute_set_item(
127    item: &mut SetItem,
128    params: &HashMap<String, PropertyValue>,
129) -> Result<(), QueryError> {
130    match item {
131        SetItem::Prop(_, value) | SetItem::MapAssign { value, .. } => {
132            substitute_return_expr(value, params)
133        }
134        SetItem::Labels(..) => Ok(()),
135    }
136}
137
138fn substitute_merge_clause(
139    m: &mut MergeClause,
140    params: &HashMap<String, PropertyValue>,
141) -> Result<(), QueryError> {
142    substitute_pattern(&mut m.pattern, params)?;
143    for item in m.on_create.iter_mut().chain(m.on_match.iter_mut()) {
144        substitute_set_item(item, params)?;
145    }
146    if let Some(with) = &mut m.with {
147        substitute_with_clause(with, params)?;
148    }
149    Ok(())
150}
151
152fn substitute_query_part(
153    part: &mut QueryPart,
154    params: &HashMap<String, PropertyValue>,
155) -> Result<(), QueryError> {
156    substitute_pattern(&mut part.pattern, params)?;
157    if let Some(expr) = &mut part.where_clause {
158        substitute_expr(expr, params)?;
159    }
160    if let Some(with) = &mut part.with {
161        substitute_with_clause(with, params)?;
162    }
163    Ok(())
164}
165
166fn substitute_unwind_clause(
167    u: &mut UnwindClause,
168    params: &HashMap<String, PropertyValue>,
169) -> Result<(), QueryError> {
170    substitute_return_expr(&mut u.source.0, params)?;
171    if let Some(expr) = &mut u.where_clause {
172        substitute_with_expr(expr, params)?;
173    }
174    if let Some(with) = &mut u.with {
175        substitute_with_clause(with, params)?;
176    }
177    Ok(())
178}
179
180fn substitute_with_clause(
181    with: &mut WithClause,
182    params: &HashMap<String, PropertyValue>,
183) -> Result<(), QueryError> {
184    for item in &mut with.items {
185        substitute_return_expr(&mut item.expr, params)?;
186    }
187    if let Some(where_clause) = &mut with.where_clause {
188        substitute_with_expr(where_clause, params)?;
189    }
190    if let Some(items) = &mut with.order_by {
191        for (expr, _) in items {
192            substitute_return_expr(expr, params)?;
193        }
194    }
195    if let Some(expr) = &mut with.skip {
196        substitute_return_expr(expr, params)?;
197    }
198    if let Some(expr) = &mut with.limit {
199        substitute_return_expr(expr, params)?;
200    }
201    Ok(())
202}
203
204fn substitute_with_expr(
205    expr: &mut WithExpr,
206    params: &HashMap<String, PropertyValue>,
207) -> Result<(), QueryError> {
208    match expr {
209        WithExpr::And(l, r) | WithExpr::Or(l, r) => {
210            substitute_with_expr(l, params)?;
211            substitute_with_expr(r, params)?;
212        }
213        WithExpr::Not(e) => substitute_with_expr(e, params)?,
214        WithExpr::Compare(lhs, _, rhs) => {
215            substitute_return_expr(lhs, params)?;
216            substitute_return_expr(rhs, params)?;
217        }
218        WithExpr::IsNull(e) => substitute_return_expr(e, params)?,
219        WithExpr::Bare(e) => substitute_return_expr(e, params)?,
220    }
221    Ok(())
222}
223
224fn substitute_pattern(
225    pattern: &mut Pattern,
226    params: &HashMap<String, PropertyValue>,
227) -> Result<(), QueryError> {
228    substitute_node(&mut pattern.start, params)?;
229    for (rel, node) in &mut pattern.hops {
230        for (_, expr) in &mut rel.props {
231            substitute_return_expr(expr, params)?;
232        }
233        substitute_node(node, params)?;
234    }
235    Ok(())
236}
237
238fn substitute_node(
239    node: &mut NodePattern,
240    params: &HashMap<String, PropertyValue>,
241) -> Result<(), QueryError> {
242    for (_, expr) in &mut node.props {
243        substitute_return_expr(expr, params)?;
244    }
245    Ok(())
246}
247
248fn substitute_expr(
249    expr: &mut Expr,
250    params: &HashMap<String, PropertyValue>,
251) -> Result<(), QueryError> {
252    match expr {
253        Expr::And(l, r) | Expr::Or(l, r) => {
254            substitute_expr(l, params)?;
255            substitute_expr(r, params)?;
256        }
257        Expr::Not(e) => substitute_expr(e, params)?,
258        Expr::Compare(_, _, lit) => substitute_literal(lit, params)?,
259        Expr::PropCompare(_, _, _) => {}
260        Expr::IsNull(_) => {}
261        Expr::HasLabel(_, _) => {}
262        Expr::VarEq(_, _) => {}
263        // Same "just variable names, no `$param`-able position" reasoning
264        // as `VarEq` above -- also planner-synthesized only, never
265        // present in the AST `substitute_params` runs against at all
266        // (built during planning, well after this pass).
267        Expr::EdgeNotInSet { .. } => {}
268        Expr::GeneralCompare(lhs, _, rhs) => {
269            substitute_return_expr(lhs, params)?;
270            substitute_return_expr(rhs, params)?;
271        }
272        Expr::GeneralIsNull(e) => substitute_return_expr(e, params)?,
273        Expr::GeneralBare(e) => substitute_return_expr(e, params)?,
274        Expr::Pattern(pattern) => substitute_pattern(pattern, params)?,
275        Expr::Exists {
276            pattern,
277            where_clause,
278        } => {
279            substitute_pattern(pattern, params)?;
280            if let Some(w) = where_clause {
281                substitute_expr(w, params)?;
282            }
283        }
284        Expr::ExistsSubquery(stmt) => substitute_params(stmt, params)?,
285    }
286    Ok(())
287}
288
289fn substitute_tail(
290    tail: &mut Tail,
291    params: &HashMap<String, PropertyValue>,
292) -> Result<(), QueryError> {
293    match tail {
294        Tail::Return(items, _) => {
295            for item in items {
296                substitute_return_expr(&mut item.expr, params)?;
297            }
298        }
299        // No `$param`-able position -- a bare `*`, nothing to substitute.
300        Tail::ReturnStar(_) => {}
301        Tail::Delete(exprs, ret) | Tail::DetachDelete(exprs, ret) => {
302            for expr in exprs {
303                substitute_return_expr(expr, params)?;
304            }
305            substitute_return_tail(ret, params)?;
306        }
307        Tail::Remove(_, ret) => {
308            substitute_return_tail(ret, params)?;
309        }
310        Tail::Set(items, ret) => {
311            for item in items {
312                substitute_set_item(item, params)?;
313            }
314            substitute_return_tail(ret, params)?;
315        }
316        Tail::Create(patterns, ret) => {
317            for pattern in patterns {
318                substitute_pattern(pattern, params)?;
319            }
320            substitute_return_tail(ret, params)?;
321        }
322    }
323    Ok(())
324}
325
326/// Substitutes params in a mutating tail's optional trailing `RETURN`
327/// (`MATCH (n) SET n.x = $x RETURN n` needs both the `SET`'s own `$x` *and*
328/// nothing extra here since this RETURN has none — but `MATCH (n) DELETE n
329/// RETURN $y` does).
330fn substitute_return_tail(
331    ret: &mut Option<ReturnTail>,
332    params: &HashMap<String, PropertyValue>,
333) -> Result<(), QueryError> {
334    if let Some(rt) = ret {
335        for item in &mut rt.items {
336            substitute_return_expr(&mut item.expr, params)?;
337        }
338    }
339    Ok(())
340}
341
342fn substitute_return_expr(
343    expr: &mut ReturnExpr,
344    params: &HashMap<String, PropertyValue>,
345) -> Result<(), QueryError> {
346    match expr {
347        ReturnExpr::Var(_) | ReturnExpr::Prop(_) | ReturnExpr::CountStar => {}
348        ReturnExpr::PatternPredicate(pattern) => substitute_pattern(pattern, params)?,
349        // A list-valued `$param` can't substitute into a bare `Literal`
350        // (no `Literal::List` -- there's no list *literal* syntax in
351        // Cypher for one to mean, see `cypher.pest`'s docs) the way a
352        // scalar one does, so this replaces the *whole* `ReturnExpr::Lit`
353        // node with a `ReturnExpr::ListLit` instead, recursively (a param
354        // list can itself contain nested lists) -- everything downstream
355        // (indexing, `IN`, iteration, ...) already handles `ListLit` like
356        // any other list-valued expression, so nothing else needs to know
357        // this value originated from a parameter rather than `[1, 2, 3]`
358        // literal syntax.
359        ReturnExpr::Lit(Literal::Param(name)) => {
360            let value = params
361                .get(name)
362                .ok_or_else(|| QueryError::MissingParam(name.clone()))?
363                .clone();
364            *expr = property_value_to_return_expr(name, &value)?;
365        }
366        ReturnExpr::Lit(_) => {}
367        ReturnExpr::Call { args, .. } => {
368            for arg in args {
369                substitute_return_expr(arg, params)?;
370            }
371        }
372        ReturnExpr::Case { test, whens, else_ } => {
373            if let Some(t) = test {
374                substitute_return_expr(t, params)?;
375            }
376            for (when, then) in whens {
377                substitute_return_expr(when, params)?;
378                substitute_return_expr(then, params)?;
379            }
380            if let Some(e) = else_ {
381                substitute_return_expr(e, params)?;
382            }
383        }
384        ReturnExpr::Arith(l, _, r) => {
385            substitute_return_expr(l, params)?;
386            substitute_return_expr(r, params)?;
387        }
388        ReturnExpr::Neg(e) => substitute_return_expr(e, params)?,
389        ReturnExpr::ListLit(items) => {
390            for item in items {
391                substitute_return_expr(item, params)?;
392            }
393        }
394        ReturnExpr::Index(base, index) => {
395            substitute_return_expr(base, params)?;
396            substitute_return_expr(index, params)?;
397        }
398        ReturnExpr::PropOf(base, _) => substitute_return_expr(base, params)?,
399        ReturnExpr::Slice(base, start, end) => {
400            substitute_return_expr(base, params)?;
401            if let Some(s) = start {
402                substitute_return_expr(s, params)?;
403            }
404            if let Some(e) = end {
405                substitute_return_expr(e, params)?;
406            }
407        }
408        ReturnExpr::ListComp {
409            source,
410            where_clause,
411            project,
412            ..
413        } => {
414            substitute_return_expr(source, params)?;
415            if let Some(w) = where_clause {
416                substitute_return_expr(w, params)?;
417            }
418            if let Some(p) = project {
419                substitute_return_expr(p, params)?;
420            }
421        }
422        ReturnExpr::Quantifier {
423            source,
424            where_clause,
425            ..
426        } => {
427            substitute_return_expr(source, params)?;
428            if let Some(w) = where_clause {
429                substitute_return_expr(w, params)?;
430            }
431        }
432        ReturnExpr::MapLit(entries) => {
433            for (_, v) in entries {
434                substitute_return_expr(v, params)?;
435            }
436        }
437        ReturnExpr::And(l, r) | ReturnExpr::Or(l, r) | ReturnExpr::Xor(l, r) => {
438            substitute_return_expr(l, params)?;
439            substitute_return_expr(r, params)?;
440        }
441        ReturnExpr::Not(e) => substitute_return_expr(e, params)?,
442        ReturnExpr::Compare(l, _, r) => {
443            substitute_return_expr(l, params)?;
444            substitute_return_expr(r, params)?;
445        }
446        ReturnExpr::IsNull(e) => substitute_return_expr(e, params)?,
447        ReturnExpr::In(needle, haystack) => {
448            substitute_return_expr(needle, params)?;
449            substitute_return_expr(haystack, params)?;
450        }
451        // No `$param`-able position -- var/labels are identifiers, not
452        // expressions.
453        ReturnExpr::HasLabel(..) => {}
454        ReturnExpr::PatternComprehension {
455            pattern,
456            where_clause,
457            projection,
458            ..
459        } => {
460            substitute_pattern(pattern, params)?;
461            if let Some(w) = where_clause {
462                substitute_expr(w, params)?;
463            }
464            substitute_return_expr(projection, params)?;
465        }
466        ReturnExpr::ExistsPattern {
467            pattern,
468            where_clause,
469        } => {
470            substitute_pattern(pattern, params)?;
471            if let Some(w) = where_clause {
472                substitute_expr(w, params)?;
473            }
474        }
475        ReturnExpr::ExistsSubquery(stmt) => substitute_params(stmt, params)?,
476    }
477    Ok(())
478}
479
480fn substitute_literal(
481    lit: &mut Literal,
482    params: &HashMap<String, PropertyValue>,
483) -> Result<(), QueryError> {
484    if let Literal::Param(name) = lit {
485        let value = params
486            .get(name)
487            .ok_or_else(|| QueryError::MissingParam(name.clone()))?;
488        *lit = property_value_to_literal(name, value)?;
489    }
490    Ok(())
491}
492
493fn property_value_to_literal(name: &str, pv: &PropertyValue) -> Result<Literal, QueryError> {
494    Ok(match pv {
495        PropertyValue::Null => Literal::Null,
496        PropertyValue::Bool(b) => Literal::Bool(*b),
497        PropertyValue::Int(i) => Literal::Int(*i),
498        PropertyValue::Float(f) => Literal::Float(*f),
499        PropertyValue::String(s) => Literal::String(s.clone()),
500        // `Literal` has no temporal variant (there's no temporal *literal*
501        // syntax in Cypher -- see cypher.pest's docs: a date/duration is
502        // always built via `date(...)`/`duration(...)`), so a Date/
503        // Duration bound in from Rust as a `$param` has nowhere to
504        // substitute to *here specifically* -- this function only produces
505        // a bare `Literal`, for the one spot that structurally requires
506        // one (pattern-level `Expr::Compare`'s RHS, `n.prop = $x`). In
507        // ordinary expression position, a temporal-valued `$param`
508        // substitutes into a `ReturnExpr::Call` (e.g. `date("...")`)
509        // instead -- see `property_value_to_return_expr`, not this
510        // function. Erroring here (not silently dropping to Null) is the
511        // same "a real gap should say so, not produce a plausible-looking
512        // wrong answer" stance `apply_arith` already documents.
513        PropertyValue::Date(_)
514        | PropertyValue::Duration { .. }
515        | PropertyValue::LocalTime(_)
516        | PropertyValue::Time { .. }
517        | PropertyValue::LocalDateTime { .. }
518        | PropertyValue::DateTime { .. } => {
519            return Err(QueryError::Type(format!(
520                "${name}: passing a temporal value as a query parameter isn't supported in a \
521                 pattern-level property comparison (only in ordinary expression position)"
522            )))
523        }
524        // Same "no literal syntax to substitute to" gap as the temporal
525        // variants above -- there's no `Literal::List`. A list-valued
526        // `$param` used in ordinary expression position (`RETURN $x`,
527        // `WHERE y IN $x`, ...) substitutes into a `ReturnExpr::ListLit`
528        // instead (`property_value_to_return_expr`, used by
529        // `substitute_return_expr`'s own `Lit` arm, not this function) --
530        // reaching here at all means a list-valued param was used
531        // somewhere a bare `Literal` is structurally required (only
532        // pattern-level `Expr::Compare`'s RHS, `n.prop = $x`), which
533        // doesn't have an equivalent list-valued shape to fall back to
534        // either (comparing a scalar property against a list isn't
535        // meaningful).
536        PropertyValue::List(_) => {
537            return Err(QueryError::Type(format!(
538                "${name}: a list-valued query parameter can't be used here (only in ordinary \
539                 expression position, not a pattern-level property comparison)"
540            )))
541        }
542        // Same "no literal syntax to substitute to" gap as `List` above --
543        // there's no `Literal::Map` either. A map-valued `$param` used in
544        // ordinary expression position substitutes into a `ReturnExpr::
545        // MapLit` instead (`property_value_to_return_expr`, not this
546        // function).
547        PropertyValue::Map(_) => {
548            return Err(QueryError::Type(format!(
549                "${name}: a map-valued query parameter can't be used here (only in ordinary \
550                 expression position, not a pattern-level property comparison)"
551            )))
552        }
553    })
554}
555
556/// Converts a parameter's stored `PropertyValue` into the `ReturnExpr`
557/// that should replace a `ReturnExpr::Lit(Literal::Param(name))` node --
558/// a bare `Literal` for a scalar value (delegates to
559/// `property_value_to_literal`), a `ReturnExpr::ListLit` for a list value
560/// (recursively -- a param list can itself contain nested lists/maps), or
561/// a `ReturnExpr::MapLit` for a map value (same recursion, TCK's Map2/
562/// Map3, Unwind1 `[6]`/`[14]`).
563fn property_value_to_return_expr(name: &str, pv: &PropertyValue) -> Result<ReturnExpr, QueryError> {
564    Ok(match pv {
565        PropertyValue::List(items) => ReturnExpr::ListLit(
566            items
567                .iter()
568                .map(|item| property_value_to_return_expr(name, item))
569                .collect::<Result<Vec<_>, _>>()?,
570        ),
571        PropertyValue::Map(entries) => ReturnExpr::MapLit(
572            entries
573                .iter()
574                .map(|(key, value)| Ok((key.clone(), property_value_to_return_expr(name, value)?)))
575                .collect::<Result<Vec<_>, QueryError>>()?,
576        ),
577        // `property_value_to_literal`'s temporal arm can't represent these
578        // (no `Literal::Date`/etc -- there's no temporal *literal* syntax
579        // in Cypher). But ordinary expression position -- unlike a bare
580        // pattern-level `Literal` -- allows a `ReturnExpr::Call`, and every
581        // temporal constructor already accepts its own formatted string
582        // back (that's exactly what makes `toString()` round-trip), so a
583        // temporal-valued param becomes a call to the matching constructor
584        // over its formatted string here, instead of erroring.
585        PropertyValue::Date(d) => temporal_call("date", crate::temporal::format_date(*d)),
586        PropertyValue::Duration {
587            months,
588            days,
589            seconds,
590            nanos,
591        } => temporal_call(
592            "duration",
593            crate::temporal::format_duration(*months, *days, *seconds, *nanos),
594        ),
595        PropertyValue::LocalTime(nanos_of_day) => temporal_call(
596            "localtime",
597            crate::temporal::format_local_time(*nanos_of_day),
598        ),
599        PropertyValue::Time {
600            nanos_of_day,
601            offset_seconds,
602        } => temporal_call(
603            "time",
604            crate::temporal::format_time(*nanos_of_day, *offset_seconds),
605        ),
606        PropertyValue::LocalDateTime {
607            epoch_seconds,
608            nanos,
609        } => temporal_call(
610            "localdatetime",
611            crate::temporal::format_local_date_time(*epoch_seconds, *nanos),
612        ),
613        PropertyValue::DateTime {
614            epoch_seconds,
615            nanos,
616            zone,
617        } => temporal_call(
618            "datetime",
619            crate::temporal::format_date_time(
620                *epoch_seconds,
621                *nanos,
622                &crate::executor::tz_from_graph(zone),
623            ),
624        ),
625        other => ReturnExpr::Lit(property_value_to_literal(name, other)?),
626    })
627}
628
629fn temporal_call(name: &str, formatted: String) -> ReturnExpr {
630    ReturnExpr::Call {
631        name: name.to_string(),
632        args: vec![ReturnExpr::Lit(Literal::String(formatted))],
633        distinct: false,
634    }
635}