1use hawdb_core::{HawDBError, Result, Value};
2use hawdb_cypher as cypher;
3use hawdb_plan_cypher::{
4 self as planner, GraphMatchStep, LogicalPlan, PhysicalPlan, Predicate, Projection,
5 ProjectionExpression, RelationshipCountFilter, SortItem, SortKey,
6};
7use std::collections::BTreeMap;
8
9const PARAMETER_SLOT_NAME_KEY: &str = "\0skein_parameter_slot";
10const PARAMETER_SLOT_PATH_KEY: &str = "\0skein_parameter_path";
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13enum ParameterCacheValue {
14 Slot(ParameterValueShape),
15 Exact(Value),
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
19enum ParameterValueShape {
20 Null,
21 Bool,
22 Int,
23 Float,
24 String,
25 Binary,
26 Uuid,
27 List(Vec<ParameterValueShape>),
28 Map(BTreeMap<String, ParameterValueShape>),
29}
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct ParameterizedLogicalPlan {
33 logical: LogicalPlan,
34 cache_key: PlanParameterCacheKey,
35 slot_count: usize,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct PlanParameterCacheKey {
40 values: BTreeMap<String, ParameterCacheValue>,
41}
42
43impl ParameterizedLogicalPlan {
44 pub fn logical(&self) -> &LogicalPlan {
45 &self.logical
46 }
47
48 pub fn cache_key(&self) -> &PlanParameterCacheKey {
49 &self.cache_key
50 }
51
52 pub fn slot_count(&self) -> usize {
53 self.slot_count
54 }
55
56 pub fn exact_variant_count(&self) -> usize {
57 self.cache_key.values.len() - self.slot_count
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62enum MarkerUse {
63 None,
64 Safe,
65 Unsafe,
66}
67
68impl MarkerUse {
69 fn combine(self, other: Self) -> Self {
70 match (self, other) {
71 (Self::Unsafe, _) | (_, Self::Unsafe) => Self::Unsafe,
72 (Self::Safe, _) | (_, Self::Safe) => Self::Safe,
73 (Self::None, Self::None) => Self::None,
74 }
75 }
76}
77
78pub fn parameterize_logical_plan(
79 statement: &cypher::Statement,
80 parameters: &BTreeMap<String, Value>,
81) -> Result<ParameterizedLogicalPlan> {
82 let actual = planner::plan_with_params(statement, parameters)?;
83 if parameters.values().any(value_contains_any_marker) {
84 return Ok(ParameterizedLogicalPlan {
85 logical: actual,
86 cache_key: PlanParameterCacheKey {
87 values: parameters
88 .iter()
89 .map(|(name, value)| (name.clone(), ParameterCacheValue::Exact(value.clone())))
90 .collect(),
91 },
92 slot_count: 0,
93 });
94 }
95 let mut template_parameters = parameters.clone();
96 let mut cache_values = BTreeMap::new();
97 let mut slot_count = 0;
98
99 for (name, value) in parameters {
100 let marker = parameter_marker(name, value, &[]);
101 let mut candidate_parameters = parameters.clone();
102 candidate_parameters.insert(name.clone(), marker.clone());
103 let is_safe = planner::plan_with_params(statement, &candidate_parameters)
104 .ok()
105 .is_some_and(|mut candidate| {
106 marker_use_in_logical_plan(&candidate, name) == MarkerUse::Safe
107 && bind_logical_plan(&mut candidate, parameters).is_ok()
108 && candidate == actual
109 });
110 if is_safe {
111 template_parameters.insert(name.clone(), marker);
112 cache_values.insert(
113 name.clone(),
114 ParameterCacheValue::Slot(parameter_value_shape(value)),
115 );
116 slot_count += 1;
117 } else {
118 cache_values.insert(name.clone(), ParameterCacheValue::Exact(value.clone()));
119 }
120 }
121
122 let logical = if slot_count == 0 {
123 actual.clone()
124 } else {
125 let candidate = planner::plan_with_params(statement, &template_parameters)?;
126 let all_slots_are_safe = cache_values.iter().all(|(name, cached)| {
127 !matches!(cached, ParameterCacheValue::Slot(_))
128 || marker_use_in_logical_plan(&candidate, name) == MarkerUse::Safe
129 });
130 let mut rebound = candidate.clone();
131 if all_slots_are_safe
132 && bind_logical_plan(&mut rebound, parameters).is_ok()
133 && rebound == actual
134 {
135 candidate
136 } else {
137 cache_values = parameters
138 .iter()
139 .map(|(name, value)| (name.clone(), ParameterCacheValue::Exact(value.clone())))
140 .collect();
141 slot_count = 0;
142 actual.clone()
143 }
144 };
145 Ok(ParameterizedLogicalPlan {
146 logical,
147 cache_key: PlanParameterCacheKey {
148 values: cache_values,
149 },
150 slot_count,
151 })
152}
153
154pub fn parameterize_value_list(name: &str, values: &[Value]) -> Vec<Value> {
160 match parameter_marker(name, &Value::List(values.to_vec()), &[]) {
161 Value::List(markers) => markers,
162 _ => unreachable!("list parameterization always produces a list"),
163 }
164}
165
166pub fn bind_physical_plan_parameters(
167 template: &PhysicalPlan,
168 parameters: &BTreeMap<String, Value>,
169 has_slots: bool,
170) -> Result<PhysicalPlan> {
171 let mut plan = template.clone();
172 if has_slots {
173 bind_physical_plan(&mut plan, parameters)?;
174 }
175 Ok(plan)
176}
177
178fn parameter_value_shape(value: &Value) -> ParameterValueShape {
179 match value {
180 Value::Null => ParameterValueShape::Null,
181 Value::Bool(_) => ParameterValueShape::Bool,
182 Value::Int(_) => ParameterValueShape::Int,
183 Value::Float(_) => ParameterValueShape::Float,
184 Value::String(_) => ParameterValueShape::String,
185 Value::Binary(_) => ParameterValueShape::Binary,
186 Value::Uuid(_) => ParameterValueShape::Uuid,
187 Value::List(values) => {
188 ParameterValueShape::List(values.iter().map(parameter_value_shape).collect())
189 }
190 Value::Map(values) => ParameterValueShape::Map(
191 values
192 .iter()
193 .map(|(key, value)| (key.clone(), parameter_value_shape(value)))
194 .collect(),
195 ),
196 }
197}
198
199fn parameter_marker(name: &str, value: &Value, path: &[usize]) -> Value {
200 match value {
201 Value::List(values) => Value::List(
202 values
203 .iter()
204 .enumerate()
205 .map(|(index, value)| {
206 let mut nested_path = path.to_vec();
207 nested_path.push(index);
208 parameter_marker(name, value, &nested_path)
209 })
210 .collect(),
211 ),
212 _ => Value::Map(BTreeMap::from([
213 (
214 PARAMETER_SLOT_NAME_KEY.to_string(),
215 Value::String(name.to_string()),
216 ),
217 (
218 PARAMETER_SLOT_PATH_KEY.to_string(),
219 Value::List(path.iter().map(|index| Value::Int(*index as i64)).collect()),
220 ),
221 ])),
222 }
223}
224
225fn marker_name_and_path(value: &Value) -> Option<(&str, Vec<usize>)> {
226 let Value::Map(values) = value else {
227 return None;
228 };
229 if values.len() != 2 {
230 return None;
231 }
232 let Value::String(name) = values.get(PARAMETER_SLOT_NAME_KEY)? else {
233 return None;
234 };
235 let Value::List(path) = values.get(PARAMETER_SLOT_PATH_KEY)? else {
236 return None;
237 };
238 let path = path
239 .iter()
240 .map(|index| match index {
241 Value::Int(index) if *index >= 0 => usize::try_from(*index).ok(),
242 _ => None,
243 })
244 .collect::<Option<Vec<_>>>()?;
245 Some((name, path))
246}
247
248fn value_contains_marker(value: &Value, name: &str) -> bool {
249 if marker_name_and_path(value).is_some_and(|(marker_name, _)| marker_name == name) {
250 return true;
251 }
252 match value {
253 Value::List(values) => values
254 .iter()
255 .any(|value| value_contains_marker(value, name)),
256 Value::Map(values) => values
257 .values()
258 .any(|value| value_contains_marker(value, name)),
259 _ => false,
260 }
261}
262
263fn value_contains_any_marker(value: &Value) -> bool {
264 if marker_name_and_path(value).is_some() {
265 return true;
266 }
267 match value {
268 Value::List(values) => values.iter().any(value_contains_any_marker),
269 Value::Map(values) => values.values().any(value_contains_any_marker),
270 _ => false,
271 }
272}
273
274fn values_marker_use<'a>(values: impl IntoIterator<Item = &'a Value>, name: &str) -> MarkerUse {
275 if values
276 .into_iter()
277 .any(|value| value_contains_marker(value, name))
278 {
279 MarkerUse::Safe
280 } else {
281 MarkerUse::None
282 }
283}
284
285fn marker_use_in_logical_plan(plan: &LogicalPlan, name: &str) -> MarkerUse {
286 match plan {
287 LogicalPlan::NodeScan { .. } | LogicalPlan::ThreadRepairStats { .. } => MarkerUse::None,
288 LogicalPlan::NodeCartesianProduct { left, right } => {
289 marker_use_in_logical_plan(left, name).combine(marker_use_in_logical_plan(right, name))
290 }
291 LogicalPlan::GraphMatch { program, input } => {
292 let mut marker_use = input.as_ref().map_or(MarkerUse::None, |input| {
293 marker_use_in_logical_plan(input, name)
294 });
295 for step in &program.steps {
296 let node = match step {
297 GraphMatchStep::Node(node) => node,
298 GraphMatchStep::Expand {
299 target, properties, ..
300 } => {
301 marker_use =
302 marker_use.combine(values_marker_use(properties.values(), name));
303 target
304 }
305 };
306 marker_use = marker_use.combine(values_marker_use(node.properties.values(), name));
307 }
308 if let Some(predicate) = &program.predicate {
309 marker_use = marker_use.combine(marker_use_in_predicate(predicate, name));
310 }
311 marker_use
312 }
313 LogicalPlan::NodeColumnLookup { input, .. }
314 | LogicalPlan::Distinct { input }
315 | LogicalPlan::Limit { input, .. } => marker_use_in_logical_plan(input, name),
316 LogicalPlan::Expand {
317 rel_properties,
318 input,
319 ..
320 } => values_marker_use(rel_properties.values(), name)
321 .combine(marker_use_in_logical_plan(input, name)),
322 LogicalPlan::OptionalDegree {
323 rel_properties,
324 target_properties,
325 input,
326 ..
327 } => values_marker_use(rel_properties.values(), name)
328 .combine(values_marker_use(target_properties.values(), name))
329 .combine(marker_use_in_logical_plan(input, name)),
330 LogicalPlan::OptionalRelationshipCountSum {
331 properties, legs, ..
332 } => {
333 let mut marker_use = values_marker_use(properties.values(), name);
334 for leg in legs {
335 if let Some(RelationshipCountFilter::PropertyNotEqOrEmpty { value, .. }) =
336 &leg.filter
337 {
338 marker_use = marker_use.combine(if value_contains_marker(value, name) {
339 MarkerUse::Safe
340 } else {
341 MarkerUse::None
342 });
343 }
344 }
345 marker_use
346 }
347 LogicalPlan::Filter {
348 predicate, input, ..
349 } => marker_use_in_predicate(predicate, name)
350 .combine(marker_use_in_logical_plan(input, name)),
351 LogicalPlan::Project { items, input } => marker_use_in_projections(items, name, false)
352 .combine(marker_use_in_logical_plan(input, name)),
353 LogicalPlan::Aggregate {
354 group_keys, input, ..
355 } => marker_use_in_projections(group_keys, name, false)
356 .combine(marker_use_in_logical_plan(input, name)),
357 LogicalPlan::Sort { items, input } => {
358 let marker_use = items.iter().fold(MarkerUse::None, |marker_use, item| {
359 marker_use.combine(marker_use_in_sort_item(item, name))
360 });
361 marker_use.combine(marker_use_in_logical_plan(input, name))
362 }
363 LogicalPlan::ShortestPath {
364 source_id,
365 source_visibility_predicate,
366 target_id,
367 target_visibility_predicate,
368 ..
369 } => {
370 let mut marker_use = values_marker_use([source_id, target_id], name);
371 if marker_use == MarkerUse::Safe {
372 marker_use = MarkerUse::Unsafe;
373 }
374 for predicate in [source_visibility_predicate, target_visibility_predicate]
375 .into_iter()
376 .flatten()
377 {
378 marker_use = marker_use.combine(marker_use_in_predicate(predicate, name));
379 }
380 marker_use
381 }
382 LogicalPlan::GraphAlgorithm {
383 node_visibility_predicate,
384 ..
385 } => node_visibility_predicate
386 .as_ref()
387 .map(|predicate| marker_use_in_predicate(predicate, name))
388 .unwrap_or(MarkerUse::None),
389 _ => MarkerUse::None,
390 }
391}
392
393fn marker_use_in_predicate(predicate: &Predicate, name: &str) -> MarkerUse {
394 match predicate {
395 Predicate::And(predicates) | Predicate::Or(predicates) => predicates
396 .iter()
397 .fold(MarkerUse::None, |marker_use, predicate| {
398 marker_use.combine(marker_use_in_predicate(predicate, name))
399 }),
400 Predicate::Not(predicate) => marker_use_in_predicate(predicate, name),
401 Predicate::PropertyEq { value, .. } | Predicate::PropertyNotEq { value, .. } => {
402 values_marker_use([value], name)
403 }
404 Predicate::PropertyIn { values, .. } => values_marker_use(values, name),
405 Predicate::IdEq { value, .. }
406 | Predicate::IdNotEq { value, .. }
407 | Predicate::IdCompare { value, .. }
408 | Predicate::PropertyCompare { value, .. }
409 | Predicate::PropertyListContains { value, .. } => {
410 if value_contains_marker(value, name) {
411 MarkerUse::Unsafe
412 } else {
413 MarkerUse::None
414 }
415 }
416 Predicate::IdIn { values, .. } => {
417 if values
418 .iter()
419 .any(|value| value_contains_marker(value, name))
420 {
421 MarkerUse::Unsafe
422 } else {
423 MarkerUse::None
424 }
425 }
426 Predicate::ExpressionEq { expression, value }
427 | Predicate::ExpressionNotEq { expression, value }
428 | Predicate::ExpressionContains { expression, value } => {
429 marker_use_in_projection_expression(expression, name, false)
430 .combine(marker_use_in_projection_expression(value, name, false))
431 }
432 Predicate::ExpressionCompare {
433 expression, value, ..
434 } => marker_use_in_projection_expression(expression, name, false)
435 .combine(marker_use_in_projection_expression(value, name, false)),
436 _ => MarkerUse::None,
437 }
438}
439
440fn marker_use_in_projections(projections: &[Projection], name: &str, safe: bool) -> MarkerUse {
441 projections
442 .iter()
443 .fold(MarkerUse::None, |marker_use, projection| {
444 marker_use.combine(marker_use_in_projection_expression(
445 &projection.expression,
446 name,
447 safe,
448 ))
449 })
450}
451
452fn marker_use_in_sort_item(item: &SortItem, name: &str) -> MarkerUse {
453 match &item.key {
454 SortKey::Expression(expression) => {
455 marker_use_in_projection_expression(expression, name, false)
456 }
457 _ => MarkerUse::None,
458 }
459}
460
461fn marker_use_in_projection_expression(
462 expression: &ProjectionExpression,
463 name: &str,
464 safe: bool,
465) -> MarkerUse {
466 let marker_use = match expression {
467 ProjectionExpression::Case { .. }
468 | ProjectionExpression::Binary { .. }
469 | ProjectionExpression::Not(_)
470 | ProjectionExpression::IsNull { .. } => {
471 let mut result = MarkerUse::None;
472 expression.all_children(|child| {
473 result = result.combine(marker_use_in_projection_expression(child, name, safe));
474 true
475 });
476 result
477 }
478
479 ProjectionExpression::Literal(value) => values_marker_use([value], name),
480 ProjectionExpression::Coalesce(expressions) => {
481 expressions
482 .iter()
483 .fold(MarkerUse::None, |marker_use, expression| {
484 marker_use.combine(marker_use_in_projection_expression(expression, name, safe))
485 })
486 }
487 ProjectionExpression::Left { expression, .. } | ProjectionExpression::Lower(expression) => {
488 marker_use_in_projection_expression(expression, name, safe)
489 }
490 ProjectionExpression::DefaultIfNullOrEq { empty, default, .. }
491 | ProjectionExpression::ColumnDefaultIfNullOrEq { empty, default, .. } => {
492 values_marker_use([empty, default], name)
493 }
494 ProjectionExpression::DefaultIfNull { default, .. }
495 | ProjectionExpression::CaseLowerPropertyDefault { default, .. }
496 | ProjectionExpression::ColumnValueDefaultIfNull { default, .. } => {
497 values_marker_use([default], name)
498 }
499 ProjectionExpression::CasePropertyNotNullOrEq {
500 empty,
501 non_empty,
502 null_or_empty,
503 ..
504 }
505 | ProjectionExpression::ColumnValueCasePropertyNotNullOrEq {
506 empty,
507 non_empty,
508 null_or_empty,
509 ..
510 } => values_marker_use([empty, non_empty, null_or_empty], name),
511 ProjectionExpression::CasePropertyEqualsRank {
512 branches, default, ..
513 } => values_marker_use(
514 branches
515 .iter()
516 .flat_map(|(left, right)| [left, right])
517 .chain([default]),
518 name,
519 ),
520 ProjectionExpression::CaseCoalesceDifferenceFloorZero { terms, .. } => {
521 values_marker_use(terms.iter().map(|term| &term.default), name)
522 }
523 ProjectionExpression::CaseEntitySearchRank(expression) => values_marker_use(
524 [
525 &expression.raw_query,
526 &expression.normalized_query,
527 &expression.raw_input,
528 &expression.exact_rank,
529 &expression.alias_rank,
530 &expression.fallback_rank,
531 ],
532 name,
533 ),
534 ProjectionExpression::CaseColumnSearchRank(expression) => values_marker_use(
535 [
536 &expression.raw_query,
537 &expression.normalized_query,
538 &expression.exact_rank,
539 &expression.contains_rank,
540 &expression.fallback_rank,
541 ],
542 name,
543 ),
544 _ => MarkerUse::None,
545 };
546 if !safe && marker_use == MarkerUse::Safe {
547 MarkerUse::Unsafe
548 } else {
549 marker_use
550 }
551}
552
553fn bind_physical_plan(plan: &mut PhysicalPlan, parameters: &BTreeMap<String, Value>) -> Result<()> {
554 match plan {
555 PhysicalPlan::GraphAlgorithm {
556 node_visibility_predicate,
557 ..
558 } => bind_optional_predicate(node_visibility_predicate, parameters)?,
559 PhysicalPlan::SourceSegmentScan { predicate, .. }
560 | PhysicalPlan::FilterExec { predicate, .. } => {
561 bind_predicate(predicate, parameters)?;
562 }
563 PhysicalPlan::NodeProjectionScanExec {
564 access,
565 predicate,
566 items,
567 ..
568 } => {
569 bind_node_projection_access(access, parameters)?;
570 bind_optional_predicate(predicate, parameters)?;
571 bind_projections(items, parameters)?;
572 }
573 PhysicalPlan::NodeCartesianProductExec { left, right }
574 | PhysicalPlan::HashJoinExec { left, right, .. } => {
575 bind_physical_plan(left, parameters)?;
576 bind_physical_plan(right, parameters)?;
577 }
578 PhysicalPlan::GraphMatchExec { program, input } => {
579 for step in &mut program.steps {
580 let node = match step {
581 GraphMatchStep::Node(node) => node,
582 GraphMatchStep::Expand {
583 target, properties, ..
584 } => {
585 bind_value_map(properties, parameters)?;
586 target
587 }
588 };
589 bind_value_map(&mut node.properties, parameters)?;
590 }
591 bind_optional_predicate(&mut program.predicate, parameters)?;
592 if let Some(input) = input {
593 bind_physical_plan(input, parameters)?;
594 }
595 }
596 PhysicalPlan::NodeColumnLookupExec { input, .. }
597 | PhysicalPlan::DistinctExec { input }
598 | PhysicalPlan::LimitExec { input, .. } => bind_physical_plan(input, parameters)?,
599 PhysicalPlan::IndexNodeSeek { value, .. } => bind_value(value, parameters)?,
600 PhysicalPlan::IndexNodeMultiSeek { values, .. } => {
601 bind_values(values, parameters)?;
602 }
603 PhysicalPlan::IndexNodeUnionSeek { branches, .. } => {
604 for branch in branches {
605 bind_values(&mut branch.values, parameters)?;
606 }
607 }
608 PhysicalPlan::IndexNodeCompositeSeek { predicates, .. } => {
609 for (_, value) in predicates {
610 bind_value(value, parameters)?;
611 }
612 }
613 PhysicalPlan::IndexNodeCompositeRangeSeek { seek, .. } => {
614 bind_composite_range_seek(seek, parameters)?;
615 }
616 PhysicalPlan::IndexNodeRangeSeek { lower, upper, .. } => {
617 for (value, _) in lower.iter_mut().chain(upper.iter_mut()) {
618 bind_value(value, parameters)?;
619 }
620 }
621 PhysicalPlan::AdjacencyExpandExec {
622 rel_properties,
623 input,
624 ..
625 } => {
626 bind_value_map(rel_properties, parameters)?;
627 bind_physical_plan(input, parameters)?;
628 }
629 PhysicalPlan::OptionalDegreeExec {
630 rel_properties,
631 target_properties,
632 input,
633 ..
634 } => {
635 bind_value_map(rel_properties, parameters)?;
636 bind_value_map(target_properties, parameters)?;
637 bind_physical_plan(input, parameters)?;
638 }
639 PhysicalPlan::OptionalRelationshipCountSumExec {
640 properties, legs, ..
641 } => {
642 bind_value_map(properties, parameters)?;
643 for leg in legs {
644 if let Some(RelationshipCountFilter::PropertyNotEqOrEmpty { value, .. }) =
645 &mut leg.filter
646 {
647 bind_value(value, parameters)?;
648 }
649 }
650 }
651 PhysicalPlan::ShortestPathExec {
652 source_id,
653 source_visibility_predicate,
654 target_id,
655 target_visibility_predicate,
656 ..
657 } => {
658 bind_value(source_id, parameters)?;
659 bind_optional_predicate(source_visibility_predicate, parameters)?;
660 bind_value(target_id, parameters)?;
661 bind_optional_predicate(target_visibility_predicate, parameters)?;
662 }
663 PhysicalPlan::ProjectExec { items, input } => {
664 bind_projections(items, parameters)?;
665 bind_physical_plan(input, parameters)?;
666 }
667 PhysicalPlan::AggregateExec {
668 group_keys, input, ..
669 } => {
670 bind_projections(group_keys, parameters)?;
671 bind_physical_plan(input, parameters)?;
672 }
673 PhysicalPlan::SortExec { items, input } | PhysicalPlan::TopNExec { items, input, .. } => {
674 bind_sort_items(items, parameters)?;
675 bind_physical_plan(input, parameters)?;
676 }
677 _ => {}
678 }
679 if let PhysicalPlan::SourceSegmentScan { .. } | PhysicalPlan::FilterExec { .. } = plan {
680 match plan {
681 PhysicalPlan::FilterExec { input, .. } => bind_physical_plan(input, parameters)?,
682 PhysicalPlan::SourceSegmentScan { .. } => {}
683 _ => unreachable!(),
684 }
685 }
686 Ok(())
687}
688
689fn bind_node_projection_access(
690 access: &mut hawdb_plan_cypher::NodeProjectionAccess,
691 parameters: &BTreeMap<String, Value>,
692) -> Result<()> {
693 match access {
694 hawdb_plan_cypher::NodeProjectionAccess::LabelScan => Ok(()),
695 hawdb_plan_cypher::NodeProjectionAccess::PropertyValues { values, .. } => {
696 bind_values(values, parameters)
697 }
698 hawdb_plan_cypher::NodeProjectionAccess::PropertyUnion { branches } => {
699 for branch in branches {
700 bind_values(&mut branch.values, parameters)?;
701 }
702 Ok(())
703 }
704 hawdb_plan_cypher::NodeProjectionAccess::CompositeEquality { predicates } => {
705 for (_, value) in predicates {
706 bind_value(value, parameters)?;
707 }
708 Ok(())
709 }
710 hawdb_plan_cypher::NodeProjectionAccess::CompositeRange { seek } => {
711 bind_composite_range_seek(seek, parameters)
712 }
713 hawdb_plan_cypher::NodeProjectionAccess::PropertyRange { lower, upper, .. } => {
714 for (value, _) in lower.iter_mut().chain(upper.iter_mut()) {
715 bind_value(value, parameters)?;
716 }
717 Ok(())
718 }
719 hawdb_plan_cypher::NodeProjectionAccess::FullText { .. } => Ok(()),
720 }
721}
722
723fn bind_composite_range_seek(
724 seek: &mut hawdb_plan_cypher::CompositeRangeSeek,
725 parameters: &BTreeMap<String, Value>,
726) -> Result<()> {
727 for (_, value) in &mut seek.equality_prefix {
728 bind_value(value, parameters)?;
729 }
730 for (value, _) in seek.lower.iter_mut().chain(seek.upper.iter_mut()) {
731 bind_value(value, parameters)?;
732 }
733 Ok(())
734}
735
736fn bind_logical_plan(plan: &mut LogicalPlan, parameters: &BTreeMap<String, Value>) -> Result<()> {
737 match plan {
738 LogicalPlan::GraphAlgorithm {
739 node_visibility_predicate,
740 ..
741 } => bind_optional_predicate(node_visibility_predicate, parameters)?,
742 LogicalPlan::NodeCartesianProduct { left, right } => {
743 bind_logical_plan(left, parameters)?;
744 bind_logical_plan(right, parameters)?;
745 }
746 LogicalPlan::NodeColumnLookup { input, .. }
747 | LogicalPlan::Distinct { input }
748 | LogicalPlan::Limit { input, .. } => bind_logical_plan(input, parameters)?,
749 LogicalPlan::Expand {
750 rel_properties,
751 input,
752 ..
753 } => {
754 bind_value_map(rel_properties, parameters)?;
755 bind_logical_plan(input, parameters)?;
756 }
757 LogicalPlan::OptionalDegree {
758 rel_properties,
759 target_properties,
760 input,
761 ..
762 } => {
763 bind_value_map(rel_properties, parameters)?;
764 bind_value_map(target_properties, parameters)?;
765 bind_logical_plan(input, parameters)?;
766 }
767 LogicalPlan::OptionalRelationshipCountSum {
768 properties, legs, ..
769 } => {
770 bind_value_map(properties, parameters)?;
771 for leg in legs {
772 if let Some(RelationshipCountFilter::PropertyNotEqOrEmpty { value, .. }) =
773 &mut leg.filter
774 {
775 bind_value(value, parameters)?;
776 }
777 }
778 }
779 LogicalPlan::ShortestPath {
780 source_id,
781 source_visibility_predicate,
782 target_id,
783 target_visibility_predicate,
784 ..
785 } => {
786 bind_value(source_id, parameters)?;
787 bind_optional_predicate(source_visibility_predicate, parameters)?;
788 bind_value(target_id, parameters)?;
789 bind_optional_predicate(target_visibility_predicate, parameters)?;
790 }
791 LogicalPlan::Filter {
792 predicate, input, ..
793 } => {
794 bind_predicate(predicate, parameters)?;
795 bind_logical_plan(input, parameters)?;
796 }
797 LogicalPlan::Project { items, input } => {
798 bind_projections(items, parameters)?;
799 bind_logical_plan(input, parameters)?;
800 }
801 LogicalPlan::Aggregate {
802 group_keys, input, ..
803 } => {
804 bind_projections(group_keys, parameters)?;
805 bind_logical_plan(input, parameters)?;
806 }
807 LogicalPlan::Sort { items, input } => {
808 bind_sort_items(items, parameters)?;
809 bind_logical_plan(input, parameters)?;
810 }
811 _ => {}
812 }
813 Ok(())
814}
815
816fn bind_optional_predicate(
817 predicate: &mut Option<Predicate>,
818 parameters: &BTreeMap<String, Value>,
819) -> Result<()> {
820 if let Some(predicate) = predicate {
821 bind_predicate(predicate, parameters)?;
822 }
823 Ok(())
824}
825
826fn bind_predicate(predicate: &mut Predicate, parameters: &BTreeMap<String, Value>) -> Result<()> {
827 match predicate {
828 Predicate::And(predicates) | Predicate::Or(predicates) => {
829 for predicate in predicates {
830 bind_predicate(predicate, parameters)?;
831 }
832 }
833 Predicate::Not(predicate) => bind_predicate(predicate, parameters)?,
834 Predicate::IdEq { value, .. }
835 | Predicate::IdNotEq { value, .. }
836 | Predicate::IdCompare { value, .. }
837 | Predicate::PropertyEq { value, .. }
838 | Predicate::PropertyNotEq { value, .. }
839 | Predicate::PropertyCompare { value, .. }
840 | Predicate::PropertyListContains { value, .. } => bind_value(value, parameters)?,
841 Predicate::IdIn { values, .. } | Predicate::PropertyIn { values, .. } => {
842 bind_values(values, parameters)?;
843 }
844 Predicate::ExpressionEq { expression, value }
845 | Predicate::ExpressionNotEq { expression, value }
846 | Predicate::ExpressionContains { expression, value } => {
847 bind_projection_expression(expression, parameters)?;
848 bind_projection_expression(value, parameters)?;
849 }
850 Predicate::ExpressionCompare {
851 expression, value, ..
852 } => {
853 bind_projection_expression(expression, parameters)?;
854 bind_projection_expression(value, parameters)?;
855 }
856 _ => {}
857 }
858 Ok(())
859}
860
861fn bind_projections(
862 projections: &mut [Projection],
863 parameters: &BTreeMap<String, Value>,
864) -> Result<()> {
865 for projection in projections {
866 bind_projection_expression(&mut projection.expression, parameters)?;
867 }
868 Ok(())
869}
870
871fn bind_sort_items(items: &mut [SortItem], parameters: &BTreeMap<String, Value>) -> Result<()> {
872 for item in items {
873 if let SortKey::Expression(expression) = &mut item.key {
874 bind_projection_expression(expression, parameters)?;
875 }
876 }
877 Ok(())
878}
879
880fn bind_projection_expression(
881 expression: &mut ProjectionExpression,
882 parameters: &BTreeMap<String, Value>,
883) -> Result<()> {
884 match expression {
885 ProjectionExpression::Case { .. }
886 | ProjectionExpression::Binary { .. }
887 | ProjectionExpression::Not(_)
888 | ProjectionExpression::IsNull { .. } => expression
889 .try_for_each_child_mut(|child| bind_projection_expression(child, parameters))?,
890
891 ProjectionExpression::Literal(value) => bind_value(value, parameters)?,
892 ProjectionExpression::Coalesce(expressions) => {
893 for expression in expressions {
894 bind_projection_expression(expression, parameters)?;
895 }
896 }
897 ProjectionExpression::Left { expression, .. } | ProjectionExpression::Lower(expression) => {
898 bind_projection_expression(expression, parameters)?;
899 }
900 ProjectionExpression::DefaultIfNullOrEq { empty, default, .. }
901 | ProjectionExpression::ColumnDefaultIfNullOrEq { empty, default, .. } => {
902 bind_value(empty, parameters)?;
903 bind_value(default, parameters)?;
904 }
905 ProjectionExpression::DefaultIfNull { default, .. }
906 | ProjectionExpression::CaseLowerPropertyDefault { default, .. }
907 | ProjectionExpression::ColumnValueDefaultIfNull { default, .. } => {
908 bind_value(default, parameters)?;
909 }
910 ProjectionExpression::CasePropertyNotNullOrEq {
911 empty,
912 non_empty,
913 null_or_empty,
914 ..
915 }
916 | ProjectionExpression::ColumnValueCasePropertyNotNullOrEq {
917 empty,
918 non_empty,
919 null_or_empty,
920 ..
921 } => {
922 bind_value(empty, parameters)?;
923 bind_value(non_empty, parameters)?;
924 bind_value(null_or_empty, parameters)?;
925 }
926 ProjectionExpression::CasePropertyEqualsRank {
927 branches, default, ..
928 } => {
929 for (left, right) in branches {
930 bind_value(left, parameters)?;
931 bind_value(right, parameters)?;
932 }
933 bind_value(default, parameters)?;
934 }
935 ProjectionExpression::CaseCoalesceDifferenceFloorZero { terms, .. } => {
936 for term in terms {
937 bind_value(&mut term.default, parameters)?;
938 }
939 }
940 ProjectionExpression::CaseEntitySearchRank(expression) => {
941 for value in [
942 &mut expression.raw_query,
943 &mut expression.normalized_query,
944 &mut expression.raw_input,
945 &mut expression.exact_rank,
946 &mut expression.alias_rank,
947 &mut expression.fallback_rank,
948 ] {
949 bind_value(value, parameters)?;
950 }
951 }
952 ProjectionExpression::CaseColumnSearchRank(expression) => {
953 for value in [
954 &mut expression.raw_query,
955 &mut expression.normalized_query,
956 &mut expression.exact_rank,
957 &mut expression.contains_rank,
958 &mut expression.fallback_rank,
959 ] {
960 bind_value(value, parameters)?;
961 }
962 }
963 _ => {}
964 }
965 Ok(())
966}
967
968fn bind_value_map(
969 values: &mut BTreeMap<String, Value>,
970 parameters: &BTreeMap<String, Value>,
971) -> Result<()> {
972 for value in values.values_mut() {
973 bind_value(value, parameters)?;
974 }
975 Ok(())
976}
977
978fn bind_values(values: &mut [Value], parameters: &BTreeMap<String, Value>) -> Result<()> {
979 for value in values {
980 bind_value(value, parameters)?;
981 }
982 Ok(())
983}
984
985fn bind_value(value: &mut Value, parameters: &BTreeMap<String, Value>) -> Result<()> {
986 if let Some((name, path)) = marker_name_and_path(value) {
987 let mut bound = parameters
988 .get(name)
989 .ok_or_else(|| HawDBError::Semantic(format!("missing parameter '${name}'")))?;
990 for index in path {
991 let Value::List(values) = bound else {
992 return Err(HawDBError::Semantic(format!(
993 "parameter '${name}' changed shape while binding a cached plan"
994 )));
995 };
996 bound = values.get(index).ok_or_else(|| {
997 HawDBError::Semantic(format!(
998 "parameter '${name}' changed shape while binding a cached plan"
999 ))
1000 })?;
1001 }
1002 *value = bound.clone();
1003 return Ok(());
1004 }
1005 match value {
1006 Value::List(values) => bind_values(values, parameters),
1007 Value::Map(values) => bind_value_map(values, parameters),
1008 _ => Ok(()),
1009 }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014 use super::{
1015 bind_physical_plan_parameters, marker_use_in_logical_plan, parameter_marker,
1016 parameterize_logical_plan, MarkerUse, ParameterCacheValue, PARAMETER_SLOT_NAME_KEY,
1017 PARAMETER_SLOT_PATH_KEY,
1018 };
1019 use hawdb_core::Value;
1020 use hawdb_cypher as cypher;
1021 use hawdb_optimizer_graph::{CascadesOptimizer, OptimizerCatalog};
1022 use hawdb_plan_cypher::{
1023 CompositeRangeSeek, ExactPropertySeekBranch, LogicalPlanRoot, NodeProjectionAccess,
1024 PhysicalPlan,
1025 };
1026 use std::collections::BTreeMap;
1027
1028 #[test]
1029 fn general_case_preserves_exact_cache_keys_and_rebinds_explicit_template_slots() {
1030 let statement = cypher::parse("MATCH (n:Item) RETURN CASE WHEN n.id = $chosen THEN lower($label) ELSE $fallback END AS result").unwrap();
1031 let parameters = BTreeMap::from([
1032 ("chosen".into(), Value::Int(1)),
1033 ("label".into(), Value::String("FIRST".into())),
1034 ("fallback".into(), Value::String("old".into())),
1035 ]);
1036 let parameterized = parameterize_logical_plan(&statement, ¶meters).unwrap();
1037 assert_eq!(parameterized.slot_count, 0);
1038 assert!(parameterized
1039 .cache_key
1040 .values
1041 .values()
1042 .all(|value| matches!(value, ParameterCacheValue::Exact(_))));
1043 let markers = parameters
1046 .iter()
1047 .map(|(name, value)| (name.clone(), parameter_marker(name, value, &[])))
1048 .collect();
1049 let logical = hawdb_plan_cypher::plan_with_params(&statement, &markers).unwrap();
1050 let root = CascadesOptimizer::default().optimize_root_with_catalog(
1051 &LogicalPlanRoot::new(logical),
1052 &OptimizerCatalog::default(),
1053 );
1054 let (template, _) = root.into_parts();
1055 let parameters = BTreeMap::from([
1056 ("chosen".into(), Value::Int(2)),
1057 ("label".into(), Value::String("SECOND".into())),
1058 ("fallback".into(), Value::String("new".into())),
1059 ]);
1060 let rebound = bind_physical_plan_parameters(&template, ¶meters, true).unwrap();
1061 let text = format!("{rebound:?}");
1062 assert!(text.contains("SECOND") && text.contains("new"), "{text}");
1063 assert!(!text.contains("hawdb_parameter_slot"), "{text}");
1064 }
1065
1066 #[test]
1067 fn clause_match_rebinds_every_step_predicate_and_input() {
1068 let query = "MATCH (a:Item {id: $a}) OPTIONAL MATCH (a)-[r:LINK {code: $code}]->(b:Item {id: $b}) WHERE b.visible = $visible RETURN b";
1069 let slots = ["a", "b", "code", "visible"];
1070 let parameters = slots
1071 .into_iter()
1072 .map(|name| {
1073 (
1074 name.to_string(),
1075 parameter_marker(name, &Value::Int(0), &[]),
1076 )
1077 })
1078 .collect();
1079 let logical = hawdb_plan_cypher::plan_pipeline_query(query, ¶meters).unwrap();
1080 for name in slots {
1081 assert_eq!(
1082 marker_use_in_logical_plan(&logical, name),
1083 MarkerUse::Safe,
1084 "{name}"
1085 );
1086 }
1087 let optimize = |logical| {
1088 CascadesOptimizer::default()
1089 .optimize_root_with_catalog(
1090 &LogicalPlanRoot::new(logical),
1091 &OptimizerCatalog::default(),
1092 )
1093 .into_parts()
1094 .0
1095 };
1096 let template = optimize(logical);
1097 let original = template.instance_fingerprint();
1098 for base in [10, 20] {
1099 let parameters = slots
1100 .into_iter()
1101 .enumerate()
1102 .map(|(index, name)| (name.to_string(), Value::Int(base + index as i64)))
1103 .collect();
1104 let rebound = bind_physical_plan_parameters(&template, ¶meters, true).unwrap();
1105 let fresh =
1106 optimize(hawdb_plan_cypher::plan_pipeline_query(query, ¶meters).unwrap());
1107 assert_eq!(rebound.instance_fingerprint(), fresh.instance_fingerprint());
1108 assert!(!rebound
1109 .instance_fingerprint()
1110 .contains("hawdb_parameter_slot"));
1111 }
1112 assert_eq!(template.instance_fingerprint(), original);
1113 assert!(bind_physical_plan_parameters(&template, &BTreeMap::new(), true).is_err());
1114 }
1115
1116 #[test]
1117 fn equality_parameters_become_rebindable_slots() {
1118 let statement =
1119 cypher::parse("MATCH (m:Memory) WHERE m.id = $id RETURN m.title AS title").unwrap();
1120 let first_parameters =
1121 BTreeMap::from([("id".to_string(), Value::String("first".to_string()))]);
1122 let parameterized = parameterize_logical_plan(&statement, &first_parameters).unwrap();
1123 assert!(matches!(
1124 parameterized.cache_key.values.get("id"),
1125 Some(ParameterCacheValue::Slot(_))
1126 ));
1127
1128 let root = CascadesOptimizer::default().optimize_root_with_catalog(
1129 &LogicalPlanRoot::new(parameterized.logical),
1130 &OptimizerCatalog::default(),
1131 );
1132 let (template, _) = root.into_parts();
1133 let rebound = bind_physical_plan_parameters(
1134 &template,
1135 &BTreeMap::from([("id".to_string(), Value::String("second".to_string()))]),
1136 true,
1137 )
1138 .unwrap();
1139
1140 assert!(rebound.explain(0).contains("second"));
1141 assert!(!rebound.explain(0).contains("hawdb_parameter_slot"));
1142 }
1143
1144 #[test]
1145 fn graph_hash_join_rebinds_both_inputs_without_changing_keys() {
1146 let seek = |variable: &str, parameter: &str| PhysicalPlan::IndexNodeSeek {
1147 variable: variable.into(),
1148 label: "Item".into(),
1149 property: "scope".into(),
1150 value: parameter_marker(parameter, &Value::Int(0), &[]),
1151 };
1152 let template = PhysicalPlan::HashJoinExec {
1153 left_key: hawdb_plan_cypher::HashJoinKey {
1154 variable: "a".into(),
1155 property: "key".into(),
1156 },
1157 right_key: hawdb_plan_cypher::HashJoinKey {
1158 variable: "b".into(),
1159 property: "key".into(),
1160 },
1161 left: Box::new(seek("a", "left_scope")),
1162 right: Box::new(seek("b", "right_scope")),
1163 };
1164 for (a, b) in [(1, 2), (3, 4)] {
1165 let bound = bind_physical_plan_parameters(
1166 &template,
1167 &BTreeMap::from([
1168 ("left_scope".into(), Value::Int(a)),
1169 ("right_scope".into(), Value::Int(b)),
1170 ]),
1171 true,
1172 )
1173 .unwrap();
1174 let PhysicalPlan::HashJoinExec {
1175 left_key,
1176 right_key,
1177 left,
1178 right,
1179 } = bound
1180 else {
1181 unreachable!()
1182 };
1183 assert_eq!(left_key.variable, "a");
1184 assert_eq!(right_key.variable, "b");
1185 assert!(
1186 matches!(*left, PhysicalPlan::IndexNodeSeek { value: Value::Int(value), .. } if value == a)
1187 );
1188 assert!(
1189 matches!(*right, PhysicalPlan::IndexNodeSeek { value: Value::Int(value), .. } if value == b)
1190 );
1191 }
1192 }
1193
1194 #[test]
1195 fn projected_index_access_rebinds_parameter_slots() {
1196 let template = PhysicalPlan::NodeProjectionScanExec {
1197 variable: "m".to_string(),
1198 label: "Memory".to_string(),
1199 access: NodeProjectionAccess::PropertyValues {
1200 property: "stable_id".to_string(),
1201 values: vec![parameter_marker(
1202 "id",
1203 &Value::String("first".to_string()),
1204 &[],
1205 )],
1206 },
1207 required_properties: vec!["stable_id".to_string()],
1208 predicate: None,
1209 items: Vec::new(),
1210 };
1211
1212 let rebound = bind_physical_plan_parameters(
1213 &template,
1214 &BTreeMap::from([("id".to_string(), Value::String("second".to_string()))]),
1215 true,
1216 )
1217 .unwrap();
1218
1219 assert!(matches!(
1220 rebound,
1221 PhysicalPlan::NodeProjectionScanExec {
1222 access: NodeProjectionAccess::PropertyValues { values, .. },
1223 ..
1224 } if values == vec![Value::String("second".to_string())]
1225 ));
1226 }
1227
1228 #[test]
1229 fn projected_union_access_rebinds_every_branch() {
1230 let marker = || parameter_marker("id", &Value::String("first".to_string()), &[]);
1231 let template = PhysicalPlan::NodeProjectionScanExec {
1232 variable: "m".to_string(),
1233 label: "Memory".to_string(),
1234 access: NodeProjectionAccess::PropertyUnion {
1235 branches: vec![
1236 ExactPropertySeekBranch {
1237 property: "id".to_string(),
1238 values: vec![marker()],
1239 },
1240 ExactPropertySeekBranch {
1241 property: "external_id".to_string(),
1242 values: vec![marker()],
1243 },
1244 ],
1245 },
1246 required_properties: vec!["id".to_string(), "external_id".to_string()],
1247 predicate: None,
1248 items: Vec::new(),
1249 };
1250
1251 let rebound = bind_physical_plan_parameters(
1252 &template,
1253 &BTreeMap::from([("id".to_string(), Value::String("second".to_string()))]),
1254 true,
1255 )
1256 .unwrap();
1257
1258 let PhysicalPlan::NodeProjectionScanExec {
1259 access: NodeProjectionAccess::PropertyUnion { branches },
1260 ..
1261 } = rebound
1262 else {
1263 panic!("expected projected union access");
1264 };
1265 assert!(branches
1266 .iter()
1267 .all(|branch| { branch.values == vec![Value::String("second".to_string())] }));
1268 }
1269
1270 #[test]
1271 fn projected_composite_range_rebinds_prefix_and_bounds() {
1272 let template = PhysicalPlan::NodeProjectionScanExec {
1273 variable: "m".to_string(),
1274 label: "Memory".to_string(),
1275 access: NodeProjectionAccess::CompositeRange {
1276 seek: CompositeRangeSeek {
1277 index_properties: vec!["space_id".to_string(), "created_at".to_string()],
1278 equality_prefix: vec![(
1279 "space_id".to_string(),
1280 parameter_marker(
1281 "space_id",
1282 &Value::String("space:first".to_string()),
1283 &[],
1284 ),
1285 )],
1286 range_property: "created_at".to_string(),
1287 lower: Some((parameter_marker("lower", &Value::Int(10), &[]), true)),
1288 upper: Some((parameter_marker("upper", &Value::Int(20), &[]), false)),
1289 },
1290 },
1291 required_properties: vec!["space_id".to_string(), "created_at".to_string()],
1292 predicate: None,
1293 items: Vec::new(),
1294 };
1295
1296 let rebound = bind_physical_plan_parameters(
1297 &template,
1298 &BTreeMap::from([
1299 (
1300 "space_id".to_string(),
1301 Value::String("space:second".to_string()),
1302 ),
1303 ("lower".to_string(), Value::Int(30)),
1304 ("upper".to_string(), Value::Int(40)),
1305 ]),
1306 true,
1307 )
1308 .unwrap();
1309
1310 assert!(matches!(
1311 rebound,
1312 PhysicalPlan::NodeProjectionScanExec {
1313 access: NodeProjectionAccess::CompositeRange { seek },
1314 ..
1315 } if seek.equality_prefix
1316 == vec![(
1317 "space_id".to_string(),
1318 Value::String("space:second".to_string()),
1319 )]
1320 && seek.lower == Some((Value::Int(30), true))
1321 && seek.upper == Some((Value::Int(40), false))
1322 ));
1323 }
1324
1325 #[test]
1326 fn pagination_parameters_remain_exact_variants() {
1327 let statement =
1328 cypher::parse("MATCH (m:Memory) RETURN m.id AS id ORDER BY m.id LIMIT $limit").unwrap();
1329 let parameterized = parameterize_logical_plan(
1330 &statement,
1331 &BTreeMap::from([("limit".to_string(), Value::Int(10))]),
1332 )
1333 .unwrap();
1334
1335 assert_eq!(
1336 parameterized.cache_key.values.get("limit"),
1337 Some(&ParameterCacheValue::Exact(Value::Int(10)))
1338 );
1339 }
1340
1341 #[test]
1342 fn parameters_that_change_logical_simplification_remain_exact_variants() {
1343 let statement = cypher::parse(
1344 "MATCH (t:Thread) WHERE $source IS NULL OR t.source = $source RETURN t.id AS id",
1345 )
1346 .unwrap();
1347 let parameterized = parameterize_logical_plan(
1348 &statement,
1349 &BTreeMap::from([("source".to_string(), Value::Null)]),
1350 )
1351 .unwrap();
1352
1353 assert_eq!(
1354 parameterized.cache_key.values.get("source"),
1355 Some(&ParameterCacheValue::Exact(Value::Null))
1356 );
1357 }
1358
1359 #[test]
1360 fn multiple_equality_parameters_share_one_rebindable_template() {
1361 let statement =
1362 cypher::parse("MATCH (m:Memory) WHERE m.id = $id AND m.kind = $kind RETURN m.id AS id")
1363 .unwrap();
1364 let parameterized = parameterize_logical_plan(
1365 &statement,
1366 &BTreeMap::from([
1367 ("id".to_string(), Value::String("memory-1".to_string())),
1368 ("kind".to_string(), Value::String("note".to_string())),
1369 ]),
1370 )
1371 .unwrap();
1372
1373 assert_eq!(parameterized.slot_count, 2);
1374 assert!(parameterized
1375 .cache_key
1376 .values
1377 .values()
1378 .all(|cached| matches!(cached, ParameterCacheValue::Slot(_))));
1379 }
1380
1381 #[test]
1382 fn marker_shaped_parameter_values_disable_slots() {
1383 let statement =
1384 cypher::parse("MATCH (m:Memory) WHERE m.metadata = $metadata RETURN m.id AS id")
1385 .unwrap();
1386 let marker_shaped = Value::Map(BTreeMap::from([
1387 (
1388 PARAMETER_SLOT_NAME_KEY.to_string(),
1389 Value::String("other".to_string()),
1390 ),
1391 (PARAMETER_SLOT_PATH_KEY.to_string(), Value::List(Vec::new())),
1392 ]));
1393 let parameterized = parameterize_logical_plan(
1394 &statement,
1395 &BTreeMap::from([("metadata".to_string(), marker_shaped.clone())]),
1396 )
1397 .unwrap();
1398
1399 assert_eq!(parameterized.slot_count, 0);
1400 assert_eq!(
1401 parameterized.cache_key.values.get("metadata"),
1402 Some(&ParameterCacheValue::Exact(marker_shaped))
1403 );
1404 }
1405}