Skip to main content

reddb_server/storage/query/planner/
shape.rs

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