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