Skip to main content

akar_main/
prepared_statement.rs

1//! PreparedStatement — parameterized query support.
2//!
3//! Allows preparing a query once and executing it multiple times with
4//! different parameter values.
5
6use akar_binder::bound_statement::{BoundMatchClause, BoundStatement};
7use akar_common::types::Value;
8use akar_parser::ast::{Clause, Expression, Query, ReturnClause, ReturnItem, WhereClause};
9use akar_planner::logical_operator::LogicalOperator;
10use std::collections::HashMap;
11
12/// A prepared statement with a cached bound statement and logical plan.
13#[derive(Debug, Clone)]
14pub struct PreparedStatement {
15    /// The original query string.
16    pub query: String,
17    /// The bound statement after semantic analysis.
18    pub bound_statement: BoundStatement,
19    /// The logical plan (cached after first optimization).
20    pub logical_plan: Option<Vec<LogicalOperator>>,
21    /// Parameter names and their resolved types (None = unknown).
22    pub parameters: Vec<String>,
23}
24
25impl PreparedStatement {
26    pub fn new(query: String, bound_statement: BoundStatement) -> Self {
27        // Extract parameter names from the bound statement
28        let parameters = extract_parameters(&bound_statement);
29
30        Self {
31            query,
32            bound_statement,
33            logical_plan: None,
34            parameters,
35        }
36    }
37
38    /// Get the expected parameter names.
39    pub fn parameter_names(&self) -> &[String] {
40        &self.parameters
41    }
42
43    /// Number of expected parameters.
44    pub fn num_parameters(&self) -> usize {
45        self.parameters.len()
46    }
47}
48
49/// Extract parameter names from a bound statement by walking the expression tree.
50fn extract_parameters(bound: &BoundStatement) -> Vec<String> {
51    let mut params = Vec::new();
52    collect_params_from_statement(bound, &mut params);
53    params.sort();
54    params.dedup();
55    params
56}
57
58fn collect_params_from_statement(bound: &BoundStatement, params: &mut Vec<String>) {
59    match bound {
60        BoundStatement::BoundQuery(q) => {
61            for clause in &q.clauses {
62                match clause {
63                    akar_binder::bound_statement::BoundClause::BoundMatch(m)
64                    | akar_binder::bound_statement::BoundClause::BoundOptionalMatch(m)
65                    | akar_binder::bound_statement::BoundClause::BoundCreate(m) => {
66                        collect_params_from_match_clause(m, params);
67                    }
68                    akar_binder::bound_statement::BoundClause::BoundReturn(r)
69                    | akar_binder::bound_statement::BoundClause::BoundWith(r) => {
70                        for expr in &r.expressions {
71                            collect_params_from_expr(&expr.expression, params);
72                        }
73                        if let Some(order_by) = &r.order_by {
74                            for item in order_by {
75                                collect_params_from_expr(&item.expression.expression, params);
76                            }
77                        }
78                        if let Some(name) = &r.limit_param {
79                            params.push(name.clone());
80                        }
81                        if let Some(name) = &r.skip_param {
82                            params.push(name.clone());
83                        }
84                    }
85                    akar_binder::bound_statement::BoundClause::BoundWhere(w) => {
86                        collect_params_from_expr(&w.expression.expression, params);
87                    }
88                    akar_binder::bound_statement::BoundClause::BoundDelete(d) => {
89                        for item in &d.items {
90                            collect_params_from_expr(&item.expression, params);
91                        }
92                    }
93                    akar_binder::bound_statement::BoundClause::BoundSet(s) => {
94                        for item in &s.items {
95                            collect_params_from_expr(&item.property, params);
96                            collect_params_from_expr(&item.value, params);
97                        }
98                    }
99                    akar_binder::bound_statement::BoundClause::BoundUnwind(u) => {
100                        collect_params_from_expr(&u.expression, params);
101                    }
102                    akar_binder::bound_statement::BoundClause::BoundForeach(f) => {
103                        collect_params_from_expr(&f.expression, params);
104                        for sub in &f.sub_statements {
105                            collect_params_from_statement(sub, params);
106                        }
107                    }
108                    akar_binder::bound_statement::BoundClause::BoundMerge(m) => {
109                        for (_, v) in &m.properties {
110                            collect_params_from_expr(v, params);
111                        }
112                        for item in m.on_create.iter().chain(m.on_match.iter()) {
113                            collect_params_from_expr(&item.property, params);
114                            collect_params_from_expr(&item.value, params);
115                        }
116                    }
117                }
118            }
119        }
120        BoundStatement::BoundCreateDml(c) => {
121            for p in &c.patterns {
122                if let Some(n) = &p.node {
123                    for (_, v) in &n.properties {
124                        collect_params_from_expr(v, params);
125                    }
126                }
127                if let Some(e) = &p.edge {
128                    for (_, v) in &e.properties {
129                        collect_params_from_expr(v, params);
130                    }
131                }
132            }
133        }
134        BoundStatement::BoundMerge(m) => {
135            for (_, v) in &m.properties {
136                collect_params_from_expr(v, params);
137            }
138            for p in &m.patterns {
139                if let Some(n) = &p.node {
140                    for (_, v) in &n.properties {
141                        collect_params_from_expr(v, params);
142                    }
143                }
144                if let Some(e) = &p.edge {
145                    for (_, v) in &e.properties {
146                        collect_params_from_expr(v, params);
147                    }
148                }
149            }
150            for item in &m.on_create {
151                collect_params_from_expr(&item.property, params);
152                collect_params_from_expr(&item.value, params);
153            }
154            for item in &m.on_match {
155                collect_params_from_expr(&item.property, params);
156                collect_params_from_expr(&item.value, params);
157            }
158        }
159        BoundStatement::BoundExplain(e) => {
160            collect_params_from_statement(&e.inner, params);
161        }
162        _ => {}
163    }
164}
165
166fn collect_params_from_match_clause(m: &BoundMatchClause, params: &mut Vec<String>) {
167    for p in &m.patterns {
168        for (_, v) in &p.properties {
169            collect_params_from_expr(v, params);
170        }
171        if let Some(e) = &p.edge {
172            for (_, v) in &e.properties {
173                collect_params_from_expr(v, params);
174            }
175        }
176    }
177}
178
179fn collect_params_from_expr(expr: &Expression, params: &mut Vec<String>) {
180    match expr {
181        Expression::Parameter(name) => {
182            params.push(name.clone());
183        }
184        Expression::PropertyAccess(obj, _) => {
185            collect_params_from_expr(obj, params);
186        }
187        Expression::FunctionCall(_, args) => {
188            for arg in args {
189                collect_params_from_expr(arg, params);
190            }
191        }
192        Expression::BinaryOp(_, left, right) => {
193            collect_params_from_expr(left, params);
194            collect_params_from_expr(right, params);
195        }
196        Expression::UnaryOp(_, inner) => {
197            collect_params_from_expr(inner, params);
198        }
199        Expression::List(items) => {
200            for item in items {
201                collect_params_from_expr(item, params);
202            }
203        }
204        Expression::Map(entries) => {
205            for (_, val) in entries {
206                collect_params_from_expr(val, params);
207            }
208        }
209        Expression::Variable(_) | Expression::Constant(_) => {}
210        Expression::ExistsSubquery(q) => {
211            for clause in &q.clauses {
212                match clause {
213                    Clause::Where(w) => collect_params_from_expr(&w.expression, params),
214                    Clause::Return(r) => {
215                        for item in &r.expressions {
216                            collect_params_from_expr(&item.expression, params);
217                        }
218                    }
219                    _ => {}
220                }
221            }
222        }
223        Expression::Case(case_expr) => {
224            if let Some(subj) = &case_expr.subject {
225                collect_params_from_expr(subj, params);
226            }
227            for alt in &case_expr.alternatives {
228                collect_params_from_expr(&alt.when, params);
229                collect_params_from_expr(&alt.then, params);
230            }
231            if let Some(else_e) = &case_expr.else_expr {
232                collect_params_from_expr(else_e, params);
233            }
234        }
235        Expression::Star => {}
236        Expression::ListPredicate { list, predicate, .. } => {
237            collect_params_from_expr(list, params);
238            collect_params_from_expr(predicate, params);
239        }
240        Expression::Lambda { body, .. } => {
241            collect_params_from_expr(body, params);
242        }
243    }
244}
245
246/// Substitute parameter references with concrete values in an expression tree.
247pub fn substitute_params(expr: &Expression, param_values: &HashMap<String, Value>) -> Result<Expression, String> {
248    match expr {
249        Expression::Parameter(name) => {
250            let value = param_values
251                .get(name)
252                .ok_or_else(|| format!("Missing parameter: ${}", name))?;
253            Ok(value_to_expression(value)?)
254        }
255        Expression::PropertyAccess(obj, prop) => {
256            let new_obj = substitute_params(obj, param_values)?;
257            Ok(Expression::PropertyAccess(Box::new(new_obj), prop.clone()))
258        }
259        Expression::FunctionCall(name, args) => {
260            let new_args: Result<Vec<_>, _> = args.iter().map(|a| substitute_params(a, param_values)).collect();
261            Ok(Expression::FunctionCall(name.clone(), new_args?))
262        }
263        Expression::BinaryOp(op, left, right) => {
264            let new_left = substitute_params(left, param_values)?;
265            let new_right = substitute_params(right, param_values)?;
266            Ok(Expression::BinaryOp(*op, Box::new(new_left), Box::new(new_right)))
267        }
268        Expression::UnaryOp(op, inner) => {
269            let new_inner = substitute_params(inner, param_values)?;
270            Ok(Expression::UnaryOp(*op, Box::new(new_inner)))
271        }
272        Expression::List(items) => {
273            let new_items: Result<Vec<_>, _> = items.iter().map(|i| substitute_params(i, param_values)).collect();
274            Ok(Expression::List(new_items?))
275        }
276        Expression::Map(entries) => {
277            let new_entries: Result<Vec<(String, Expression)>, String> = entries
278                .iter()
279                .map(|(k, v)| Ok((k.clone(), substitute_params(v, param_values)?)))
280                .collect();
281            Ok(Expression::Map(new_entries?))
282        }
283        // Non-parameter expressions pass through
284        Expression::Variable(_) | Expression::Constant(_) => Ok(expr.clone()),
285        Expression::ExistsSubquery(q) => Ok(Expression::ExistsSubquery(Box::new(substitute_params_in_query(
286            q,
287            param_values,
288        )?))),
289        Expression::Case(case_expr) => {
290            use akar_parser::ast::{CaseAlternative, CaseExpr};
291            let subject = if let Some(subj) = &case_expr.subject {
292                Some(Box::new(substitute_params(subj, param_values)?))
293            } else {
294                None
295            };
296            let alternatives: Result<Vec<CaseAlternative>, String> = case_expr
297                .alternatives
298                .iter()
299                .map(|alt| {
300                    Ok(CaseAlternative {
301                        when: substitute_params(&alt.when, param_values)?,
302                        then: substitute_params(&alt.then, param_values)?,
303                    })
304                })
305                .collect();
306            let else_expr = if let Some(e) = &case_expr.else_expr {
307                Some(Box::new(substitute_params(e, param_values)?))
308            } else {
309                None
310            };
311            Ok(Expression::Case(CaseExpr {
312                subject,
313                alternatives: alternatives?,
314                else_expr,
315            }))
316        }
317        Expression::Star => Ok(expr.clone()),
318        Expression::ListPredicate {
319            quantifier,
320            list,
321            var_name,
322            predicate,
323        } => {
324            let new_list = substitute_params(list, param_values)?;
325            let new_predicate = substitute_params(predicate, param_values)?;
326            Ok(Expression::ListPredicate {
327                quantifier: *quantifier,
328                list: Box::new(new_list),
329                var_name: var_name.clone(),
330                predicate: Box::new(new_predicate),
331            })
332        }
333        Expression::Lambda { var_name, body } => {
334            let new_body = substitute_params(body, param_values)?;
335            Ok(Expression::Lambda {
336                var_name: var_name.clone(),
337                body: Box::new(new_body),
338            })
339        }
340    }
341}
342
343/// Substitute parameters in a Query's clauses.
344fn substitute_params_in_query(query: &Query, param_values: &HashMap<String, Value>) -> Result<Query, String> {
345    let mut new_clauses = Vec::new();
346    for clause in &query.clauses {
347        let new_clause = match clause {
348            Clause::Where(w) => {
349                let new_expr = substitute_params(&w.expression, param_values)?;
350                Clause::Where(WhereClause { expression: new_expr })
351            }
352            Clause::Return(r) => {
353                let new_items: Result<Vec<ReturnItem>, String> = r
354                    .expressions
355                    .iter()
356                    .map(|item| {
357                        let new_expr = substitute_params(&item.expression, param_values)?;
358                        Ok(ReturnItem {
359                            expression: new_expr,
360                            alias: item.alias.clone(),
361                        })
362                    })
363                    .collect();
364                Clause::Return(ReturnClause {
365                    expressions: new_items?,
366                    distinct: r.distinct,
367                    order_by: r.order_by.clone(),
368                    limit: r.limit,
369                    skip: r.skip,
370                    limit_param: r.limit_param.clone(),
371                    skip_param: r.skip_param.clone(),
372                })
373            }
374            other => other.clone(),
375        };
376        new_clauses.push(new_clause);
377    }
378    Ok(Query { clauses: new_clauses })
379}
380
381/// Convert a Value to a Constant for expression substitution.
382///
383/// Returns an error instead of silently corrupting the parameter: a
384/// `UInt64`/`Int128` that overflows `i64` or a type the parser cannot
385/// represent as a constant (Blob, Date, List, …) would previously become a
386/// wrong `Integer` or `Null` (P51.32).
387fn value_to_constant(value: &Value) -> Result<akar_parser::ast::Constant, String> {
388    use akar_parser::ast::Constant;
389    match value {
390        Value::Null => Ok(Constant::Null),
391        Value::Bool(b) => Ok(Constant::Bool(*b)),
392        Value::Int64(n) => Ok(Constant::Integer(*n)),
393        Value::Int32(n) => Ok(Constant::Integer(*n as i64)),
394        Value::Int16(n) => Ok(Constant::Integer(*n as i64)),
395        Value::Int8(n) => Ok(Constant::Integer(*n as i64)),
396        Value::UInt64(n) => i64::try_from(*n)
397            .map(Constant::Integer)
398            .map_err(|_| format!("UInt64 parameter {n} exceeds i64 range and cannot be used in a query")),
399        Value::UInt32(n) => Ok(Constant::Integer(*n as i64)),
400        Value::UInt16(n) => Ok(Constant::Integer(*n as i64)),
401        Value::UInt8(n) => Ok(Constant::Integer(*n as i64)),
402        Value::Int128(n) => i64::try_from(*n)
403            .map(Constant::Integer)
404            .map_err(|_| format!("Int128 parameter {n} exceeds i64 range and cannot be used in a query")),
405        Value::Double(f) => Ok(Constant::Float(*f)),
406        Value::Float(f) => Ok(Constant::Float(*f as f64)),
407        Value::String(s) => Ok(Constant::String(s.clone())),
408        Value::Blob(_) => Err("BLOB parameters are not supported in queries".into()),
409        other => Err(format!(
410            "Parameter type {:?} cannot be used in a query",
411            std::mem::discriminant(other)
412        )),
413    }
414}
415
416/// Convert an Akar [`Value`] to an AST [`Expression`].
417///
418/// Like [`value_to_constant`] but supports compound types (List) by
419/// producing `Expression::List` nodes.  Used by `substitute_params`
420/// when a parameter value is a list (e.g. embedding vector, ID array).
421fn value_to_expression(value: &Value) -> Result<akar_parser::ast::Expression, String> {
422    use akar_parser::ast::Expression;
423    match value {
424        Value::List(items) => {
425            let mut exprs = Vec::with_capacity(items.len());
426            for item in items {
427                exprs.push(value_to_expression(item)?);
428            }
429            Ok(Expression::List(exprs))
430        }
431        // JSON objects → Value::Struct → Expression::Map so UNWIND $batch AS
432        // row ... row.field works for batched row objects (P69).
433        Value::Struct(fields) => {
434            let mut entries = Vec::with_capacity(fields.len());
435            for (name, v) in fields {
436                entries.push((name.clone(), value_to_expression(v)?));
437            }
438            Ok(Expression::Map(entries))
439        }
440        Value::Map(pairs) => {
441            let mut entries = Vec::with_capacity(pairs.len());
442            for (k, v) in pairs {
443                let key = match k {
444                    Value::String(s) => s.clone(),
445                    other => return Err(format!("Map key {other:?} is not a string")),
446                };
447                entries.push((key, value_to_expression(v)?));
448            }
449            Ok(Expression::Map(entries))
450        }
451        // Scalars: delegate to value_to_constant → Expression::Constant
452        other => Ok(Expression::Constant(value_to_constant(other)?)),
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use akar_common::types::Value;
460    use akar_parser::ast::*;
461    use std::collections::HashMap;
462
463    #[test]
464    fn test_extract_parameters_simple() {
465        let expr = Expression::BinaryOp(
466            BinaryOp::GreaterThan,
467            Box::new(Expression::PropertyAccess(
468                Box::new(Expression::Variable("p".into())),
469                "age".into(),
470            )),
471            Box::new(Expression::Parameter("min_age".into())),
472        );
473        let mut params = Vec::new();
474        collect_params_from_expr(&expr, &mut params);
475        assert_eq!(params, vec!["min_age"]);
476    }
477
478    #[test]
479    fn test_extract_multiple_params() {
480        let expr = Expression::BinaryOp(
481            BinaryOp::And,
482            Box::new(Expression::BinaryOp(
483                BinaryOp::GreaterThan,
484                Box::new(Expression::PropertyAccess(
485                    Box::new(Expression::Variable("p".into())),
486                    "age".into(),
487                )),
488                Box::new(Expression::Parameter("min_age".into())),
489            )),
490            Box::new(Expression::BinaryOp(
491                BinaryOp::LessThan,
492                Box::new(Expression::PropertyAccess(
493                    Box::new(Expression::Variable("p".into())),
494                    "age".into(),
495                )),
496                Box::new(Expression::Parameter("max_age".into())),
497            )),
498        );
499        let mut params = Vec::new();
500        collect_params_from_expr(&expr, &mut params);
501        params.sort();
502        assert_eq!(params, vec!["max_age", "min_age"]);
503    }
504
505    #[test]
506    fn test_substitute_params() {
507        let expr = Expression::BinaryOp(
508            BinaryOp::Equal,
509            Box::new(Expression::Variable("p".into())),
510            Box::new(Expression::Parameter("name".into())),
511        );
512        let mut params = HashMap::new();
513        params.insert("name".into(), Value::String("Alice".into()));
514
515        let substituted = substitute_params(&expr, &params).unwrap();
516        match substituted {
517            Expression::BinaryOp(_, _, right) => match *right {
518                Expression::Constant(Constant::String(s)) => {
519                    assert_eq!(s, "Alice");
520                }
521                _ => panic!("Expected constant string"),
522            },
523            _ => panic!("Expected binary op"),
524        }
525    }
526
527    #[test]
528    fn test_substitute_missing_param() {
529        let expr = Expression::BinaryOp(
530            BinaryOp::Equal,
531            Box::new(Expression::Variable("p".into())),
532            Box::new(Expression::Parameter("missing".into())),
533        );
534        let params = HashMap::new();
535        assert!(substitute_params(&expr, &params).is_err());
536    }
537
538    #[test]
539    fn test_value_to_constant() {
540        assert_eq!(value_to_constant(&Value::Int64(42)), Ok(Constant::Integer(42)));
541        assert_eq!(
542            value_to_constant(&Value::String("hi".into())),
543            Ok(Constant::String("hi".into()))
544        );
545        assert_eq!(value_to_constant(&Value::Bool(true)), Ok(Constant::Bool(true)));
546        assert_eq!(value_to_constant(&Value::Double(3.15)), Ok(Constant::Float(3.15)));
547        assert_eq!(value_to_constant(&Value::Null), Ok(Constant::Null));
548    }
549
550    #[test]
551    fn test_value_to_constant_uint64_overflow_errors() {
552        // Values beyond i64::MAX must error instead of silently wrapping.
553        assert_eq!(
554            value_to_constant(&Value::UInt64(i64::MAX as u64 + 1)),
555            Err("UInt64 parameter 9223372036854775808 exceeds i64 range and cannot be used in a query".into())
556        );
557        assert_eq!(
558            value_to_constant(&Value::UInt64(u64::MAX)),
559            Err("UInt64 parameter 18446744073709551615 exceeds i64 range and cannot be used in a query".into())
560        );
561        // Fits — ok.
562        assert_eq!(
563            value_to_constant(&Value::UInt64(i64::MAX as u64)),
564            Ok(Constant::Integer(i64::MAX))
565        );
566    }
567
568    #[test]
569    fn test_value_to_constant_blob_errors() {
570        assert_eq!(
571            value_to_constant(&Value::Blob(vec![1, 2, 3])),
572            Err("BLOB parameters are not supported in queries".into())
573        );
574    }
575
576    #[test]
577    fn test_no_params() {
578        let expr = Expression::BinaryOp(
579            BinaryOp::Equal,
580            Box::new(Expression::Variable("a".into())),
581            Box::new(Expression::Variable("b".into())),
582        );
583        let mut params = Vec::new();
584        collect_params_from_expr(&expr, &mut params);
585        assert!(params.is_empty());
586    }
587
588    #[test]
589    fn test_value_to_expression_list() {
590        let val = Value::List(vec![Value::Int64(1), Value::Int64(2), Value::Int64(3)]);
591        let expr = value_to_expression(&val).unwrap();
592        match expr {
593            Expression::List(items) => {
594                assert_eq!(items.len(), 3);
595                // Each item should be a Constant(Integer)
596                for (i, item) in items.iter().enumerate() {
597                    match item {
598                        Expression::Constant(Constant::Integer(n)) => {
599                            assert_eq!(*n, (i + 1) as i64);
600                        }
601                        other => panic!("expected Integer constant at {i}, got {other:?}"),
602                    }
603                }
604            }
605            other => panic!("expected List, got {other:?}"),
606        }
607    }
608
609    #[test]
610    fn test_value_to_expression_nested_list() {
611        let val = Value::List(vec![
612            Value::List(vec![Value::Int64(1), Value::Int64(2)]),
613            Value::List(vec![Value::Int64(3), Value::Int64(4)]),
614        ]);
615        let expr = value_to_expression(&val).unwrap();
616        match expr {
617            Expression::List(outer) => {
618                assert_eq!(outer.len(), 2);
619                match &outer[0] {
620                    Expression::List(inner) => {
621                        assert_eq!(inner.len(), 2);
622                    }
623                    other => panic!("expected inner List, got {other:?}"),
624                }
625            }
626            other => panic!("expected List, got {other:?}"),
627        }
628    }
629}