1use crate::ast::{Assignment, Expr, Literal};
29use crate::plan::PlanNode;
30use rustc_hash::FxHashMap;
31
32pub struct PlanCache {
38 cache: FxHashMap<u64, PlanNode>,
39 capacity: usize,
40 pub hits: u64,
41 pub misses: u64,
42}
43
44impl PlanCache {
45 pub fn new(capacity: usize) -> Self {
46 PlanCache {
47 cache: FxHashMap::default(),
48 capacity,
49 hits: 0,
50 misses: 0,
51 }
52 }
53
54 pub fn insert(&mut self, hash: u64, plan: PlanNode, source_literal_count: usize) {
72 if contains_grouped_having(&plan) {
80 return;
81 }
82 if count_literal_slots(&plan) != source_literal_count {
83 return;
84 }
85 if self.cache.len() >= self.capacity && !self.cache.contains_key(&hash) {
86 self.cache.clear();
91 }
92 self.cache.insert(hash, plan);
93 }
94
95 pub fn get_with_substitution(&mut self, hash: u64, literals: &[Literal]) -> Option<PlanNode> {
107 match self.cache.get(&hash) {
108 Some(template) => {
109 self.hits += 1;
110 let mut plan = template.clone();
111 let mut idx = 0usize;
112 substitute_plan(&mut plan, literals, &mut idx);
113 debug_assert_eq!(
114 idx,
115 literals.len(),
116 "plan substitution consumed {idx} literals but query had {}",
117 literals.len(),
118 );
119 Some(plan)
120 }
121 None => {
122 self.misses += 1;
123 None
124 }
125 }
126 }
127
128 pub fn len(&self) -> usize {
129 self.cache.len()
130 }
131
132 pub fn is_empty(&self) -> bool {
133 self.cache.is_empty()
134 }
135
136 pub fn clear(&mut self) {
137 self.cache.clear();
138 }
139}
140
141fn contains_grouped_having(plan: &PlanNode) -> bool {
142 match plan {
143 PlanNode::GroupBy {
144 having: Some(_), ..
145 } => true,
146 PlanNode::Filter { input, .. }
147 | PlanNode::Project { input, .. }
148 | PlanNode::Sort { input, .. }
149 | PlanNode::Limit { input, .. }
150 | PlanNode::Offset { input, .. }
151 | PlanNode::Aggregate { input, .. }
152 | PlanNode::Distinct { input }
153 | PlanNode::GroupBy { input, .. }
154 | PlanNode::Update { input, .. }
155 | PlanNode::Delete { input, .. }
156 | PlanNode::Window { input, .. }
157 | PlanNode::Explain { input } => contains_grouped_having(input),
158 PlanNode::NestedLoopJoin { left, right, .. } | PlanNode::Union { left, right, .. } => {
159 contains_grouped_having(left) || contains_grouped_having(right)
160 }
161 PlanNode::SeqScan { .. }
162 | PlanNode::AliasScan { .. }
163 | PlanNode::IndexScan { .. }
164 | PlanNode::RangeScan { .. }
165 | PlanNode::ExprIndexScan { .. }
166 | PlanNode::ExprRangeScan { .. }
167 | PlanNode::OrderedExprIndexScan { .. }
168 | PlanNode::AlterTable { .. }
169 | PlanNode::DropTable { .. }
170 | PlanNode::Insert { .. }
171 | PlanNode::Upsert { .. }
172 | PlanNode::CreateTable { .. }
173 | PlanNode::ListTypes
174 | PlanNode::Describe { .. }
175 | PlanNode::CreateView { .. }
176 | PlanNode::RefreshView { .. }
177 | PlanNode::DropView { .. }
178 | PlanNode::Begin
179 | PlanNode::Commit
180 | PlanNode::Rollback => false,
181 }
182}
183
184pub(crate) fn substitute_plan(plan: &mut PlanNode, literals: &[Literal], idx: &mut usize) {
203 match plan {
204 PlanNode::SeqScan { .. } => {}
205 PlanNode::AliasScan { .. } => {}
206 PlanNode::IndexScan { key, .. } => {
207 substitute_expr(key, literals, idx);
208 }
209 PlanNode::RangeScan { start, end, .. } => {
210 if let Some((expr, _)) = start {
211 substitute_expr(expr, literals, idx);
212 }
213 if let Some((expr, _)) = end {
214 substitute_expr(expr, literals, idx);
215 }
216 }
217 PlanNode::ExprIndexScan { key, .. } => substitute_expr(key, literals, idx),
218 PlanNode::ExprRangeScan { start, end, .. } => {
219 if let Some((expr, _)) = start {
220 substitute_expr(expr, literals, idx);
221 }
222 if let Some((expr, _)) = end {
223 substitute_expr(expr, literals, idx);
224 }
225 }
226 PlanNode::OrderedExprIndexScan { limit, offset, .. } => {
227 substitute_expr(limit, literals, idx);
228 if let Some(offset) = offset {
229 substitute_expr(offset, literals, idx);
230 }
231 }
232 PlanNode::Filter { input, predicate } => {
233 substitute_plan(input, literals, idx);
234 substitute_expr(predicate, literals, idx);
235 }
236 PlanNode::Project { input, fields } => {
237 if let PlanNode::GroupBy {
238 input: group_input,
239 keys,
240 aggregates,
241 having: None,
242 } = input.as_mut()
243 {
244 substitute_plan(group_input, literals, idx);
249 for key in keys {
250 substitute_expr(&mut key.expr, literals, idx);
251 }
252 let mut visited = std::collections::HashSet::new();
253 for field in fields {
254 substitute_group_projection_expr(
255 &mut field.expr,
256 aggregates,
257 &mut visited,
258 literals,
259 idx,
260 );
261 }
262 } else {
263 substitute_plan(input, literals, idx);
264 for f in fields {
265 substitute_expr(&mut f.expr, literals, idx);
266 }
267 }
268 }
269 PlanNode::Sort { input, keys } => {
270 substitute_plan(input, literals, idx);
271 for key in keys {
272 substitute_expr(&mut key.expr, literals, idx);
273 }
274 }
275 PlanNode::AlterTable { .. } => {}
276 PlanNode::DropTable { .. } => {}
277 PlanNode::Limit { input, count } => {
278 if let PlanNode::Offset {
287 input: inner,
288 count: off_count,
289 } = input.as_mut()
290 {
291 substitute_plan(inner, literals, idx);
292 substitute_expr(count, literals, idx);
293 substitute_expr(off_count, literals, idx);
294 } else {
295 substitute_plan(input, literals, idx);
296 substitute_expr(count, literals, idx);
297 }
298 }
299 PlanNode::Offset { input, count } => {
300 substitute_plan(input, literals, idx);
303 substitute_expr(count, literals, idx);
304 }
305 PlanNode::Aggregate {
306 input, argument, ..
307 } => {
308 substitute_plan(input, literals, idx);
309 if let Some(argument) = argument {
310 substitute_expr(argument, literals, idx);
311 }
312 }
313 PlanNode::NestedLoopJoin {
314 left, right, on, ..
315 } => {
316 substitute_plan(left, literals, idx);
321 substitute_plan(right, literals, idx);
322 if let Some(pred) = on {
323 substitute_expr(pred, literals, idx);
324 }
325 }
326 PlanNode::Distinct { input } => {
327 substitute_plan(input, literals, idx);
328 }
329 PlanNode::GroupBy {
330 input,
331 keys,
332 aggregates,
333 having,
334 } => {
335 substitute_plan(input, literals, idx);
336 for key in keys {
337 substitute_expr(&mut key.expr, literals, idx);
338 }
339 for aggregate in aggregates {
340 substitute_expr(&mut aggregate.argument, literals, idx);
341 }
342 if let Some(pred) = having {
343 substitute_expr(pred, literals, idx);
344 }
345 }
346 PlanNode::Insert { rows, .. } => {
347 for assignments in rows {
348 substitute_assignments(assignments, literals, idx);
349 }
350 }
351 PlanNode::Upsert {
352 assignments,
353 on_conflict,
354 ..
355 } => {
356 substitute_assignments(assignments, literals, idx);
357 substitute_assignments(on_conflict, literals, idx);
358 }
359 PlanNode::Update {
360 input, assignments, ..
361 } => {
362 substitute_plan(input, literals, idx);
363 substitute_assignments(assignments, literals, idx);
364 }
365 PlanNode::Delete { input, .. } => {
366 substitute_plan(input, literals, idx);
367 }
368 PlanNode::CreateTable { .. } => {}
369 PlanNode::CreateView { .. } => {}
370 PlanNode::RefreshView { .. } => {}
371 PlanNode::DropView { .. } => {}
372 PlanNode::Window { input, windows } => {
373 substitute_plan(input, literals, idx);
374 for w in windows {
375 for arg in &mut w.args {
376 substitute_expr(arg, literals, idx);
377 }
378 for expr in &mut w.partition_by {
379 substitute_expr(expr, literals, idx);
380 }
381 for key in &mut w.order_by {
382 substitute_expr(&mut key.expr, literals, idx);
383 }
384 }
385 }
386 PlanNode::Union { left, right, .. } => {
387 substitute_plan(left, literals, idx);
388 substitute_plan(right, literals, idx);
389 }
390 PlanNode::Explain { input } => {
391 substitute_plan(input, literals, idx);
392 }
393 PlanNode::ListTypes | PlanNode::Describe { .. } => {}
394 PlanNode::Begin | PlanNode::Commit | PlanNode::Rollback => {}
395 }
396}
397
398fn substitute_assignments(assignments: &mut [Assignment], literals: &[Literal], idx: &mut usize) {
399 for a in assignments {
400 substitute_expr(&mut a.value, literals, idx);
401 }
402}
403
404fn substitute_group_projection_expr(
405 expr: &mut Expr,
406 aggregates: &mut [crate::plan::GroupAgg],
407 visited: &mut std::collections::HashSet<String>,
408 literals: &[Literal],
409 idx: &mut usize,
410) {
411 if let Expr::Field(name) = expr {
412 if visited.insert(name.clone()) {
413 if let Some(aggregate) = aggregates.iter_mut().find(|agg| agg.output_name == *name) {
414 substitute_expr(&mut aggregate.argument, literals, idx);
415 return;
416 }
417 }
418 }
419 match expr {
420 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
421 substitute_group_projection_expr(left, aggregates, visited, literals, idx);
422 substitute_group_projection_expr(right, aggregates, visited, literals, idx);
423 }
424 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) => {
425 substitute_group_projection_expr(inner, aggregates, visited, literals, idx);
426 }
427 Expr::ScalarFunc(_, args) => {
428 for arg in args {
429 substitute_group_projection_expr(arg, aggregates, visited, literals, idx);
430 }
431 }
432 Expr::InList { expr, list, .. } => {
433 substitute_group_projection_expr(expr, aggregates, visited, literals, idx);
434 for item in list {
435 substitute_group_projection_expr(item, aggregates, visited, literals, idx);
436 }
437 }
438 Expr::Case { whens, else_expr } => {
439 for (condition, result) in whens {
440 substitute_group_projection_expr(condition, aggregates, visited, literals, idx);
441 substitute_group_projection_expr(result, aggregates, visited, literals, idx);
442 }
443 if let Some(expr) = else_expr {
444 substitute_group_projection_expr(expr, aggregates, visited, literals, idx);
445 }
446 }
447 _ => substitute_expr(expr, literals, idx),
448 }
449}
450
451pub(crate) fn count_literal_slots(plan: &PlanNode) -> usize {
457 let mut n = 0usize;
458 count_plan(plan, &mut n);
459 n
460}
461
462fn count_plan(plan: &PlanNode, n: &mut usize) {
463 match plan {
464 PlanNode::SeqScan { .. } => {}
465 PlanNode::AliasScan { .. } => {}
466 PlanNode::IndexScan { key, .. } => count_expr(key, n),
467 PlanNode::RangeScan { start, end, .. } => {
468 if let Some((expr, _)) = start {
469 count_expr(expr, n);
470 }
471 if let Some((expr, _)) = end {
472 count_expr(expr, n);
473 }
474 }
475 PlanNode::ExprIndexScan { key, .. } => count_expr(key, n),
476 PlanNode::ExprRangeScan { start, end, .. } => {
477 if let Some((expr, _)) = start {
478 count_expr(expr, n);
479 }
480 if let Some((expr, _)) = end {
481 count_expr(expr, n);
482 }
483 }
484 PlanNode::OrderedExprIndexScan { limit, offset, .. } => {
485 count_expr(limit, n);
486 if let Some(offset) = offset {
487 count_expr(offset, n);
488 }
489 }
490 PlanNode::Filter { input, predicate } => {
491 count_plan(input, n);
492 count_expr(predicate, n);
493 }
494 PlanNode::Project { input, fields } => {
495 if let PlanNode::GroupBy {
496 input: group_input,
497 keys,
498 aggregates,
499 having: None,
500 } = input.as_ref()
501 {
502 count_plan(group_input, n);
503 for key in keys {
504 count_expr(&key.expr, n);
505 }
506 let mut visited = std::collections::HashSet::new();
507 for field in fields {
508 count_group_projection_expr(&field.expr, aggregates, &mut visited, n);
509 }
510 } else {
511 count_plan(input, n);
512 for f in fields {
513 count_expr(&f.expr, n);
514 }
515 }
516 }
517 PlanNode::Sort { input, keys } => {
518 count_plan(input, n);
519 for key in keys {
520 count_expr(&key.expr, n);
521 }
522 }
523 PlanNode::Limit { input, count } => {
524 if let PlanNode::Offset {
529 input: inner,
530 count: off_count,
531 } = input.as_ref()
532 {
533 count_plan(inner, n);
534 count_expr(count, n);
535 count_expr(off_count, n);
536 } else {
537 count_plan(input, n);
538 count_expr(count, n);
539 }
540 }
541 PlanNode::Offset { input, count } => {
542 count_plan(input, n);
543 count_expr(count, n);
544 }
545 PlanNode::Aggregate {
546 input, argument, ..
547 } => {
548 count_plan(input, n);
549 if let Some(argument) = argument {
550 count_expr(argument, n);
551 }
552 }
553 PlanNode::NestedLoopJoin {
554 left, right, on, ..
555 } => {
556 count_plan(left, n);
557 count_plan(right, n);
558 if let Some(pred) = on {
559 count_expr(pred, n);
560 }
561 }
562 PlanNode::Distinct { input } => count_plan(input, n),
563 PlanNode::GroupBy {
564 input,
565 keys,
566 aggregates,
567 having,
568 } => {
569 count_plan(input, n);
570 for key in keys {
571 count_expr(&key.expr, n);
572 }
573 for aggregate in aggregates {
574 count_expr(&aggregate.argument, n);
575 }
576 if let Some(pred) = having {
577 count_expr(pred, n);
578 }
579 }
580 PlanNode::Insert { rows, .. } => {
581 for assignments in rows {
582 for a in assignments {
583 count_expr(&a.value, n);
584 }
585 }
586 }
587 PlanNode::Upsert {
588 assignments,
589 on_conflict,
590 ..
591 } => {
592 for a in assignments {
593 count_expr(&a.value, n);
594 }
595 for a in on_conflict {
596 count_expr(&a.value, n);
597 }
598 }
599 PlanNode::Update {
600 input, assignments, ..
601 } => {
602 count_plan(input, n);
603 for a in assignments {
604 count_expr(&a.value, n);
605 }
606 }
607 PlanNode::Delete { input, .. } => count_plan(input, n),
608 PlanNode::CreateTable { .. } => {}
609 PlanNode::AlterTable { .. } => {}
610 PlanNode::DropTable { .. } => {}
611 PlanNode::CreateView { .. } => {}
612 PlanNode::RefreshView { .. } => {}
613 PlanNode::DropView { .. } => {}
614 PlanNode::Window { input, windows } => {
615 count_plan(input, n);
616 for w in windows {
617 for arg in &w.args {
618 count_expr(arg, n);
619 }
620 for expr in &w.partition_by {
621 count_expr(expr, n);
622 }
623 for key in &w.order_by {
624 count_expr(&key.expr, n);
625 }
626 }
627 }
628 PlanNode::Union { left, right, .. } => {
629 count_plan(left, n);
630 count_plan(right, n);
631 }
632 PlanNode::Explain { input } => {
633 count_plan(input, n);
634 }
635 PlanNode::ListTypes | PlanNode::Describe { .. } => {}
636 PlanNode::Begin | PlanNode::Commit | PlanNode::Rollback => {}
637 }
638}
639
640fn count_group_projection_expr(
641 expr: &Expr,
642 aggregates: &[crate::plan::GroupAgg],
643 visited: &mut std::collections::HashSet<String>,
644 n: &mut usize,
645) {
646 if let Expr::Field(name) = expr {
647 if visited.insert(name.clone()) {
648 if let Some(aggregate) = aggregates.iter().find(|agg| agg.output_name == *name) {
649 count_expr(&aggregate.argument, n);
650 return;
651 }
652 }
653 }
654 match expr {
655 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
656 count_group_projection_expr(left, aggregates, visited, n);
657 count_group_projection_expr(right, aggregates, visited, n);
658 }
659 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) => {
660 count_group_projection_expr(inner, aggregates, visited, n);
661 }
662 Expr::ScalarFunc(_, args) => {
663 for arg in args {
664 count_group_projection_expr(arg, aggregates, visited, n);
665 }
666 }
667 Expr::InList { expr, list, .. } => {
668 count_group_projection_expr(expr, aggregates, visited, n);
669 for item in list {
670 count_group_projection_expr(item, aggregates, visited, n);
671 }
672 }
673 Expr::Case { whens, else_expr } => {
674 for (condition, result) in whens {
675 count_group_projection_expr(condition, aggregates, visited, n);
676 count_group_projection_expr(result, aggregates, visited, n);
677 }
678 if let Some(expr) = else_expr {
679 count_group_projection_expr(expr, aggregates, visited, n);
680 }
681 }
682 _ => count_expr(expr, n),
683 }
684}
685
686fn count_expr(expr: &Expr, n: &mut usize) {
687 match expr {
688 Expr::Literal(_) => *n += 1,
689 Expr::Field(_) | Expr::QualifiedField { .. } | Expr::Param(_) => {}
690 Expr::BinaryOp(l, _, r) => {
691 count_expr(l, n);
692 count_expr(r, n);
693 }
694 Expr::UnaryOp(_, inner) => count_expr(inner, n),
695 Expr::FunctionCall(_, inner, _) => count_expr(inner, n),
696 Expr::Coalesce(l, r) => {
697 count_expr(l, n);
698 count_expr(r, n);
699 }
700 Expr::InList { expr, list, .. } => {
701 count_expr(expr, n);
702 for item in list {
703 count_expr(item, n);
704 }
705 }
706 Expr::ScalarFunc(_, args) => {
707 for a in args {
708 count_expr(a, n);
709 }
710 }
711 Expr::Cast(inner, _) => count_expr(inner, n),
712 Expr::Case { whens, else_expr } => {
713 for (cond, result) in whens {
714 count_expr(cond, n);
715 count_expr(result, n);
716 }
717 if let Some(e) = else_expr {
718 count_expr(e, n);
719 }
720 }
721 Expr::InSubquery { expr, .. } => {
722 count_expr(expr, n);
723 }
726 Expr::ExistsSubquery { .. } => {
727 }
730 Expr::Window {
731 args,
732 partition_by,
733 order_by,
734 ..
735 } => {
736 for a in args {
737 count_expr(a, n);
738 }
739 for expr in partition_by {
740 count_expr(expr, n);
741 }
742 for key in order_by {
743 count_expr(&key.expr, n);
744 }
745 }
746 Expr::JsonPath { base, .. } => count_expr(base, n),
750 Expr::ValueLit(_) => {}
753 Expr::Null => {}
754 }
755}
756
757fn substitute_expr(expr: &mut Expr, literals: &[Literal], idx: &mut usize) {
758 match expr {
759 Expr::Literal(_) => {
760 *expr = Expr::Literal(literals[*idx].clone());
764 *idx += 1;
765 }
766 Expr::Field(_) | Expr::QualifiedField { .. } | Expr::Param(_) => {}
767 Expr::BinaryOp(l, _, r) => {
768 substitute_expr(l, literals, idx);
769 substitute_expr(r, literals, idx);
770 }
771 Expr::UnaryOp(_, inner) => {
772 substitute_expr(inner, literals, idx);
773 }
774 Expr::FunctionCall(_, inner, _) => {
775 substitute_expr(inner, literals, idx);
776 }
777 Expr::Coalesce(l, r) => {
778 substitute_expr(l, literals, idx);
779 substitute_expr(r, literals, idx);
780 }
781 Expr::InList { expr, list, .. } => {
782 substitute_expr(expr, literals, idx);
783 for item in list {
784 substitute_expr(item, literals, idx);
785 }
786 }
787 Expr::ScalarFunc(_, args) => {
788 for a in args {
789 substitute_expr(a, literals, idx);
790 }
791 }
792 Expr::Cast(inner, _) => substitute_expr(inner, literals, idx),
793 Expr::Case { whens, else_expr } => {
794 for (cond, result) in whens {
795 substitute_expr(cond, literals, idx);
796 substitute_expr(result, literals, idx);
797 }
798 if let Some(e) = else_expr {
799 substitute_expr(e, literals, idx);
800 }
801 }
802 Expr::InSubquery { expr, .. } => {
803 substitute_expr(expr, literals, idx);
804 }
805 Expr::ExistsSubquery { .. } => {
806 }
809 Expr::Window {
810 args,
811 partition_by,
812 order_by,
813 ..
814 } => {
815 for a in args {
816 substitute_expr(a, literals, idx);
817 }
818 for expr in partition_by {
819 substitute_expr(expr, literals, idx);
820 }
821 for key in order_by {
822 substitute_expr(&mut key.expr, literals, idx);
823 }
824 }
825 Expr::JsonPath { base, .. } => substitute_expr(base, literals, idx),
828 Expr::ValueLit(_) => {}
831 Expr::Null => {}
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838 use crate::canonicalize::canonicalize;
839 use crate::planner;
840
841 #[test]
842 fn test_cache_hit_substitutes_literal() {
843 let mut cache = PlanCache::new(100);
844
845 let q1 = "User filter .id = 42";
847 let (h1, lits1) = canonicalize(q1).unwrap();
848 let p1 = planner::plan(q1).unwrap();
849 cache.insert(h1, p1, lits1.len());
850
851 let q2 = "User filter .id = 99";
854 let (h2, lits2) = canonicalize(q2).unwrap();
855 assert_eq!(h1, h2, "different literals must hash the same");
856
857 let plan = cache.get_with_substitution(h2, &lits2).expect("hit");
858
859 match plan {
861 PlanNode::IndexScan { key, .. } => {
862 assert_eq!(key, Expr::Literal(Literal::Int(99)));
863 }
864 other => panic!("expected IndexScan, got {other:?}"),
865 }
866
867 assert_eq!(lits1, vec![Literal::Int(42)]);
870 assert_eq!(cache.hits, 1);
871 assert_eq!(cache.misses, 0);
872 }
873
874 #[test]
875 fn test_subquery_plan_not_cached() {
876 let mut cache = PlanCache::new(100);
883 let q = "User filter .id in (Ord filter .total > 100 { .user_id })";
884 let (h, lits) = canonicalize(q).unwrap();
885 assert_eq!(lits.len(), 1, "canonicalize collects the inner literal");
886 let plan = planner::plan(q).unwrap();
887 assert_eq!(
888 count_literal_slots(&plan),
889 0,
890 "the subquery literal is not a reachable substitution slot"
891 );
892 cache.insert(h, plan, lits.len());
893 assert!(cache.is_empty(), "subquery plans must not be cached (#137)");
894 assert!(cache.get_with_substitution(h, &lits).is_none());
895 }
896
897 #[test]
898 fn test_cache_miss_returns_none_and_bumps_counter() {
899 let mut cache = PlanCache::new(100);
900 assert!(cache.get_with_substitution(99999, &[]).is_none());
901 assert_eq!(cache.misses, 1);
902 assert_eq!(cache.hits, 0);
903 }
904
905 #[test]
906 fn test_multi_literal_filter_substitution() {
907 let mut cache = PlanCache::new(100);
908 let q1 = r#"User filter .age > 30 and .status = "active" { .name }"#;
909 let (h1, lits1) = canonicalize(q1).unwrap();
910 cache.insert(h1, planner::plan(q1).unwrap(), lits1.len());
911
912 let q2 = r#"User filter .age > 50 and .status = "pending" { .name }"#;
913 let (h2, lits2) = canonicalize(q2).unwrap();
914 let plan = cache.get_with_substitution(h2, &lits2).expect("hit");
915
916 let mut found = Vec::new();
918 collect_literals_for_test(&plan, &mut found);
919 assert_eq!(
920 found,
921 vec![Literal::Int(50), Literal::String("pending".into()),]
922 );
923 }
924
925 #[test]
926 fn test_grouped_join_having_is_not_cached_without_source_slot_ordinals() {
927 let mut cache = PlanCache::new(100);
932 let q1 = "User as u join Order as o on u.id = o.user_id \
933 group u.status having count(o.total) > 1 { u.status, n: count(o.total) }";
934 let (h1, lits1) = canonicalize(q1).unwrap();
935 let p1 = planner::plan(q1).unwrap();
936
937 assert_eq!(lits1.len(), 1, "only the HAVING literal is collected");
941 assert_eq!(
942 count_literal_slots(&p1),
943 lits1.len(),
944 "group keys/args are structural, so slots == literals (#137)"
945 );
946 cache.insert(h1, p1, lits1.len());
947 assert!(cache.is_empty(), "grouped HAVING plans must not cache");
948
949 let q2 = "User as u join Order as o on u.id = o.user_id \
950 group u.status having count(o.total) > 5 { u.status, n: count(o.total) }";
951 let (h2, lits2) = canonicalize(q2).unwrap();
952 assert_eq!(h1, h2, "different HAVING literal must hash the same");
953
954 assert!(cache.get_with_substitution(h2, &lits2).is_none());
955 assert_eq!(cache.hits, 0);
956 assert_eq!(cache.misses, 1);
957 }
958
959 #[test]
960 fn json_path_slot_count_invariant() {
961 for q in [
966 r#"Post filter .data->author->name = "x""#,
967 r#"Post filter .data->tags->0 = "rust""#,
968 r#"Post filter .data->age > 21 and .data->year = 2026"#,
969 r#"Post filter .data->"weird key" = 1 { .id }"#,
970 r#"Post { author: .data->author, first_tag: .data->tags->0 }"#,
971 ] {
972 let (_, lits) = canonicalize(q).unwrap();
973 let plan = planner::plan(q).unwrap();
974 assert_eq!(
975 count_literal_slots(&plan),
976 lits.len(),
977 "slot count must equal source literal count for `{q}`"
978 );
979 }
980 }
981
982 #[test]
983 fn json_path_plan_round_trips_cache() {
984 let mut cache = PlanCache::new(100);
988 let q1 = r#"Post filter .data->age > 21"#;
989 let (h1, lits1) = canonicalize(q1).unwrap();
990 let p1 = planner::plan(q1).unwrap();
991 assert_eq!(count_literal_slots(&p1), lits1.len());
992 cache.insert(h1, p1, lits1.len());
993 assert_eq!(cache.len(), 1, "path plan must cache");
994
995 let q2 = r#"Post filter .data->age > 65"#;
996 let (h2, lits2) = canonicalize(q2).unwrap();
997 assert_eq!(h1, h2, "same path, different literal → same hash");
998 let plan = cache.get_with_substitution(h2, &lits2).expect("hit");
999
1000 let mut found = Vec::new();
1001 collect_literals_for_test(&plan, &mut found);
1002 assert_eq!(found, vec![Literal::Int(65)], "new literal substituted");
1003 assert_eq!(cache.hits, 1);
1004 }
1005
1006 #[test]
1007 fn json_path_different_path_is_a_cache_miss() {
1008 let mut cache = PlanCache::new(100);
1009 let q1 = r#"Post filter .data->age > 21"#;
1010 let (h1, lits1) = canonicalize(q1).unwrap();
1011 cache.insert(h1, planner::plan(q1).unwrap(), lits1.len());
1012
1013 let q2 = r#"Post filter .data->year > 21"#;
1016 let (h2, lits2) = canonicalize(q2).unwrap();
1017 assert!(
1018 cache.get_with_substitution(h2, &lits2).is_none(),
1019 "a different path must not hit the cached plan"
1020 );
1021 }
1022
1023 #[test]
1024 fn expression_aggregate_literal_substitutes_on_cache_hit() {
1025 let mut cache = PlanCache::new(8);
1026 let q1 = "Post group .data->kind { total: sum(.data->age + 1) }";
1027 let (h1, literals1) = canonicalize(q1).unwrap();
1028 let plan1 = planner::plan(q1).unwrap();
1029 assert_eq!(count_literal_slots(&plan1), literals1.len());
1030 cache.insert(h1, plan1, literals1.len());
1031
1032 let q2 = "Post group .data->kind { total: sum(.data->age + 7) }";
1033 let (h2, literals2) = canonicalize(q2).unwrap();
1034 assert_eq!(h1, h2);
1035 let plan = cache.get_with_substitution(h2, &literals2).expect("hit");
1036 let mut found = Vec::new();
1037 collect_literals_for_test(&plan, &mut found);
1038 assert_eq!(found, vec![Literal::Int(7)]);
1039 }
1040
1041 #[test]
1042 fn expression_index_equality_and_range_bounds_substitute_on_cache_hits() {
1043 let mut cache = PlanCache::new(8);
1044
1045 let q1 = "Post filter .data->age = 21";
1046 let (h1, literals1) = canonicalize(q1).unwrap();
1047 let plan1 = planner::plan(q1).unwrap();
1048 assert!(matches!(plan1, PlanNode::ExprIndexScan { .. }));
1049 assert_eq!(count_literal_slots(&plan1), literals1.len());
1050 cache.insert(h1, plan1, literals1.len());
1051
1052 let q2 = "Post filter .data->age = 65";
1053 let (h2, literals2) = canonicalize(q2).unwrap();
1054 assert_eq!(h1, h2);
1055 let plan = cache.get_with_substitution(h2, &literals2).expect("hit");
1056 let mut found = Vec::new();
1057 collect_literals_for_test(&plan, &mut found);
1058 assert_eq!(found, vec![Literal::Int(65)]);
1059
1060 let q3 = "Post filter .data->age >= 18 and .data->age < 65";
1061 let (h3, literals3) = canonicalize(q3).unwrap();
1062 let plan3 = planner::plan(q3).unwrap();
1063 assert!(matches!(plan3, PlanNode::ExprRangeScan { .. }));
1064 assert_eq!(count_literal_slots(&plan3), literals3.len());
1065 cache.insert(h3, plan3, literals3.len());
1066
1067 let q4 = "Post filter .data->age >= 25 and .data->age < 80";
1068 let (h4, literals4) = canonicalize(q4).unwrap();
1069 assert_eq!(h3, h4);
1070 let plan = cache.get_with_substitution(h4, &literals4).expect("hit");
1071 let mut found = Vec::new();
1072 collect_literals_for_test(&plan, &mut found);
1073 assert_eq!(found, vec![Literal::Int(25), Literal::Int(80)]);
1074 }
1075
1076 #[test]
1077 fn ordered_expression_scan_limit_offset_substitute_in_canonical_order() {
1078 let mut cache = PlanCache::new(8);
1079 let q1 = "Post order .data->age desc limit 10 offset 2";
1080 let (h1, literals1) = canonicalize(q1).unwrap();
1081 let plan1 = planner::plan(q1).unwrap();
1082 assert!(matches!(plan1, PlanNode::OrderedExprIndexScan { .. }));
1083 assert_eq!(count_literal_slots(&plan1), literals1.len());
1084 cache.insert(h1, plan1, literals1.len());
1085
1086 let q2 = "Post order .data->age desc limit 20 offset 3";
1087 let (h2, literals2) = canonicalize(q2).unwrap();
1088 assert_eq!(h1, h2);
1089 let plan = cache.get_with_substitution(h2, &literals2).expect("hit");
1090 let mut found = Vec::new();
1091 collect_literals_for_test(&plan, &mut found);
1092 assert_eq!(found, vec![Literal::Int(20), Literal::Int(3)]);
1093 }
1094
1095 #[test]
1096 fn test_update_by_pk_substitution() {
1097 let mut cache = PlanCache::new(100);
1098 let q1 = "User filter .id = 1 update { age := 100 }";
1099 let (h1, lits1) = canonicalize(q1).unwrap();
1100 cache.insert(h1, planner::plan(q1).unwrap(), lits1.len());
1101
1102 let q2 = "User filter .id = 7 update { age := 200 }";
1103 let (h2, lits2) = canonicalize(q2).unwrap();
1104 let plan = cache.get_with_substitution(h2, &lits2).expect("hit");
1105
1106 let mut found = Vec::new();
1107 collect_literals_for_test(&plan, &mut found);
1108 assert_eq!(found, vec![Literal::Int(7), Literal::Int(200)]);
1109 }
1110
1111 #[test]
1112 fn test_insert_substitution() {
1113 let mut cache = PlanCache::new(100);
1114 let q1 = r#"insert User { id := 1, name := "Alice", age := 20 }"#;
1115 let (h1, lits1) = canonicalize(q1).unwrap();
1116 cache.insert(h1, planner::plan(q1).unwrap(), lits1.len());
1117
1118 let q2 = r#"insert User { id := 2, name := "Bob", age := 30 }"#;
1119 let (h2, lits2) = canonicalize(q2).unwrap();
1120 let plan = cache.get_with_substitution(h2, &lits2).expect("hit");
1121
1122 let mut found = Vec::new();
1123 collect_literals_for_test(&plan, &mut found);
1124 assert_eq!(
1125 found,
1126 vec![
1127 Literal::Int(2),
1128 Literal::String("Bob".into()),
1129 Literal::Int(30),
1130 ]
1131 );
1132 }
1133
1134 #[test]
1138 fn test_insert_uuid_sugar_cacheable_and_substitutes() {
1139 let mut cache = PlanCache::new(100);
1140 let q1 = r#"insert User { id := uuid("00000000-0000-0000-0000-000000000001") }"#;
1141 let (h1, lits1) = canonicalize(q1).unwrap();
1142 assert_eq!(lits1.len(), 1, "the inner string is the only literal");
1143 let plan = planner::plan(q1).unwrap();
1144 assert_eq!(
1145 count_literal_slots(&plan),
1146 1,
1147 "Cast wrapping a Literal is a reachable substitution slot"
1148 );
1149 cache.insert(h1, plan, lits1.len());
1150 assert!(!cache.is_empty(), "uuid() insert must be cacheable");
1151
1152 let q2 = r#"insert User { id := uuid("00000000-0000-0000-0000-000000000002") }"#;
1153 let (h2, lits2) = canonicalize(q2).unwrap();
1154 assert_eq!(h1, h2, "same shape hashes identically");
1155 let subst = cache.get_with_substitution(h2, &lits2).expect("hit");
1156
1157 let mut found = Vec::new();
1158 collect_literals_for_test(&subst, &mut found);
1159 assert_eq!(
1160 found,
1161 vec![Literal::String(
1162 "00000000-0000-0000-0000-000000000002".into()
1163 )],
1164 "the second call's uuid must be substituted in, not the cached one"
1165 );
1166 }
1167
1168 #[test]
1173 fn test_two_arg_cast_uuid_not_cached() {
1174 let mut cache = PlanCache::new(100);
1175 let q = r#"User filter .id = cast(.other, "uuid")"#;
1176 let (h, lits) = canonicalize(q).unwrap();
1177 assert_eq!(
1178 lits.len(),
1179 1,
1180 "canonicalize collects the cast-target string"
1181 );
1182 let plan = planner::plan(q).unwrap();
1183 assert_eq!(
1184 count_literal_slots(&plan),
1185 0,
1186 "the cast target is baked into the AST, not a slot"
1187 );
1188 cache.insert(h, plan, lits.len());
1189 assert!(cache.is_empty(), "cast(x, \"uuid\") must not be cached");
1190 }
1191
1192 #[test]
1193 fn test_eviction_on_capacity() {
1194 let mut cache = PlanCache::new(2);
1195 let q1 = "User";
1196 let q2 = "User filter .age > 1";
1197 let _q3 = "User filter .age > 2";
1198 let q3_distinct = "User filter .id = 5";
1201
1202 let (h1, lits1) = canonicalize(q1).unwrap();
1203 let (h2, lits2) = canonicalize(q2).unwrap();
1204 let (h3, lits3) = canonicalize(q3_distinct).unwrap();
1205 cache.insert(h1, planner::plan(q1).unwrap(), lits1.len());
1206 cache.insert(h2, planner::plan(q2).unwrap(), lits2.len());
1207 cache.insert(h3, planner::plan(q3_distinct).unwrap(), lits3.len());
1209 assert!(cache.cache.contains_key(&h3));
1210 assert_eq!(cache.cache.len(), 1);
1211 }
1212
1213 fn collect_literals_for_test(plan: &PlanNode, out: &mut Vec<Literal>) {
1217 match plan {
1218 PlanNode::SeqScan { .. } => {}
1219 PlanNode::AliasScan { .. } => {}
1220 PlanNode::IndexScan { key, .. } => collect_expr_literals(key, out),
1221 PlanNode::RangeScan { start, end, .. } => {
1222 if let Some((expr, _)) = start {
1223 collect_expr_literals(expr, out);
1224 }
1225 if let Some((expr, _)) = end {
1226 collect_expr_literals(expr, out);
1227 }
1228 }
1229 PlanNode::ExprIndexScan { key, .. } => collect_expr_literals(key, out),
1230 PlanNode::ExprRangeScan { start, end, .. } => {
1231 if let Some((expr, _)) = start {
1232 collect_expr_literals(expr, out);
1233 }
1234 if let Some((expr, _)) = end {
1235 collect_expr_literals(expr, out);
1236 }
1237 }
1238 PlanNode::OrderedExprIndexScan { limit, offset, .. } => {
1239 collect_expr_literals(limit, out);
1240 if let Some(offset) = offset {
1241 collect_expr_literals(offset, out);
1242 }
1243 }
1244 PlanNode::Filter { input, predicate } => {
1245 collect_literals_for_test(input, out);
1246 collect_expr_literals(predicate, out);
1247 }
1248 PlanNode::Project { input, fields } => {
1249 collect_literals_for_test(input, out);
1250 for f in fields {
1251 collect_expr_literals(&f.expr, out);
1252 }
1253 }
1254 PlanNode::Sort { input, keys } => {
1255 collect_literals_for_test(input, out);
1256 for key in keys {
1257 collect_expr_literals(&key.expr, out);
1258 }
1259 }
1260 PlanNode::Limit { input, count } => {
1261 collect_literals_for_test(input, out);
1262 collect_expr_literals(count, out);
1263 }
1264 PlanNode::Offset { input, count } => {
1265 collect_literals_for_test(input, out);
1266 collect_expr_literals(count, out);
1267 }
1268 PlanNode::Aggregate {
1269 input, argument, ..
1270 } => {
1271 collect_literals_for_test(input, out);
1272 if let Some(argument) = argument {
1273 collect_expr_literals(argument, out);
1274 }
1275 }
1276 PlanNode::NestedLoopJoin {
1277 left, right, on, ..
1278 } => {
1279 collect_literals_for_test(left, out);
1280 collect_literals_for_test(right, out);
1281 if let Some(pred) = on {
1282 collect_expr_literals(pred, out);
1283 }
1284 }
1285 PlanNode::Insert { rows, .. } => {
1286 for assignments in rows {
1287 for a in assignments {
1288 collect_expr_literals(&a.value, out);
1289 }
1290 }
1291 }
1292 PlanNode::Upsert {
1293 assignments,
1294 on_conflict,
1295 ..
1296 } => {
1297 for a in assignments {
1298 collect_expr_literals(&a.value, out);
1299 }
1300 for a in on_conflict {
1301 collect_expr_literals(&a.value, out);
1302 }
1303 }
1304 PlanNode::Update {
1305 input, assignments, ..
1306 } => {
1307 collect_literals_for_test(input, out);
1308 for a in assignments {
1309 collect_expr_literals(&a.value, out);
1310 }
1311 }
1312 PlanNode::Distinct { input } => collect_literals_for_test(input, out),
1313 PlanNode::GroupBy {
1314 input,
1315 keys,
1316 aggregates,
1317 having,
1318 } => {
1319 collect_literals_for_test(input, out);
1320 for key in keys {
1321 collect_expr_literals(&key.expr, out);
1322 }
1323 for aggregate in aggregates {
1324 collect_expr_literals(&aggregate.argument, out);
1325 }
1326 if let Some(pred) = having {
1327 collect_expr_literals(pred, out);
1328 }
1329 }
1330 PlanNode::Delete { input, .. } => collect_literals_for_test(input, out),
1331 PlanNode::CreateTable { .. } => {}
1332 PlanNode::AlterTable { .. } => {}
1333 PlanNode::DropTable { .. } => {}
1334 PlanNode::CreateView { .. } => {}
1335 PlanNode::RefreshView { .. } => {}
1336 PlanNode::DropView { .. } => {}
1337 PlanNode::Window { input, windows } => {
1338 collect_literals_for_test(input, out);
1339 for w in windows {
1340 for arg in &w.args {
1341 collect_expr_literals(arg, out);
1342 }
1343 for expr in &w.partition_by {
1344 collect_expr_literals(expr, out);
1345 }
1346 for key in &w.order_by {
1347 collect_expr_literals(&key.expr, out);
1348 }
1349 }
1350 }
1351 PlanNode::Union { left, right, .. } => {
1352 collect_literals_for_test(left, out);
1353 collect_literals_for_test(right, out);
1354 }
1355 PlanNode::Explain { input } => {
1356 collect_literals_for_test(input, out);
1357 }
1358 PlanNode::ListTypes | PlanNode::Describe { .. } => {}
1359 PlanNode::Begin | PlanNode::Commit | PlanNode::Rollback => {}
1360 }
1361 }
1362
1363 fn collect_expr_literals(expr: &Expr, out: &mut Vec<Literal>) {
1364 match expr {
1365 Expr::Literal(l) => out.push(l.clone()),
1366 Expr::Field(_) | Expr::QualifiedField { .. } | Expr::Param(_) => {}
1367 Expr::BinaryOp(l, _, r) => {
1368 collect_expr_literals(l, out);
1369 collect_expr_literals(r, out);
1370 }
1371 Expr::UnaryOp(_, inner) => collect_expr_literals(inner, out),
1372 Expr::FunctionCall(_, inner, _) => collect_expr_literals(inner, out),
1373 Expr::Coalesce(l, r) => {
1374 collect_expr_literals(l, out);
1375 collect_expr_literals(r, out);
1376 }
1377 Expr::InList { expr, list, .. } => {
1378 collect_expr_literals(expr, out);
1379 for item in list {
1380 collect_expr_literals(item, out);
1381 }
1382 }
1383 Expr::ScalarFunc(_, args) => {
1384 for a in args {
1385 collect_expr_literals(a, out);
1386 }
1387 }
1388 Expr::Cast(inner, _) => collect_expr_literals(inner, out),
1389 Expr::Case { whens, else_expr } => {
1390 for (cond, result) in whens {
1391 collect_expr_literals(cond, out);
1392 collect_expr_literals(result, out);
1393 }
1394 if let Some(e) = else_expr {
1395 collect_expr_literals(e, out);
1396 }
1397 }
1398 Expr::InSubquery { expr, .. } => {
1399 collect_expr_literals(expr, out);
1400 }
1401 Expr::ExistsSubquery { .. } => {}
1402 Expr::Window {
1403 args,
1404 partition_by,
1405 order_by,
1406 ..
1407 } => {
1408 for a in args {
1409 collect_expr_literals(a, out);
1410 }
1411 for expr in partition_by {
1412 collect_expr_literals(expr, out);
1413 }
1414 for key in order_by {
1415 collect_expr_literals(&key.expr, out);
1416 }
1417 }
1418 Expr::JsonPath { base, .. } => collect_expr_literals(base, out),
1421 Expr::ValueLit(_) => {}
1422 Expr::Null => {}
1423 }
1424 }
1425}