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