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