Skip to main content

reddb_server/storage/query/planner/
shape.rs

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