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