Skip to main content

reddb_server/storage/query/planner/
shape.rs

1use crate::storage::engine::vector_metadata::{MetadataFilter, MetadataValue};
2use crate::storage::query::ast::{
3    Expr, FusionStrategy, GraphPattern, GraphQuery, HybridQuery, JoinQuery, NodePattern,
4    NodeSelector, OrderByClause, PathQuery, Projection, PropertyFilter, QueryExpr, SelectItem,
5    TableQuery, TableSource, VectorQuery, VectorSource,
6};
7use crate::storage::query::sql_lowering::{
8    expr_to_filter, filter_to_expr, projection_from_literal, PARAMETER_PROJECTION_PREFIX,
9};
10use crate::storage::schema::Value;
11
12const PROJECTION_PARAM_PREFIX: &str = "__shape_projection_param__:";
13const STRING_PARAM_PREFIX: &str = "__shape_string_param__:";
14const VALUE_PARAM_PREFIX: &str = "__shape_value_param__:";
15const ROW_SELECTOR_TABLE_PREFIX: &str = "__shape_row_selector__:";
16const METADATA_VALUE_PARAM_PREFIX: &str = "__shape_metadata_value_param__:";
17const VECTOR_TEXT_PARAM_PREFIX: &str = "__shape_vector_text_param__:";
18const VECTOR_REF_ID_PREFIX: &str = "__shape_vector_ref_id__:";
19const FLOAT32_PARAM_BITS_BASE: u32 = 0x7fc0_0000;
20const FLOAT64_PARAM_BITS_BASE: u64 = 0x7ff8_0000_0000_0000;
21const U32_PARAM_BASE: u32 = 0xfff0_0000;
22
23#[derive(Debug, Clone)]
24pub struct ParameterizedQuery {
25    pub shape: QueryExpr,
26    pub parameter_count: usize,
27}
28
29pub fn parameterize_query_expr(expr: &QueryExpr) -> Option<ParameterizedQuery> {
30    let mut next_index = 0usize;
31    let shape = parameterize_query_expr_inner(expr, &mut next_index)?;
32    Some(ParameterizedQuery {
33        shape,
34        parameter_count: next_index,
35    })
36}
37
38/// Bind user-supplied positional `$N` parameters into a freshly-parsed
39/// query expression. Unlike [`bind_parameterized_query`] — which is the
40/// other half of the auto-parameterized prepared-statement shape cache —
41/// this entry point assumes the parser already emitted `Expr::Parameter`
42/// nodes (one per `$N` reference) and just substitutes them in place.
43///
44/// Validation (arity, gaps) is performed by `user_params::validate`
45/// before this call; here we trust the slice indexes match the parser's
46/// 0-based slot numbering.
47pub fn bind_user_param_query(expr: &QueryExpr, params: &[Value]) -> Option<QueryExpr> {
48    bind_query_expr_inner(expr, params)
49}
50
51pub fn bind_parameterized_query(
52    expr: &QueryExpr,
53    binds: &[Value],
54    parameter_count: usize,
55) -> Option<QueryExpr> {
56    if binds.len() != parameter_count {
57        return None;
58    }
59    bind_query_expr_inner(expr, binds)
60}
61
62fn parameterize_query_expr_inner(expr: &QueryExpr, next_index: &mut usize) -> Option<QueryExpr> {
63    match expr {
64        QueryExpr::Table(query) => Some(QueryExpr::Table(parameterize_table_query(
65            query, next_index,
66        )?)),
67        QueryExpr::Join(query) => {
68            Some(QueryExpr::Join(parameterize_join_query(query, next_index)?))
69        }
70        QueryExpr::Graph(query) => Some(QueryExpr::Graph(parameterize_graph_query(
71            query, next_index,
72        )?)),
73        QueryExpr::Path(query) => {
74            Some(QueryExpr::Path(parameterize_path_query(query, next_index)?))
75        }
76        QueryExpr::Vector(query) => Some(QueryExpr::Vector(parameterize_vector_query(
77            query, next_index,
78        )?)),
79        QueryExpr::Hybrid(query) => Some(QueryExpr::Hybrid(parameterize_hybrid_query(
80            query, next_index,
81        )?)),
82        _ => None,
83    }
84}
85
86fn parameterize_table_query(query: &TableQuery, next_index: &mut usize) -> Option<TableQuery> {
87    let source = match &query.source {
88        Some(TableSource::Name(name)) => Some(TableSource::Name(name.clone())),
89        Some(TableSource::Subquery(inner)) => Some(TableSource::Subquery(Box::new(
90            parameterize_query_expr_inner(inner, next_index)?,
91        ))),
92        // Table-valued functions have no parameterizable subexpressions
93        // (identifier args only); preserve them verbatim (issue #795). The
94        // inline-graph form (issue #799) is likewise preserved verbatim — its
95        // subquery literals are not lifted into plan parameters.
96        Some(other @ (TableSource::Function { .. } | TableSource::InlineGraphFunction { .. })) => {
97            Some(other.clone())
98        }
99        None => None,
100    };
101
102    let select_items = query
103        .select_items
104        .iter()
105        .map(|item| parameterize_select_item(item, next_index))
106        .collect::<Option<Vec<_>>>()?;
107
108    let where_expr = query
109        .where_expr
110        .as_ref()
111        .map(|expr| parameterize_expr(expr, next_index))
112        .or_else(|| {
113            query
114                .filter
115                .as_ref()
116                .map(|filter| parameterize_expr(&filter_to_expr(filter), next_index))
117        });
118
119    let group_by_exprs = if !query.group_by_exprs.is_empty() {
120        query
121            .group_by_exprs
122            .iter()
123            .map(|expr| parameterize_expr(expr, next_index))
124            .collect()
125    } else {
126        Vec::new()
127    };
128
129    let having_expr = query
130        .having_expr
131        .as_ref()
132        .map(|expr| parameterize_expr(expr, next_index))
133        .or_else(|| {
134            query
135                .having
136                .as_ref()
137                .map(|filter| parameterize_expr(&filter_to_expr(filter), next_index))
138        });
139
140    let order_by = query
141        .order_by
142        .iter()
143        .map(|clause| parameterize_order_by(clause, next_index))
144        .collect::<Option<Vec<_>>>()?;
145
146    Some(TableQuery {
147        table: query.table.clone(),
148        source,
149        alias: query.alias.clone(),
150        select_items,
151        columns: Vec::new(),
152        where_expr,
153        filter: None,
154        group_by_exprs,
155        group_by: Vec::new(),
156        having_expr,
157        having: None,
158        order_by,
159        limit: query.limit,
160        limit_param: query.limit_param,
161        offset: query.offset,
162        offset_param: query.offset_param,
163        expand: query.expand.clone(),
164        as_of: query.as_of.clone(),
165        sessionize: query.sessionize.clone(),
166        distinct: query.distinct,
167    })
168}
169
170fn parameterize_select_item(item: &SelectItem, next_index: &mut usize) -> Option<SelectItem> {
171    match item {
172        SelectItem::Wildcard => Some(SelectItem::Wildcard),
173        SelectItem::Expr { expr, alias } => Some(SelectItem::Expr {
174            expr: parameterize_expr(expr, next_index),
175            alias: alias.clone(),
176        }),
177    }
178}
179
180fn parameterize_order_by(clause: &OrderByClause, next_index: &mut usize) -> Option<OrderByClause> {
181    Some(OrderByClause {
182        field: clause.field.clone(),
183        expr: clause
184            .expr
185            .as_ref()
186            .map(|expr| parameterize_expr(expr, next_index)),
187        ascending: clause.ascending,
188        nulls_first: clause.nulls_first,
189    })
190}
191
192fn parameterize_expr(expr: &Expr, next_index: &mut usize) -> Expr {
193    match expr {
194        Expr::Literal { value, span } => {
195            let index = *next_index;
196            *next_index += 1;
197            let _ = value;
198            Expr::Parameter { index, span: *span }
199        }
200        Expr::Column { .. } | Expr::Parameter { .. } => expr.clone(),
201        Expr::BinaryOp { op, lhs, rhs, span } => Expr::BinaryOp {
202            op: *op,
203            lhs: Box::new(parameterize_expr(lhs, next_index)),
204            rhs: Box::new(parameterize_expr(rhs, next_index)),
205            span: *span,
206        },
207        Expr::UnaryOp { op, operand, span } => Expr::UnaryOp {
208            op: *op,
209            operand: Box::new(parameterize_expr(operand, next_index)),
210            span: *span,
211        },
212        Expr::Cast {
213            inner,
214            target,
215            span,
216        } => Expr::Cast {
217            inner: Box::new(parameterize_expr(inner, next_index)),
218            target: *target,
219            span: *span,
220        },
221        Expr::FunctionCall { name, args, span } => Expr::FunctionCall {
222            name: name.clone(),
223            args: args
224                .iter()
225                .map(|arg| parameterize_expr(arg, next_index))
226                .collect(),
227            span: *span,
228        },
229        Expr::Case {
230            branches,
231            else_,
232            span,
233        } => Expr::Case {
234            branches: branches
235                .iter()
236                .map(|(cond, value)| {
237                    (
238                        parameterize_expr(cond, next_index),
239                        parameterize_expr(value, next_index),
240                    )
241                })
242                .collect(),
243            else_: else_
244                .as_ref()
245                .map(|expr| Box::new(parameterize_expr(expr, next_index))),
246            span: *span,
247        },
248        Expr::IsNull {
249            operand,
250            negated,
251            span,
252        } => Expr::IsNull {
253            operand: Box::new(parameterize_expr(operand, next_index)),
254            negated: *negated,
255            span: *span,
256        },
257        Expr::InList {
258            target,
259            values,
260            negated,
261            span,
262        } => Expr::InList {
263            target: Box::new(parameterize_expr(target, next_index)),
264            values: values
265                .iter()
266                .map(|value| parameterize_expr(value, next_index))
267                .collect(),
268            negated: *negated,
269            span: *span,
270        },
271        Expr::Between {
272            target,
273            low,
274            high,
275            negated,
276            span,
277        } => Expr::Between {
278            target: Box::new(parameterize_expr(target, next_index)),
279            low: Box::new(parameterize_expr(low, next_index)),
280            high: Box::new(parameterize_expr(high, next_index)),
281            negated: *negated,
282            span: *span,
283        },
284        Expr::Subquery { .. } => expr.clone(),
285        // Slice 7a (#589): preserve WindowFunctionCall shape unchanged
286        // for parameterization. The slice only ships parser/AST, so
287        // there are no parameter substitutions to thread through yet.
288        Expr::WindowFunctionCall { .. } => expr.clone(),
289    }
290}
291
292fn bind_query_expr_inner(expr: &QueryExpr, binds: &[Value]) -> Option<QueryExpr> {
293    match expr {
294        QueryExpr::Table(query) => Some(QueryExpr::Table(bind_table_query(query, binds)?)),
295        QueryExpr::Join(query) => Some(QueryExpr::Join(bind_join_query(query, binds)?)),
296        QueryExpr::Graph(query) => Some(QueryExpr::Graph(bind_graph_query(query, binds)?)),
297        QueryExpr::Path(query) => Some(QueryExpr::Path(bind_path_query(query, binds)?)),
298        QueryExpr::Vector(query) => Some(QueryExpr::Vector(bind_vector_query(query, binds)?)),
299        QueryExpr::Hybrid(query) => Some(QueryExpr::Hybrid(bind_hybrid_query(query, binds)?)),
300        _ => None,
301    }
302}
303
304fn parameterize_vector_query(query: &VectorQuery, next_index: &mut usize) -> Option<VectorQuery> {
305    Some(VectorQuery {
306        alias: query.alias.clone(),
307        collection: query.collection.clone(),
308        query_vector: parameterize_vector_source(&query.query_vector, next_index)?,
309        k: query.k,
310        filter: query
311            .filter
312            .as_ref()
313            .map(|filter| parameterize_metadata_filter(filter, next_index)),
314        metric: query.metric,
315        include_vectors: query.include_vectors,
316        include_metadata: query.include_metadata,
317        threshold: query
318            .threshold
319            .map(|_| encode_f32_placeholder(allocate_param_index(next_index))),
320    })
321}
322
323fn bind_vector_query(query: &VectorQuery, binds: &[Value]) -> Option<VectorQuery> {
324    Some(VectorQuery {
325        alias: query.alias.clone(),
326        collection: query.collection.clone(),
327        query_vector: bind_vector_source(&query.query_vector, binds)?,
328        k: query.k,
329        filter: query
330            .filter
331            .as_ref()
332            .and_then(|filter| bind_metadata_filter(filter, binds)),
333        metric: query.metric,
334        include_vectors: query.include_vectors,
335        include_metadata: query.include_metadata,
336        threshold: query
337            .threshold
338            .and_then(|value| bind_placeholder_f32(value, binds)),
339    })
340}
341
342fn parameterize_hybrid_query(query: &HybridQuery, next_index: &mut usize) -> Option<HybridQuery> {
343    Some(HybridQuery {
344        alias: query.alias.clone(),
345        structured: Box::new(parameterize_query_expr_inner(
346            &query.structured,
347            next_index,
348        )?),
349        vector: parameterize_vector_query(&query.vector, next_index)?,
350        fusion: parameterize_fusion_strategy(&query.fusion, next_index),
351        limit: query.limit,
352    })
353}
354
355fn bind_hybrid_query(query: &HybridQuery, binds: &[Value]) -> Option<HybridQuery> {
356    Some(HybridQuery {
357        alias: query.alias.clone(),
358        structured: Box::new(bind_query_expr_inner(&query.structured, binds)?),
359        vector: bind_vector_query(&query.vector, binds)?,
360        fusion: bind_fusion_strategy(&query.fusion, binds)?,
361        limit: query.limit,
362    })
363}
364
365fn parameterize_vector_source(
366    source: &VectorSource,
367    next_index: &mut usize,
368) -> Option<VectorSource> {
369    match source {
370        VectorSource::Literal(values) => Some(VectorSource::Literal(
371            values
372                .iter()
373                .map(|_| encode_f32_placeholder(allocate_param_index(next_index)))
374                .collect(),
375        )),
376        VectorSource::Text(_) => Some(VectorSource::Text(format!(
377            "{VECTOR_TEXT_PARAM_PREFIX}{}",
378            allocate_param_index(next_index)
379        ))),
380        VectorSource::Reference { collection, .. } => Some(VectorSource::Reference {
381            collection: format!(
382                "{VECTOR_REF_ID_PREFIX}{}:{collection}",
383                allocate_param_index(next_index)
384            ),
385            vector_id: 0,
386        }),
387        VectorSource::Subquery(expr) => Some(VectorSource::Subquery(Box::new(
388            parameterize_query_expr_inner(expr, next_index)?,
389        ))),
390    }
391}
392
393fn bind_vector_source(source: &VectorSource, binds: &[Value]) -> Option<VectorSource> {
394    match source {
395        VectorSource::Literal(values) => Some(VectorSource::Literal(
396            values
397                .iter()
398                .map(|value| bind_placeholder_f32(*value, binds))
399                .collect::<Option<Vec<_>>>()?,
400        )),
401        VectorSource::Text(text) => {
402            if let Some(index) = parse_placeholder_index(text, VECTOR_TEXT_PARAM_PREFIX) {
403                Some(VectorSource::Text(bind_value_to_string(binds.get(index)?)?))
404            } else {
405                Some(VectorSource::Text(text.clone()))
406            }
407        }
408        VectorSource::Reference {
409            collection,
410            vector_id,
411        } => {
412            if let Some((index, original_collection)) =
413                parse_prefixed_index_with_suffix(collection, VECTOR_REF_ID_PREFIX)
414            {
415                Some(VectorSource::Reference {
416                    collection: original_collection.to_string(),
417                    vector_id: bind_value_to_u64(binds.get(index)?)?,
418                })
419            } else {
420                Some(VectorSource::Reference {
421                    collection: collection.clone(),
422                    vector_id: *vector_id,
423                })
424            }
425        }
426        VectorSource::Subquery(expr) => Some(VectorSource::Subquery(Box::new(
427            bind_query_expr_inner(expr, binds)?,
428        ))),
429    }
430}
431
432fn parameterize_fusion_strategy(fusion: &FusionStrategy, next_index: &mut usize) -> FusionStrategy {
433    match fusion {
434        FusionStrategy::Rerank { .. } => FusionStrategy::Rerank {
435            weight: encode_f32_placeholder(allocate_param_index(next_index)),
436        },
437        FusionStrategy::FilterThenSearch => FusionStrategy::FilterThenSearch,
438        FusionStrategy::SearchThenFilter => FusionStrategy::SearchThenFilter,
439        FusionStrategy::RRF { .. } => FusionStrategy::RRF {
440            k: encode_u32_placeholder(allocate_param_index(next_index)),
441        },
442        FusionStrategy::Intersection => FusionStrategy::Intersection,
443        FusionStrategy::Union { .. } => FusionStrategy::Union {
444            structured_weight: encode_f32_placeholder(allocate_param_index(next_index)),
445            vector_weight: encode_f32_placeholder(allocate_param_index(next_index)),
446        },
447    }
448}
449
450fn bind_fusion_strategy(fusion: &FusionStrategy, binds: &[Value]) -> Option<FusionStrategy> {
451    match fusion {
452        FusionStrategy::Rerank { weight } => Some(FusionStrategy::Rerank {
453            weight: bind_placeholder_f32(*weight, binds)?,
454        }),
455        FusionStrategy::FilterThenSearch => Some(FusionStrategy::FilterThenSearch),
456        FusionStrategy::SearchThenFilter => Some(FusionStrategy::SearchThenFilter),
457        FusionStrategy::RRF { k } => Some(FusionStrategy::RRF {
458            k: bind_placeholder_u32(*k, binds)?,
459        }),
460        FusionStrategy::Intersection => Some(FusionStrategy::Intersection),
461        FusionStrategy::Union {
462            structured_weight,
463            vector_weight,
464        } => Some(FusionStrategy::Union {
465            structured_weight: bind_placeholder_f32(*structured_weight, binds)?,
466            vector_weight: bind_placeholder_f32(*vector_weight, binds)?,
467        }),
468    }
469}
470
471fn parameterize_metadata_filter(filter: &MetadataFilter, next_index: &mut usize) -> MetadataFilter {
472    match filter {
473        MetadataFilter::Eq(key, value) => {
474            MetadataFilter::Eq(key.clone(), parameterize_metadata_value(value, next_index))
475        }
476        MetadataFilter::Ne(key, value) => {
477            MetadataFilter::Ne(key.clone(), parameterize_metadata_value(value, next_index))
478        }
479        MetadataFilter::Gt(key, value) => {
480            MetadataFilter::Gt(key.clone(), parameterize_metadata_value(value, next_index))
481        }
482        MetadataFilter::Gte(key, value) => {
483            MetadataFilter::Gte(key.clone(), parameterize_metadata_value(value, next_index))
484        }
485        MetadataFilter::Lt(key, value) => {
486            MetadataFilter::Lt(key.clone(), parameterize_metadata_value(value, next_index))
487        }
488        MetadataFilter::Lte(key, value) => {
489            MetadataFilter::Lte(key.clone(), parameterize_metadata_value(value, next_index))
490        }
491        MetadataFilter::In(key, values) => MetadataFilter::In(
492            key.clone(),
493            values
494                .iter()
495                .map(|value| parameterize_metadata_value(value, next_index))
496                .collect(),
497        ),
498        MetadataFilter::NotIn(key, values) => MetadataFilter::NotIn(
499            key.clone(),
500            values
501                .iter()
502                .map(|value| parameterize_metadata_value(value, next_index))
503                .collect(),
504        ),
505        MetadataFilter::Contains(_, _) => MetadataFilter::Contains(
506            match filter {
507                MetadataFilter::Contains(key, _) => key.clone(),
508                _ => unreachable!(),
509            },
510            format!("{STRING_PARAM_PREFIX}{}", allocate_param_index(next_index)),
511        ),
512        MetadataFilter::StartsWith(_, _) => MetadataFilter::StartsWith(
513            match filter {
514                MetadataFilter::StartsWith(key, _) => key.clone(),
515                _ => unreachable!(),
516            },
517            format!("{STRING_PARAM_PREFIX}{}", allocate_param_index(next_index)),
518        ),
519        MetadataFilter::EndsWith(_, _) => MetadataFilter::EndsWith(
520            match filter {
521                MetadataFilter::EndsWith(key, _) => key.clone(),
522                _ => unreachable!(),
523            },
524            format!("{STRING_PARAM_PREFIX}{}", allocate_param_index(next_index)),
525        ),
526        MetadataFilter::GeoRadius {
527            key,
528            center_lat,
529            center_lon,
530            radius_km,
531        } => MetadataFilter::GeoRadius {
532            key: key.clone(),
533            center_lat: parameterize_f64(*center_lat, next_index),
534            center_lon: parameterize_f64(*center_lon, next_index),
535            radius_km: parameterize_f64(*radius_km, next_index),
536        },
537        MetadataFilter::Exists(key) => MetadataFilter::Exists(key.clone()),
538        MetadataFilter::NotExists(key) => MetadataFilter::NotExists(key.clone()),
539        MetadataFilter::And(filters) => MetadataFilter::And(
540            filters
541                .iter()
542                .map(|filter| parameterize_metadata_filter(filter, next_index))
543                .collect(),
544        ),
545        MetadataFilter::Or(filters) => MetadataFilter::Or(
546            filters
547                .iter()
548                .map(|filter| parameterize_metadata_filter(filter, next_index))
549                .collect(),
550        ),
551        MetadataFilter::Not(inner) => {
552            MetadataFilter::Not(Box::new(parameterize_metadata_filter(inner, next_index)))
553        }
554    }
555}
556
557fn bind_metadata_filter(filter: &MetadataFilter, binds: &[Value]) -> Option<MetadataFilter> {
558    match filter {
559        MetadataFilter::Eq(key, value) => Some(MetadataFilter::Eq(
560            key.clone(),
561            bind_metadata_value(value, binds)?,
562        )),
563        MetadataFilter::Ne(key, value) => Some(MetadataFilter::Ne(
564            key.clone(),
565            bind_metadata_value(value, binds)?,
566        )),
567        MetadataFilter::Gt(key, value) => Some(MetadataFilter::Gt(
568            key.clone(),
569            bind_metadata_value(value, binds)?,
570        )),
571        MetadataFilter::Gte(key, value) => Some(MetadataFilter::Gte(
572            key.clone(),
573            bind_metadata_value(value, binds)?,
574        )),
575        MetadataFilter::Lt(key, value) => Some(MetadataFilter::Lt(
576            key.clone(),
577            bind_metadata_value(value, binds)?,
578        )),
579        MetadataFilter::Lte(key, value) => Some(MetadataFilter::Lte(
580            key.clone(),
581            bind_metadata_value(value, binds)?,
582        )),
583        MetadataFilter::In(key, values) => Some(MetadataFilter::In(
584            key.clone(),
585            values
586                .iter()
587                .map(|value| bind_metadata_value(value, binds))
588                .collect::<Option<Vec<_>>>()?,
589        )),
590        MetadataFilter::NotIn(key, values) => Some(MetadataFilter::NotIn(
591            key.clone(),
592            values
593                .iter()
594                .map(|value| bind_metadata_value(value, binds))
595                .collect::<Option<Vec<_>>>()?,
596        )),
597        MetadataFilter::Contains(key, value) => Some(MetadataFilter::Contains(
598            key.clone(),
599            bind_placeholder_string(value, binds)?.unwrap_or_default(),
600        )),
601        MetadataFilter::StartsWith(key, value) => Some(MetadataFilter::StartsWith(
602            key.clone(),
603            bind_placeholder_string(value, binds)?.unwrap_or_default(),
604        )),
605        MetadataFilter::EndsWith(key, value) => Some(MetadataFilter::EndsWith(
606            key.clone(),
607            bind_placeholder_string(value, binds)?.unwrap_or_default(),
608        )),
609        MetadataFilter::GeoRadius {
610            key,
611            center_lat,
612            center_lon,
613            radius_km,
614        } => Some(MetadataFilter::GeoRadius {
615            key: key.clone(),
616            center_lat: bind_placeholder_f64(*center_lat, binds)?,
617            center_lon: bind_placeholder_f64(*center_lon, binds)?,
618            radius_km: bind_placeholder_f64(*radius_km, binds)?,
619        }),
620        MetadataFilter::Exists(key) => Some(MetadataFilter::Exists(key.clone())),
621        MetadataFilter::NotExists(key) => Some(MetadataFilter::NotExists(key.clone())),
622        MetadataFilter::And(filters) => Some(MetadataFilter::And(
623            filters
624                .iter()
625                .map(|filter| bind_metadata_filter(filter, binds))
626                .collect::<Option<Vec<_>>>()?,
627        )),
628        MetadataFilter::Or(filters) => Some(MetadataFilter::Or(
629            filters
630                .iter()
631                .map(|filter| bind_metadata_filter(filter, binds))
632                .collect::<Option<Vec<_>>>()?,
633        )),
634        MetadataFilter::Not(inner) => Some(MetadataFilter::Not(Box::new(bind_metadata_filter(
635            inner, binds,
636        )?))),
637    }
638}
639
640fn parameterize_metadata_value(_value: &MetadataValue, next_index: &mut usize) -> MetadataValue {
641    MetadataValue::String(format!(
642        "{METADATA_VALUE_PARAM_PREFIX}{}",
643        allocate_param_index(next_index)
644    ))
645}
646
647fn bind_metadata_value(value: &MetadataValue, binds: &[Value]) -> Option<MetadataValue> {
648    match value {
649        MetadataValue::String(text) => {
650            if let Some(index) = parse_placeholder_index(text, METADATA_VALUE_PARAM_PREFIX) {
651                Some(bind_value_to_metadata_value(binds.get(index)?)?)
652            } else {
653                Some(MetadataValue::String(text.clone()))
654            }
655        }
656        other => Some(other.clone()),
657    }
658}
659
660fn parameterize_join_query(query: &JoinQuery, next_index: &mut usize) -> Option<JoinQuery> {
661    Some(JoinQuery {
662        left: Box::new(parameterize_query_expr_inner(&query.left, next_index)?),
663        right: Box::new(parameterize_query_expr_inner(&query.right, next_index)?),
664        join_type: query.join_type,
665        on: query.on.clone(),
666        filter: query
667            .filter
668            .as_ref()
669            .map(|filter| parameterize_filter(filter, next_index)),
670        order_by: query
671            .order_by
672            .iter()
673            .map(|clause| parameterize_order_by(clause, next_index))
674            .collect::<Option<Vec<_>>>()?,
675        limit: query.limit,
676        offset: query.offset,
677        return_items: query
678            .return_items
679            .iter()
680            .map(|item| parameterize_select_item(item, next_index))
681            .collect::<Option<Vec<_>>>()?,
682        return_: query
683            .return_
684            .iter()
685            .map(|projection| parameterize_projection(projection, next_index))
686            .collect::<Option<Vec<_>>>()?,
687    })
688}
689
690fn bind_join_query(query: &JoinQuery, binds: &[Value]) -> Option<JoinQuery> {
691    Some(JoinQuery {
692        left: Box::new(bind_query_expr_inner(&query.left, binds)?),
693        right: Box::new(bind_query_expr_inner(&query.right, binds)?),
694        join_type: query.join_type,
695        on: query.on.clone(),
696        filter: query
697            .filter
698            .as_ref()
699            .and_then(|filter| bind_filter(filter, binds)),
700        order_by: query
701            .order_by
702            .iter()
703            .map(|clause| bind_order_by(clause, binds))
704            .collect::<Option<Vec<_>>>()?,
705        limit: query.limit,
706        offset: query.offset,
707        return_items: query
708            .return_items
709            .iter()
710            .map(|item| bind_select_item(item, binds))
711            .collect::<Option<Vec<_>>>()?,
712        return_: query
713            .return_
714            .iter()
715            .map(|projection| bind_projection(projection, binds))
716            .collect::<Option<Vec<_>>>()?,
717    })
718}
719
720fn parameterize_graph_query(query: &GraphQuery, next_index: &mut usize) -> Option<GraphQuery> {
721    Some(GraphQuery {
722        alias: query.alias.clone(),
723        pattern: parameterize_graph_pattern(&query.pattern, next_index),
724        filter: query
725            .filter
726            .as_ref()
727            .map(|filter| parameterize_filter(filter, next_index)),
728        return_: query
729            .return_
730            .iter()
731            .map(|projection| parameterize_projection(projection, next_index))
732            .collect::<Option<Vec<_>>>()?,
733        limit: query.limit,
734    })
735}
736
737fn bind_graph_query(query: &GraphQuery, binds: &[Value]) -> Option<GraphQuery> {
738    Some(GraphQuery {
739        alias: query.alias.clone(),
740        pattern: bind_graph_pattern(&query.pattern, binds)?,
741        filter: query
742            .filter
743            .as_ref()
744            .and_then(|filter| bind_filter(filter, binds)),
745        return_: query
746            .return_
747            .iter()
748            .map(|projection| bind_projection(projection, binds))
749            .collect::<Option<Vec<_>>>()?,
750        limit: query.limit,
751    })
752}
753
754fn parameterize_path_query(query: &PathQuery, next_index: &mut usize) -> Option<PathQuery> {
755    Some(PathQuery {
756        alias: query.alias.clone(),
757        from: parameterize_node_selector(&query.from, next_index),
758        to: parameterize_node_selector(&query.to, next_index),
759        via: query.via.clone(),
760        max_length: query.max_length,
761        filter: query
762            .filter
763            .as_ref()
764            .map(|filter| parameterize_filter(filter, next_index)),
765        return_: query
766            .return_
767            .iter()
768            .map(|projection| parameterize_projection(projection, next_index))
769            .collect::<Option<Vec<_>>>()?,
770    })
771}
772
773fn bind_path_query(query: &PathQuery, binds: &[Value]) -> Option<PathQuery> {
774    Some(PathQuery {
775        alias: query.alias.clone(),
776        from: bind_node_selector(&query.from, binds)?,
777        to: bind_node_selector(&query.to, binds)?,
778        via: query.via.clone(),
779        max_length: query.max_length,
780        filter: query
781            .filter
782            .as_ref()
783            .and_then(|filter| bind_filter(filter, binds)),
784        return_: query
785            .return_
786            .iter()
787            .map(|projection| bind_projection(projection, binds))
788            .collect::<Option<Vec<_>>>()?,
789    })
790}
791
792fn parameterize_filter(
793    filter: &crate::storage::query::ast::Filter,
794    next_index: &mut usize,
795) -> crate::storage::query::ast::Filter {
796    expr_to_filter(&parameterize_expr(&filter_to_expr(filter), next_index))
797}
798
799fn bind_filter(
800    filter: &crate::storage::query::ast::Filter,
801    binds: &[Value],
802) -> Option<crate::storage::query::ast::Filter> {
803    Some(expr_to_filter(&bind_expr(&filter_to_expr(filter), binds)?))
804}
805
806fn parameterize_projection(projection: &Projection, next_index: &mut usize) -> Option<Projection> {
807    match projection {
808        Projection::All => Some(Projection::All),
809        Projection::Column(column) => {
810            Some(parameterize_projection_column(column, None, next_index))
811        }
812        Projection::Alias(column, alias) => Some(parameterize_projection_column(
813            column,
814            Some(alias.as_str()),
815            next_index,
816        )),
817        Projection::Function(name, args) => Some(Projection::Function(
818            name.clone(),
819            args.iter()
820                .map(|arg| parameterize_projection(arg, next_index))
821                .collect::<Option<Vec<_>>>()?,
822        )),
823        Projection::Expression(filter, alias) => Some(Projection::Expression(
824            Box::new(parameterize_filter(filter, next_index)),
825            alias.clone(),
826        )),
827        Projection::Field(field, alias) => Some(Projection::Field(field.clone(), alias.clone())),
828        // Slice 7a (#589): window projections are preserved verbatim;
829        // they don't carry parameter slots yet.
830        Projection::Window { .. } => Some(projection.clone()),
831    }
832}
833
834fn bind_projection(projection: &Projection, binds: &[Value]) -> Option<Projection> {
835    match projection {
836        Projection::All => Some(Projection::All),
837        Projection::Column(column) => bind_projection_column(column, None, binds),
838        Projection::Alias(column, alias) => {
839            bind_projection_column(column, Some(alias.as_str()), binds)
840        }
841        Projection::Function(name, args) => Some(Projection::Function(
842            name.clone(),
843            args.iter()
844                .map(|arg| bind_projection(arg, binds))
845                .collect::<Option<Vec<_>>>()?,
846        )),
847        Projection::Expression(filter, alias) => Some(Projection::Expression(
848            Box::new(bind_filter(filter, binds)?),
849            alias.clone(),
850        )),
851        Projection::Field(field, alias) => Some(Projection::Field(field.clone(), alias.clone())),
852        Projection::Window { .. } => Some(projection.clone()),
853    }
854}
855
856fn parameterize_projection_column(
857    column: &str,
858    alias: Option<&str>,
859    next_index: &mut usize,
860) -> Projection {
861    if column.starts_with("LIT:") {
862        let index = *next_index;
863        *next_index += 1;
864        let placeholder = format!("{PROJECTION_PARAM_PREFIX}{index}");
865        if let Some(alias) = alias {
866            Projection::Alias(placeholder, alias.to_string())
867        } else {
868            Projection::Column(placeholder)
869        }
870    } else if let Some(alias) = alias {
871        Projection::Alias(column.to_string(), alias.to_string())
872    } else {
873        Projection::Column(column.to_string())
874    }
875}
876
877fn bind_projection_column(
878    column: &str,
879    alias: Option<&str>,
880    binds: &[Value],
881) -> Option<Projection> {
882    if let Some(index) = parse_placeholder_index(column, PROJECTION_PARAM_PREFIX) {
883        let projection = projection_from_literal(binds.get(index)?)?;
884        Some(attach_projection_alias(projection, alias))
885    } else if let Some(index) = parse_placeholder_index(column, PARAMETER_PROJECTION_PREFIX) {
886        let projection = projection_from_literal(binds.get(index)?)?;
887        Some(attach_projection_alias(projection, alias))
888    } else if let Some(alias) = alias {
889        Some(Projection::Alias(column.to_string(), alias.to_string()))
890    } else {
891        Some(Projection::Column(column.to_string()))
892    }
893}
894
895fn parameterize_graph_pattern(pattern: &GraphPattern, next_index: &mut usize) -> GraphPattern {
896    GraphPattern {
897        nodes: pattern
898            .nodes
899            .iter()
900            .map(|node| parameterize_node_pattern(node, next_index))
901            .collect(),
902        edges: pattern.edges.clone(),
903    }
904}
905
906fn bind_graph_pattern(pattern: &GraphPattern, binds: &[Value]) -> Option<GraphPattern> {
907    Some(GraphPattern {
908        nodes: pattern
909            .nodes
910            .iter()
911            .map(|node| bind_node_pattern(node, binds))
912            .collect::<Option<Vec<_>>>()?,
913        edges: pattern.edges.clone(),
914    })
915}
916
917fn parameterize_node_pattern(node: &NodePattern, next_index: &mut usize) -> NodePattern {
918    NodePattern {
919        alias: node.alias.clone(),
920        node_label: node.node_label.clone(),
921        properties: node
922            .properties
923            .iter()
924            .map(|property| parameterize_property_filter(property, next_index))
925            .collect(),
926    }
927}
928
929fn bind_node_pattern(node: &NodePattern, binds: &[Value]) -> Option<NodePattern> {
930    Some(NodePattern {
931        alias: node.alias.clone(),
932        node_label: node.node_label.clone(),
933        properties: node
934            .properties
935            .iter()
936            .map(|property| bind_property_filter(property, binds))
937            .collect::<Option<Vec<_>>>()?,
938    })
939}
940
941fn parameterize_property_filter(filter: &PropertyFilter, next_index: &mut usize) -> PropertyFilter {
942    PropertyFilter {
943        name: filter.name.clone(),
944        op: filter.op,
945        value: parameterize_value_placeholder(next_index),
946    }
947}
948
949fn bind_property_filter(filter: &PropertyFilter, binds: &[Value]) -> Option<PropertyFilter> {
950    Some(PropertyFilter {
951        name: filter.name.clone(),
952        op: filter.op,
953        value: bind_value_placeholder(&filter.value, binds)?,
954    })
955}
956
957fn parameterize_node_selector(selector: &NodeSelector, next_index: &mut usize) -> NodeSelector {
958    match selector {
959        NodeSelector::ById(_) => {
960            let index = *next_index;
961            *next_index += 1;
962            NodeSelector::ById(format!("{STRING_PARAM_PREFIX}{index}"))
963        }
964        NodeSelector::ByType { node_label, filter } => NodeSelector::ByType {
965            node_label: node_label.clone(),
966            filter: filter
967                .as_ref()
968                .map(|filter| parameterize_property_filter(filter, next_index)),
969        },
970        NodeSelector::ByRow { table, .. } => {
971            let index = *next_index;
972            *next_index += 1;
973            NodeSelector::ByRow {
974                table: format!("{ROW_SELECTOR_TABLE_PREFIX}{index}:{table}"),
975                row_id: 0,
976            }
977        }
978    }
979}
980
981fn bind_node_selector(selector: &NodeSelector, binds: &[Value]) -> Option<NodeSelector> {
982    match selector {
983        NodeSelector::ById(id) => {
984            if let Some(index) = parse_placeholder_index(id, STRING_PARAM_PREFIX) {
985                Some(NodeSelector::ById(bind_value_to_string(binds.get(index)?)?))
986            } else {
987                Some(NodeSelector::ById(id.clone()))
988            }
989        }
990        NodeSelector::ByType { node_label, filter } => Some(NodeSelector::ByType {
991            node_label: node_label.clone(),
992            filter: filter
993                .as_ref()
994                .and_then(|filter| bind_property_filter(filter, binds)),
995        }),
996        NodeSelector::ByRow { table, row_id } => {
997            if let Some((index, original_table)) = parse_row_selector_placeholder(table) {
998                Some(NodeSelector::ByRow {
999                    table: original_table.to_string(),
1000                    row_id: bind_value_to_u64(binds.get(index)?)?,
1001                })
1002            } else {
1003                Some(NodeSelector::ByRow {
1004                    table: table.clone(),
1005                    row_id: *row_id,
1006                })
1007            }
1008        }
1009    }
1010}
1011
1012fn parameterize_value_placeholder(next_index: &mut usize) -> Value {
1013    let index = *next_index;
1014    *next_index += 1;
1015    Value::text(format!("{VALUE_PARAM_PREFIX}{index}"))
1016}
1017
1018fn bind_value_placeholder(value: &Value, binds: &[Value]) -> Option<Value> {
1019    match value {
1020        Value::Text(text) => {
1021            if let Some(index) = parse_placeholder_index(text, VALUE_PARAM_PREFIX) {
1022                binds.get(index).cloned()
1023            } else {
1024                Some(value.clone())
1025            }
1026        }
1027        _ => Some(value.clone()),
1028    }
1029}
1030
1031fn attach_projection_alias(projection: Projection, alias: Option<&str>) -> Projection {
1032    let Some(alias) = alias else {
1033        return projection;
1034    };
1035    match projection {
1036        Projection::Field(field, _) => Projection::Field(field, Some(alias.to_string())),
1037        Projection::Expression(filter, _) => {
1038            Projection::Expression(filter, Some(alias.to_string()))
1039        }
1040        Projection::Function(name, args) => {
1041            if name.contains(':') {
1042                Projection::Function(name, args)
1043            } else {
1044                Projection::Function(format!("{name}:{alias}"), args)
1045            }
1046        }
1047        Projection::Column(column) => Projection::Alias(column, alias.to_string()),
1048        Projection::Alias(column, _) => Projection::Alias(column, alias.to_string()),
1049        Projection::All => Projection::All,
1050        Projection::Window {
1051            name, args, window, ..
1052        } => Projection::Window {
1053            name,
1054            args,
1055            window,
1056            alias: Some(alias.to_string()),
1057        },
1058    }
1059}
1060
1061fn bind_value_to_string(value: &Value) -> Option<String> {
1062    match value {
1063        Value::Null => None,
1064        _ => Some(value.to_string()),
1065    }
1066}
1067
1068fn bind_placeholder_string(value: &str, binds: &[Value]) -> Option<Option<String>> {
1069    if let Some(index) = parse_placeholder_index(value, STRING_PARAM_PREFIX) {
1070        Some(bind_value_to_string(binds.get(index)?))
1071    } else {
1072        Some(Some(value.to_string()))
1073    }
1074}
1075
1076fn bind_value_to_u64(value: &Value) -> Option<u64> {
1077    match value {
1078        Value::UnsignedInteger(value) => Some(*value),
1079        Value::Integer(value) if *value >= 0 => Some(*value as u64),
1080        Value::BigInt(value) if *value >= 0 => Some(*value as u64),
1081        Value::Text(value) => value.parse().ok(),
1082        _ => None,
1083    }
1084}
1085
1086fn parse_placeholder_index(value: &str, prefix: &str) -> Option<usize> {
1087    value.strip_prefix(prefix)?.parse().ok()
1088}
1089
1090fn parse_prefixed_index_with_suffix<'a>(value: &'a str, prefix: &str) -> Option<(usize, &'a str)> {
1091    let rest = value.strip_prefix(prefix)?;
1092    let (index, suffix) = rest.split_once(':')?;
1093    Some((index.parse().ok()?, suffix))
1094}
1095
1096fn parse_row_selector_placeholder(value: &str) -> Option<(usize, &str)> {
1097    let rest = value.strip_prefix(ROW_SELECTOR_TABLE_PREFIX)?;
1098    let (index, table) = rest.split_once(':')?;
1099    Some((index.parse().ok()?, table))
1100}
1101
1102fn allocate_param_index(next_index: &mut usize) -> usize {
1103    let index = *next_index;
1104    *next_index += 1;
1105    index
1106}
1107
1108fn encode_f32_placeholder(index: usize) -> f32 {
1109    f32::from_bits(FLOAT32_PARAM_BITS_BASE | (index as u32 & 0x003f_ffff))
1110}
1111
1112fn decode_f32_placeholder(value: f32) -> Option<usize> {
1113    let bits = value.to_bits();
1114    if bits & FLOAT32_PARAM_BITS_BASE == FLOAT32_PARAM_BITS_BASE {
1115        Some((bits & 0x003f_ffff) as usize)
1116    } else {
1117        None
1118    }
1119}
1120
1121fn bind_placeholder_f32(value: f32, binds: &[Value]) -> Option<f32> {
1122    if let Some(index) = decode_f32_placeholder(value) {
1123        bind_value_to_f32(binds.get(index)?)
1124    } else {
1125        Some(value)
1126    }
1127}
1128
1129fn parameterize_f64(value: f64, next_index: &mut usize) -> f64 {
1130    if value.is_finite() {
1131        encode_f64_placeholder(allocate_param_index(next_index))
1132    } else {
1133        value
1134    }
1135}
1136
1137fn encode_f64_placeholder(index: usize) -> f64 {
1138    f64::from_bits(FLOAT64_PARAM_BITS_BASE | (index as u64 & 0x0007_ffff_ffff_ffff))
1139}
1140
1141fn decode_f64_placeholder(value: f64) -> Option<usize> {
1142    let bits = value.to_bits();
1143    if bits & FLOAT64_PARAM_BITS_BASE == FLOAT64_PARAM_BITS_BASE {
1144        Some((bits & 0x0007_ffff_ffff_ffff) as usize)
1145    } else {
1146        None
1147    }
1148}
1149
1150fn bind_placeholder_f64(value: f64, binds: &[Value]) -> Option<f64> {
1151    if let Some(index) = decode_f64_placeholder(value) {
1152        bind_value_to_f64(binds.get(index)?)
1153    } else {
1154        Some(value)
1155    }
1156}
1157
1158fn encode_u32_placeholder(index: usize) -> u32 {
1159    U32_PARAM_BASE | (index as u32 & 0x000f_ffff)
1160}
1161
1162fn decode_u32_placeholder(value: u32) -> Option<usize> {
1163    if value & 0xfff0_0000 == U32_PARAM_BASE {
1164        Some((value & 0x000f_ffff) as usize)
1165    } else {
1166        None
1167    }
1168}
1169
1170fn bind_placeholder_u32(value: u32, binds: &[Value]) -> Option<u32> {
1171    if let Some(index) = decode_u32_placeholder(value) {
1172        bind_value_to_u64(binds.get(index)?).and_then(|value| u32::try_from(value).ok())
1173    } else {
1174        Some(value)
1175    }
1176}
1177
1178fn bind_value_to_f32(value: &Value) -> Option<f32> {
1179    match value {
1180        Value::Float(value) => Some(*value as f32),
1181        Value::Integer(value) => Some(*value as f32),
1182        Value::UnsignedInteger(value) => Some(*value as f32),
1183        Value::BigInt(value) => Some(*value as f32),
1184        Value::Text(value) => value.parse().ok(),
1185        _ => None,
1186    }
1187}
1188
1189fn bind_value_to_f64(value: &Value) -> Option<f64> {
1190    match value {
1191        Value::Float(value) => Some(*value),
1192        Value::Integer(value) => Some(*value as f64),
1193        Value::UnsignedInteger(value) => Some(*value as f64),
1194        Value::BigInt(value) => Some(*value as f64),
1195        Value::Text(value) => value.parse().ok(),
1196        _ => None,
1197    }
1198}
1199
1200fn bind_value_to_metadata_value(value: &Value) -> Option<MetadataValue> {
1201    match value {
1202        Value::Text(value) => Some(MetadataValue::String(value.to_string())),
1203        Value::Integer(value) => Some(MetadataValue::Integer(*value)),
1204        Value::UnsignedInteger(value) => i64::try_from(*value).ok().map(MetadataValue::Integer),
1205        Value::BigInt(value) => Some(MetadataValue::Integer(*value)),
1206        Value::Float(value) => Some(MetadataValue::Float(*value)),
1207        Value::Boolean(value) => Some(MetadataValue::Bool(*value)),
1208        Value::Null => Some(MetadataValue::Null),
1209        _ => None,
1210    }
1211}
1212
1213fn bind_table_query(query: &TableQuery, binds: &[Value]) -> Option<TableQuery> {
1214    let source = match &query.source {
1215        Some(TableSource::Name(name)) => Some(TableSource::Name(name.clone())),
1216        Some(TableSource::Subquery(inner)) => Some(TableSource::Subquery(Box::new(
1217            bind_query_expr_inner(inner, binds)?,
1218        ))),
1219        // Table-valued functions take identifier args only — nothing to bind;
1220        // preserve them verbatim (issue #795). The inline-graph form (issue
1221        // #799) is preserved verbatim for the same reason (no plan params).
1222        Some(other @ (TableSource::Function { .. } | TableSource::InlineGraphFunction { .. })) => {
1223            Some(other.clone())
1224        }
1225        None => None,
1226    };
1227
1228    Some(TableQuery {
1229        table: query.table.clone(),
1230        source,
1231        alias: query.alias.clone(),
1232        select_items: query
1233            .select_items
1234            .iter()
1235            .map(|item| bind_select_item(item, binds))
1236            .collect::<Option<Vec<_>>>()?,
1237        columns: query
1238            .columns
1239            .iter()
1240            .map(|projection| bind_projection(projection, binds))
1241            .collect::<Option<Vec<_>>>()?,
1242        where_expr: query
1243            .where_expr
1244            .as_ref()
1245            .and_then(|expr| bind_expr(expr, binds)),
1246        filter: None,
1247        group_by_exprs: query
1248            .group_by_exprs
1249            .iter()
1250            .map(|expr| bind_expr(expr, binds))
1251            .collect::<Option<Vec<_>>>()?,
1252        group_by: Vec::new(),
1253        having_expr: query
1254            .having_expr
1255            .as_ref()
1256            .and_then(|expr| bind_expr(expr, binds)),
1257        having: None,
1258        order_by: query
1259            .order_by
1260            .iter()
1261            .map(|clause| bind_order_by(clause, binds))
1262            .collect::<Option<Vec<_>>>()?,
1263        limit: query.limit,
1264        limit_param: query.limit_param,
1265        offset: query.offset,
1266        offset_param: query.offset_param,
1267        expand: query.expand.clone(),
1268        as_of: query.as_of.clone(),
1269        sessionize: query.sessionize.clone(),
1270        distinct: query.distinct,
1271    })
1272}
1273
1274fn bind_select_item(item: &SelectItem, binds: &[Value]) -> Option<SelectItem> {
1275    match item {
1276        SelectItem::Wildcard => Some(SelectItem::Wildcard),
1277        SelectItem::Expr { expr, alias } => Some(SelectItem::Expr {
1278            expr: bind_expr(expr, binds)?,
1279            alias: alias.clone(),
1280        }),
1281    }
1282}
1283
1284fn bind_order_by(clause: &OrderByClause, binds: &[Value]) -> Option<OrderByClause> {
1285    Some(OrderByClause {
1286        field: clause.field.clone(),
1287        expr: clause.expr.as_ref().and_then(|expr| bind_expr(expr, binds)),
1288        ascending: clause.ascending,
1289        nulls_first: clause.nulls_first,
1290    })
1291}
1292
1293fn bind_expr(expr: &Expr, binds: &[Value]) -> Option<Expr> {
1294    match expr {
1295        Expr::Literal { .. } | Expr::Column { .. } => Some(expr.clone()),
1296        Expr::Parameter { index, span } => Some(Expr::Literal {
1297            value: binds.get(*index)?.clone(),
1298            span: *span,
1299        }),
1300        Expr::BinaryOp { op, lhs, rhs, span } => Some(Expr::BinaryOp {
1301            op: *op,
1302            lhs: Box::new(bind_expr(lhs, binds)?),
1303            rhs: Box::new(bind_expr(rhs, binds)?),
1304            span: *span,
1305        }),
1306        Expr::UnaryOp { op, operand, span } => Some(Expr::UnaryOp {
1307            op: *op,
1308            operand: Box::new(bind_expr(operand, binds)?),
1309            span: *span,
1310        }),
1311        Expr::Cast {
1312            inner,
1313            target,
1314            span,
1315        } => Some(Expr::Cast {
1316            inner: Box::new(bind_expr(inner, binds)?),
1317            target: *target,
1318            span: *span,
1319        }),
1320        Expr::FunctionCall { name, args, span } => Some(Expr::FunctionCall {
1321            name: name.clone(),
1322            args: args
1323                .iter()
1324                .map(|arg| bind_expr(arg, binds))
1325                .collect::<Option<Vec<_>>>()?,
1326            span: *span,
1327        }),
1328        Expr::Case {
1329            branches,
1330            else_,
1331            span,
1332        } => Some(Expr::Case {
1333            branches: branches
1334                .iter()
1335                .map(|(cond, value)| Some((bind_expr(cond, binds)?, bind_expr(value, binds)?)))
1336                .collect::<Option<Vec<_>>>()?,
1337            else_: else_
1338                .as_ref()
1339                .and_then(|expr| bind_expr(expr, binds).map(Box::new)),
1340            span: *span,
1341        }),
1342        Expr::IsNull {
1343            operand,
1344            negated,
1345            span,
1346        } => Some(Expr::IsNull {
1347            operand: Box::new(bind_expr(operand, binds)?),
1348            negated: *negated,
1349            span: *span,
1350        }),
1351        Expr::InList {
1352            target,
1353            values,
1354            negated,
1355            span,
1356        } => Some(Expr::InList {
1357            target: Box::new(bind_expr(target, binds)?),
1358            values: values
1359                .iter()
1360                .map(|value| bind_expr(value, binds))
1361                .collect::<Option<Vec<_>>>()?,
1362            negated: *negated,
1363            span: *span,
1364        }),
1365        Expr::Between {
1366            target,
1367            low,
1368            high,
1369            negated,
1370            span,
1371        } => Some(Expr::Between {
1372            target: Box::new(bind_expr(target, binds)?),
1373            low: Box::new(bind_expr(low, binds)?),
1374            high: Box::new(bind_expr(high, binds)?),
1375            negated: *negated,
1376            span: *span,
1377        }),
1378        Expr::Subquery { .. } => Some(expr.clone()),
1379        Expr::WindowFunctionCall { .. } => Some(expr.clone()),
1380    }
1381}
1382
1383#[cfg(test)]
1384mod tests {
1385    use super::*;
1386    use crate::storage::query::ast::{BinOp, FieldRef, SelectItem, TableQuery};
1387    use crate::storage::query::modes::parse_multi;
1388
1389    #[test]
1390    fn table_shape_round_trips_with_new_binds() {
1391        let query = QueryExpr::Table(TableQuery {
1392            table: "users".to_string(),
1393            source: None,
1394            alias: None,
1395            select_items: vec![SelectItem::Expr {
1396                expr: Expr::Column {
1397                    field: FieldRef::TableColumn {
1398                        table: String::new(),
1399                        column: "name".to_string(),
1400                    },
1401                    span: crate::storage::query::ast::Span::synthetic(),
1402                },
1403                alias: None,
1404            }],
1405            columns: Vec::new(),
1406            where_expr: Some(Expr::BinaryOp {
1407                op: BinOp::Eq,
1408                lhs: Box::new(Expr::Column {
1409                    field: FieldRef::TableColumn {
1410                        table: String::new(),
1411                        column: "age".to_string(),
1412                    },
1413                    span: crate::storage::query::ast::Span::synthetic(),
1414                }),
1415                rhs: Box::new(Expr::Literal {
1416                    value: Value::Integer(18),
1417                    span: crate::storage::query::ast::Span::synthetic(),
1418                }),
1419                span: crate::storage::query::ast::Span::synthetic(),
1420            }),
1421            filter: None,
1422            group_by_exprs: Vec::new(),
1423            group_by: Vec::new(),
1424            having_expr: None,
1425            having: None,
1426            order_by: Vec::new(),
1427            limit: None,
1428            limit_param: None,
1429            offset: None,
1430            offset_param: None,
1431            expand: None,
1432            as_of: None,
1433            sessionize: None,
1434            distinct: false,
1435        });
1436
1437        let prepared = parameterize_query_expr(&query).unwrap();
1438        assert_eq!(prepared.parameter_count, 1);
1439
1440        let rebound = bind_parameterized_query(
1441            &prepared.shape,
1442            &[Value::Integer(42)],
1443            prepared.parameter_count,
1444        )
1445        .unwrap();
1446
1447        let QueryExpr::Table(bound_table) = rebound else {
1448            panic!("expected table query");
1449        };
1450        match bound_table.where_expr.unwrap() {
1451            Expr::BinaryOp { rhs, .. } => match *rhs {
1452                Expr::Literal { value, .. } => assert_eq!(value, Value::Integer(42)),
1453                other => panic!("expected rebound literal, got {other:?}"),
1454            },
1455            other => panic!("expected binary op, got {other:?}"),
1456        }
1457    }
1458
1459    #[test]
1460    fn user_param_binding_preserves_literal_projection_columns() {
1461        let query = parse_multi("SELECT $1").unwrap();
1462        let rebound = bind_user_param_query(&query, &[Value::Integer(42)]).unwrap();
1463        let QueryExpr::Table(bound_table) = rebound else {
1464            panic!("expected table query");
1465        };
1466        assert_eq!(
1467            bound_table.columns,
1468            vec![Projection::Column("LIT:42".into())]
1469        );
1470    }
1471}