1use crate::logical::*;
2use crate::physical::*;
3use lora_analyzer::{symbols::VarId, LiteralValue, ResolvedExpr};
4use lora_ast::BinaryOp;
5use lora_store::GraphStats;
6use std::collections::BTreeSet;
7
8pub struct Optimizer;
9
10impl Default for Optimizer {
11 fn default() -> Self {
12 Self::new()
13 }
14}
15
16impl Optimizer {
17 pub fn new() -> Self {
18 Self
19 }
20
21 pub fn optimize(&mut self, mut plan: LogicalPlan, stats: &GraphStats) -> LogicalPlan {
26 self.push_filter_below_projection(&mut plan);
27 self.use_indexed_node_scans(&mut plan, stats);
28 self.use_indexed_rel_scans(&mut plan, stats);
29 self.annotate_top_k_sorts(&mut plan);
30 self.remove_redundant_limit(&mut plan);
31 plan
32 }
33
34 fn push_filter_below_projection(&self, plan: &mut LogicalPlan) {
35 let len = plan.nodes.len();
36
37 for i in 0..len {
38 let input_id = match &plan.nodes[i] {
39 LogicalOp::Filter(f) => f.input,
40 _ => continue,
41 };
42
43 let Some(input) = plan.nodes.get(input_id) else {
44 continue;
45 };
46
47 if !can_push_filter_below_projection(&plan.nodes[i], input) {
48 continue;
49 }
50
51 push_filter_below_projection_at(plan, i, input_id);
52 }
53 }
54
55 fn remove_redundant_limit(&self, _plan: &mut LogicalPlan) {
56 }
58
59 fn annotate_top_k_sorts(&self, plan: &mut LogicalPlan) {
60 let len = plan.nodes.len();
61 for i in 0..len {
62 let Some((input, bound)) = limit_sort_bound(&plan.nodes[i]) else {
63 continue;
64 };
65
66 if let Some(sort) = sort_op_mut(&mut plan.nodes[input]) {
67 sort.top_k = merge_top_k_bound(sort.top_k, bound);
68 }
69 }
70 }
71
72 fn use_indexed_node_scans(&self, plan: &mut LogicalPlan, stats: &GraphStats) {
78 let len = plan.nodes.len();
79 for i in 0..len {
80 let (input_id, predicate) = match &plan.nodes[i] {
81 LogicalOp::Filter(f) => (f.input, f.predicate.clone()),
82 _ => continue,
83 };
84 let LogicalOp::NodeScan(scan) = &plan.nodes[input_id] else {
85 continue;
86 };
87
88 let candidates = collect_index_candidates(scan, &predicate, stats);
89 let Some(best) = pick_best_candidate(&plan.nodes[input_id], candidates, stats) else {
90 continue;
91 };
92 plan.nodes[input_id] = best;
93 }
94 }
95
96 fn use_indexed_rel_scans(&self, plan: &mut LogicalPlan, stats: &GraphStats) {
104 let len = plan.nodes.len();
105 for i in 0..len {
106 let (filter_input, predicate) = match &plan.nodes[i] {
107 LogicalOp::Filter(f) => (f.input, f.predicate.clone()),
108 _ => continue,
109 };
110 let LogicalOp::Expand(expand) = &plan.nodes[filter_input] else {
111 continue;
112 };
113 if expand.range.is_some() || expand.rel_properties.is_some() {
116 continue;
117 }
118 let Some(rel_var) = expand.rel else {
119 continue;
120 };
121 let LogicalOp::NodeScan(src_scan) = &plan.nodes[expand.input] else {
125 continue;
126 };
127 if src_scan.input.is_some() || !src_scan.labels.is_empty() {
128 continue;
129 }
130 let nodescan_input = src_scan.input;
131 let expand = expand.clone();
132
133 let candidates =
134 collect_rel_index_candidates(&expand, &predicate, rel_var, nodescan_input, stats);
135 let Some(best) = pick_best_candidate(&plan.nodes[filter_input], candidates, stats)
136 else {
137 continue;
138 };
139 plan.nodes[filter_input] = best;
143 }
144 }
145
146 pub fn lower_to_physical(&mut self, logical: LogicalPlan) -> PhysicalPlan {
150 let LogicalPlan { root, nodes } = logical;
151
152 let nodes = nodes.into_iter().map(lower_logical_op).collect();
153
154 PhysicalPlan { root, nodes }
155 }
156}
157
158fn can_push_filter_below_projection(filter: &LogicalOp, input: &LogicalOp) -> bool {
159 let (LogicalOp::Filter(filter), LogicalOp::Projection(proj)) = (filter, input) else {
160 return false;
161 };
162
163 if proj.distinct || proj.include_existing {
164 return false;
165 }
166
167 let output_vars: BTreeSet<VarId> = proj.items.iter().map(|item| item.output).collect();
168 let pred_vars = collect_vars(&filter.predicate);
169 !pred_vars.iter().any(|v| output_vars.contains(v))
170}
171
172fn push_filter_below_projection_at(
173 plan: &mut LogicalPlan,
174 filter_id: PlanNodeId,
175 projection_id: PlanNodeId,
176) {
177 let filter = match plan.nodes.get(filter_id).cloned() {
178 Some(LogicalOp::Filter(f)) => f,
179 _ => return,
180 };
181 let proj = match plan.nodes.get(projection_id).cloned() {
182 Some(LogicalOp::Projection(p)) => p,
183 _ => return,
184 };
185
186 plan.nodes[projection_id] = LogicalOp::Filter(Filter {
187 input: proj.input,
188 predicate: filter.predicate,
189 });
190 plan.nodes[filter_id] = LogicalOp::Projection(Projection {
191 input: projection_id,
192 distinct: proj.distinct,
193 items: proj.items,
194 include_existing: proj.include_existing,
195 });
196}
197
198fn lower_logical_op(op: LogicalOp) -> PhysicalOp {
199 match op {
200 LogicalOp::Argument(_) => PhysicalOp::Argument(ArgumentExec),
201
202 LogicalOp::NodeScan(scan) => lower_node_scan(scan),
203
204 LogicalOp::NodeByPropertyScan(scan) => {
205 PhysicalOp::NodeByPropertyScan(NodeByPropertyScanExec {
206 input: scan.input,
207 var: scan.var,
208 labels: scan.labels,
209 key: scan.key,
210 value: scan.value,
211 })
212 }
213
214 LogicalOp::NodeByPropertyRangeScan(scan) => {
215 PhysicalOp::NodeByPropertyRangeScan(NodeByPropertyRangeScanExec {
216 input: scan.input,
217 var: scan.var,
218 labels: scan.labels,
219 key: scan.key,
220 lo: scan.lo,
221 lo_inclusive: scan.lo_inclusive,
222 hi: scan.hi,
223 hi_inclusive: scan.hi_inclusive,
224 })
225 }
226
227 LogicalOp::NodeByTextScan(scan) => PhysicalOp::NodeByTextScan(NodeByTextScanExec {
228 input: scan.input,
229 var: scan.var,
230 labels: scan.labels,
231 key: scan.key,
232 predicate: scan.predicate,
233 query: scan.query,
234 }),
235
236 LogicalOp::NodeByPointScan(scan) => PhysicalOp::NodeByPointScan(NodeByPointScanExec {
237 input: scan.input,
238 var: scan.var,
239 labels: scan.labels,
240 key: scan.key,
241 predicate: scan.predicate,
242 }),
243
244 LogicalOp::RelByPropertyRangeScan(scan) => {
245 PhysicalOp::RelByPropertyRangeScan(RelByPropertyRangeScanExec {
246 input: scan.input,
247 src: scan.src,
248 rel: scan.rel,
249 dst: scan.dst,
250 types: scan.types,
251 direction: scan.direction,
252 key: scan.key,
253 lo: scan.lo,
254 lo_inclusive: scan.lo_inclusive,
255 hi: scan.hi,
256 hi_inclusive: scan.hi_inclusive,
257 })
258 }
259
260 LogicalOp::RelByTextScan(scan) => PhysicalOp::RelByTextScan(RelByTextScanExec {
261 input: scan.input,
262 src: scan.src,
263 rel: scan.rel,
264 dst: scan.dst,
265 types: scan.types,
266 direction: scan.direction,
267 key: scan.key,
268 predicate: scan.predicate,
269 query: scan.query,
270 }),
271
272 LogicalOp::RelByPointScan(scan) => PhysicalOp::RelByPointScan(RelByPointScanExec {
273 input: scan.input,
274 src: scan.src,
275 rel: scan.rel,
276 dst: scan.dst,
277 types: scan.types,
278 direction: scan.direction,
279 key: scan.key,
280 predicate: scan.predicate,
281 }),
282
283 LogicalOp::Expand(expand) => PhysicalOp::Expand(ExpandExec {
284 input: expand.input,
285 src: expand.src,
286 rel: expand.rel,
287 dst: expand.dst,
288 types: expand.types,
289 direction: expand.direction,
290 rel_properties: expand.rel_properties,
291 range: expand.range,
292 }),
293
294 LogicalOp::Filter(filter) => PhysicalOp::Filter(FilterExec {
295 input: filter.input,
296 predicate: filter.predicate,
297 }),
298
299 LogicalOp::Projection(proj) => PhysicalOp::Projection(ProjectionExec {
300 input: proj.input,
301 distinct: proj.distinct,
302 items: proj.items,
303 include_existing: proj.include_existing,
304 }),
305
306 LogicalOp::Unwind(unwind) => PhysicalOp::Unwind(UnwindExec {
307 input: unwind.input,
308 expr: unwind.expr,
309 alias: unwind.alias,
310 }),
311
312 LogicalOp::Aggregation(agg) => PhysicalOp::HashAggregation(HashAggregationExec {
313 input: agg.input,
314 group_by: agg.group_by,
315 aggregates: agg.aggregates,
316 }),
317
318 LogicalOp::Sort(sort) => PhysicalOp::Sort(SortExec {
319 input: sort.input,
320 items: sort.items,
321 top_k: sort.top_k,
322 }),
323
324 LogicalOp::Limit(limit) => PhysicalOp::Limit(LimitExec {
325 input: limit.input,
326 skip: limit.skip,
327 limit: limit.limit,
328 }),
329
330 LogicalOp::Create(create) => PhysicalOp::Create(CreateExec {
331 input: create.input,
332 pattern: create.pattern,
333 }),
334
335 LogicalOp::Merge(merge) => PhysicalOp::Merge(MergeExec {
336 input: merge.input,
337 pattern_part: merge.pattern_part,
338 actions: merge.actions,
339 }),
340
341 LogicalOp::Delete(delete) => PhysicalOp::Delete(DeleteExec {
342 input: delete.input,
343 detach: delete.detach,
344 expressions: delete.expressions,
345 }),
346
347 LogicalOp::Set(set) => PhysicalOp::Set(SetExec {
348 input: set.input,
349 items: set.items,
350 }),
351
352 LogicalOp::Remove(remove) => PhysicalOp::Remove(RemoveExec {
353 input: remove.input,
354 items: remove.items,
355 }),
356
357 LogicalOp::OptionalMatch(om) => PhysicalOp::OptionalMatch(OptionalMatchExec {
358 input: om.input,
359 inner: om.inner,
360 new_vars: om.new_vars,
361 }),
362
363 LogicalOp::PathBuild(pb) => PhysicalOp::PathBuild(PathBuildExec {
364 input: pb.input,
365 output: pb.output,
366 node_vars: pb.node_vars,
367 rel_vars: pb.rel_vars,
368 shortest_path_all: pb.shortest_path_all,
369 }),
370 }
371}
372
373fn lower_node_scan(scan: NodeScan) -> PhysicalOp {
374 if scan.labels.is_empty() {
375 PhysicalOp::NodeScan(NodeScanExec {
376 input: scan.input,
377 var: scan.var,
378 })
379 } else {
380 PhysicalOp::NodeByLabelScan(NodeByLabelScanExec {
381 input: scan.input,
382 var: scan.var,
383 labels: scan.labels,
384 })
385 }
386}
387
388fn collect_vars(expr: &ResolvedExpr) -> BTreeSet<VarId> {
389 let mut vars = BTreeSet::new();
390 collect_vars_inner(expr, &mut vars);
391 vars
392}
393
394fn collect_index_candidates(
399 scan: &NodeScan,
400 predicate: &ResolvedExpr,
401 stats: &GraphStats,
402) -> Vec<LogicalOp> {
403 let mut out = Vec::new();
404
405 if let Some((var, key, value)) = property_equality_for_var(predicate, scan.var) {
406 out.push(LogicalOp::NodeByPropertyScan(NodeByPropertyScan {
407 input: scan.input,
408 var,
409 labels: scan.labels.clone(),
410 key,
411 value,
412 }));
413 }
414
415 if let Some(bounds) = collect_range_bounds(predicate, scan.var) {
416 if !is_tautological_range(&bounds)
417 && first_simple_label(&scan.labels)
418 .is_some_and(|label| stats.has_node_range_index(label, &bounds.key))
419 {
420 out.push(LogicalOp::NodeByPropertyRangeScan(
421 NodeByPropertyRangeScan {
422 input: scan.input,
423 var: scan.var,
424 labels: scan.labels.clone(),
425 key: bounds.key,
426 lo: bounds.lo,
427 lo_inclusive: bounds.lo_inclusive,
428 hi: bounds.hi,
429 hi_inclusive: bounds.hi_inclusive,
430 },
431 ));
432 }
433 }
434
435 if let Some(candidate) = text_predicate_for_var(predicate, scan.var) {
436 if !is_tautological_text(&candidate)
437 && first_simple_label(&scan.labels)
438 .is_some_and(|label| stats.has_node_text_index(label, &candidate.key))
439 {
440 out.push(LogicalOp::NodeByTextScan(NodeByTextScan {
441 input: scan.input,
442 var: scan.var,
443 labels: scan.labels.clone(),
444 key: candidate.key,
445 predicate: candidate.predicate,
446 query: candidate.query,
447 }));
448 }
449 }
450
451 if let Some(candidate) = point_predicate_for_var(predicate, scan.var) {
452 if !is_tautological_point(&candidate)
453 && first_simple_label(&scan.labels)
454 .is_some_and(|label| stats.has_node_point_index(label, &candidate.key))
455 {
456 out.push(LogicalOp::NodeByPointScan(NodeByPointScan {
457 input: scan.input,
458 var: scan.var,
459 labels: scan.labels.clone(),
460 key: candidate.key,
461 predicate: candidate.predicate,
462 }));
463 }
464 }
465
466 out
467}
468
469fn collect_rel_index_candidates(
474 expand: &Expand,
475 predicate: &ResolvedExpr,
476 rel_var: VarId,
477 input: Option<PlanNodeId>,
478 stats: &GraphStats,
479) -> Vec<LogicalOp> {
480 let mut out = Vec::new();
481
482 if let Some(candidate) = text_predicate_for_var(predicate, rel_var) {
483 if !is_tautological_text(&candidate)
484 && rel_types_have_index(&expand.types, |ty| {
485 stats.has_relationship_text_index(ty, &candidate.key)
486 })
487 {
488 out.push(LogicalOp::RelByTextScan(RelByTextScan {
489 input,
490 src: expand.src,
491 rel: rel_var,
492 dst: expand.dst,
493 types: expand.types.clone(),
494 direction: expand.direction,
495 key: candidate.key,
496 predicate: candidate.predicate,
497 query: candidate.query,
498 }));
499 }
500 }
501
502 if let Some(bounds) = collect_range_bounds(predicate, rel_var) {
503 if !is_tautological_range(&bounds)
504 && rel_types_have_index(&expand.types, |ty| {
505 stats.has_relationship_range_index(ty, &bounds.key)
506 })
507 {
508 out.push(LogicalOp::RelByPropertyRangeScan(RelByPropertyRangeScan {
509 input,
510 src: expand.src,
511 rel: rel_var,
512 dst: expand.dst,
513 types: expand.types.clone(),
514 direction: expand.direction,
515 key: bounds.key,
516 lo: bounds.lo,
517 lo_inclusive: bounds.lo_inclusive,
518 hi: bounds.hi,
519 hi_inclusive: bounds.hi_inclusive,
520 }));
521 }
522 }
523
524 if let Some(candidate) = point_predicate_for_var(predicate, rel_var) {
525 if !is_tautological_point(&candidate)
526 && rel_types_have_index(&expand.types, |ty| {
527 stats.has_relationship_point_index(ty, &candidate.key)
528 })
529 {
530 out.push(LogicalOp::RelByPointScan(RelByPointScan {
531 input,
532 src: expand.src,
533 rel: rel_var,
534 dst: expand.dst,
535 types: expand.types.clone(),
536 direction: expand.direction,
537 key: candidate.key,
538 predicate: candidate.predicate,
539 }));
540 }
541 }
542
543 out
544}
545
546fn rel_types_have_index<F>(types: &[String], mut has_index: F) -> bool
547where
548 F: FnMut(&str) -> bool,
549{
550 !types.is_empty() && types.iter().all(|ty| has_index(ty))
551}
552
553fn pick_best_candidate(
559 original: &LogicalOp,
560 candidates: Vec<LogicalOp>,
561 stats: &GraphStats,
562) -> Option<LogicalOp> {
563 if candidates.is_empty() {
564 return None;
565 }
566
567 let baseline = score_logical_op(original, stats);
568
569 let mut best: Option<(LogicalOp, Option<u64>)> = None;
570 for candidate in candidates {
571 let score = score_logical_op(&candidate, stats);
572 if !improves_over(score, baseline) {
573 continue;
574 }
575 let take = match &best {
576 None => true,
577 Some((_, current_best)) => is_cheaper(score, *current_best),
578 };
579 if take {
580 best = Some((candidate, score));
581 }
582 }
583
584 best.map(|(op, _)| op)
585}
586
587fn improves_over(score: Option<u64>, baseline: Option<u64>) -> bool {
593 match (score, baseline) {
594 (Some(s), Some(b)) => s <= b,
595 (Some(_), None) => true,
596 (None, Some(_)) => false,
597 (None, None) => true,
598 }
599}
600
601fn is_cheaper(score: Option<u64>, current_best: Option<u64>) -> bool {
602 match (score, current_best) {
603 (Some(s), Some(b)) => s < b,
604 (Some(_), None) => true,
605 (None, _) => false,
606 }
607}
608
609fn score_logical_op(op: &LogicalOp, stats: &GraphStats) -> Option<u64> {
615 match op {
616 LogicalOp::NodeScan(scan) => match label_estimate(&scan.labels, stats) {
617 Some(rows) => Some(rows),
618 None if scan.labels.is_empty() => Some(stats.node_count as u64),
619 None => None,
620 },
621 LogicalOp::NodeByPropertyScan(scan) => {
622 let label = first_simple_label(&scan.labels)?;
623 stats.estimate_node_property_equality(label, &scan.key)
624 }
625 LogicalOp::NodeByPropertyRangeScan(scan) => {
626 let base = label_estimate(&scan.labels, stats)?;
631 let denom = match (scan.lo.is_some(), scan.hi.is_some()) {
632 (true, true) => 4,
633 _ => 3,
634 };
635 Some(base.div_ceil(denom))
636 }
637 LogicalOp::NodeByTextScan(scan) => {
638 let base = label_estimate(&scan.labels, stats)?;
639 let denom = match scan.predicate {
641 TextPredicate::StartsWith | TextPredicate::EndsWith => 4,
642 TextPredicate::Contains => 2,
643 };
644 Some(base.div_ceil(denom))
645 }
646 LogicalOp::NodeByPointScan(scan) => {
647 let base = label_estimate(&scan.labels, stats)?;
648 Some(base.div_ceil(5))
651 }
652 LogicalOp::Filter(_) => None,
653 LogicalOp::Expand(expand) => {
654 let count = if expand.types.is_empty() {
659 stats.relationship_count as u64
660 } else {
661 let mut total: u64 = 0;
662 for ty in &expand.types {
663 total = total.saturating_add(stats.relationship_type_count(ty)?);
664 }
665 total
666 };
667 Some(match expand.direction {
669 lora_ast::Direction::Undirected => count.saturating_mul(2),
670 _ => count,
671 })
672 }
673 LogicalOp::RelByPropertyRangeScan(scan) => {
674 let base = rel_type_estimate(&scan.types, stats)?;
675 let denom = match (scan.lo.is_some(), scan.hi.is_some()) {
676 (true, true) => 4,
677 _ => 3,
678 };
679 let est = base.div_ceil(denom);
680 Some(match scan.direction {
681 lora_ast::Direction::Undirected => est.saturating_mul(2),
682 _ => est,
683 })
684 }
685 LogicalOp::RelByTextScan(scan) => {
686 let base = rel_type_estimate(&scan.types, stats)?;
687 let denom = match scan.predicate {
688 TextPredicate::StartsWith | TextPredicate::EndsWith => 4,
689 TextPredicate::Contains => 2,
690 };
691 let est = base.div_ceil(denom);
692 Some(match scan.direction {
693 lora_ast::Direction::Undirected => est.saturating_mul(2),
694 _ => est,
695 })
696 }
697 LogicalOp::RelByPointScan(scan) => {
698 let base = rel_type_estimate(&scan.types, stats)?;
699 let est = base.div_ceil(5);
700 Some(match scan.direction {
701 lora_ast::Direction::Undirected => est.saturating_mul(2),
702 _ => est,
703 })
704 }
705 _ => None,
706 }
707}
708
709fn rel_type_estimate(types: &[String], stats: &GraphStats) -> Option<u64> {
710 if types.is_empty() {
711 return Some(stats.relationship_count as u64);
712 }
713 let mut total: u64 = 0;
714 for ty in types {
715 total = total.saturating_add(stats.relationship_type_count(ty)?);
716 }
717 Some(total)
718}
719
720fn label_estimate(labels: &[Vec<String>], stats: &GraphStats) -> Option<u64> {
725 let label = first_simple_label(labels)?;
726 stats.label_count(label)
727}
728
729fn first_simple_label(labels: &[Vec<String>]) -> Option<&str> {
730 labels.first()?.first().map(String::as_str)
731}
732
733fn is_tautological_range(bounds: &RangeBounds) -> bool {
734 let lo_open = match (&bounds.lo, bounds.lo_inclusive) {
735 (None, _) => true,
736 (Some(expr), false) => matches!(
737 expr,
738 ResolvedExpr::Literal(LiteralValue::Integer(v)) if *v == i64::MIN
739 ),
740 (Some(_), true) => false,
741 };
742 let hi_open = match (&bounds.hi, bounds.hi_inclusive) {
743 (None, _) => true,
744 (Some(expr), false) => matches!(
745 expr,
746 ResolvedExpr::Literal(LiteralValue::Integer(v)) if *v == i64::MAX
747 ),
748 (Some(_), true) => false,
749 };
750 lo_open && hi_open
751}
752
753fn is_tautological_text(candidate: &TextCandidate) -> bool {
754 matches!(
755 &candidate.query,
756 ResolvedExpr::Literal(LiteralValue::String(s)) if s.is_empty()
757 )
758}
759
760fn is_tautological_point(candidate: &PointCandidate) -> bool {
761 match &candidate.predicate {
762 PointPredicate::WithinBBox {
763 lower_left,
764 upper_right,
765 } => is_world_bbox(lower_left, upper_right),
766 PointPredicate::WithinDistance { .. } => false,
767 }
768}
769
770fn is_world_bbox(lower_left: &ResolvedExpr, upper_right: &ResolvedExpr) -> bool {
775 fn const_number(expr: &ResolvedExpr) -> Option<f64> {
779 match expr {
780 ResolvedExpr::Literal(LiteralValue::Float(v)) => Some(*v),
781 ResolvedExpr::Literal(LiteralValue::Integer(v)) => Some(*v as f64),
782 ResolvedExpr::Unary {
783 op: lora_ast::UnaryOp::Neg,
784 expr,
785 } => const_number(expr).map(|v| -v),
786 ResolvedExpr::Unary {
787 op: lora_ast::UnaryOp::Pos,
788 expr,
789 } => const_number(expr),
790 _ => None,
791 }
792 }
793
794 fn point_lon_lat(expr: &ResolvedExpr) -> Option<(f64, f64)> {
795 let ResolvedExpr::Function { name, args, .. } = expr else {
796 return None;
797 };
798 if !name.eq_ignore_ascii_case("point") || args.len() != 1 {
799 return None;
800 }
801 let ResolvedExpr::Map(items) = &args[0] else {
802 return None;
803 };
804 let mut lon: Option<f64> = None;
805 let mut lat: Option<f64> = None;
806 for (key, value) in items {
807 let n = const_number(value)?;
808 match key.as_str() {
809 "longitude" | "x" => lon = Some(n),
810 "latitude" | "y" => lat = Some(n),
811 _ => {}
812 }
813 }
814 Some((lon?, lat?))
815 }
816
817 let Some((ll_lon, ll_lat)) = point_lon_lat(lower_left) else {
818 return false;
819 };
820 let Some((ur_lon, ur_lat)) = point_lon_lat(upper_right) else {
821 return false;
822 };
823 ll_lon <= -180.0 && ll_lat <= -90.0 && ur_lon >= 180.0 && ur_lat >= 90.0
824}
825
826struct RangeBounds {
827 key: String,
828 lo: Option<ResolvedExpr>,
829 lo_inclusive: bool,
830 hi: Option<ResolvedExpr>,
831 hi_inclusive: bool,
832}
833
834fn collect_range_bounds(predicate: &ResolvedExpr, var: VarId) -> Option<RangeBounds> {
838 let mut key: Option<String> = None;
839 let mut lo: Option<ResolvedExpr> = None;
840 let mut lo_inclusive = false;
841 let mut hi: Option<ResolvedExpr> = None;
842 let mut hi_inclusive = false;
843 let mut any = false;
844
845 walk_and_for_range(
846 predicate,
847 var,
848 &mut |found_key, side, value, inclusive| match side {
849 RangeSide::Lower => {
850 if key.as_deref().map(|k| k != found_key).unwrap_or(false) {
851 return;
852 }
853 key = Some(found_key.to_string());
854 if lo
855 .as_ref()
856 .map(|current| lower_bound_is_tighter(&value, inclusive, current, lo_inclusive))
857 .unwrap_or(true)
858 {
859 lo = Some(value);
860 lo_inclusive = inclusive;
861 }
862 any = true;
863 }
864 RangeSide::Upper => {
865 if key.as_deref().map(|k| k != found_key).unwrap_or(false) {
866 return;
867 }
868 key = Some(found_key.to_string());
869 if hi
870 .as_ref()
871 .map(|current| upper_bound_is_tighter(&value, inclusive, current, hi_inclusive))
872 .unwrap_or(true)
873 {
874 hi = Some(value);
875 hi_inclusive = inclusive;
876 }
877 any = true;
878 }
879 },
880 );
881
882 if !any {
883 return None;
884 }
885 Some(RangeBounds {
886 key: key?,
887 lo,
888 lo_inclusive,
889 hi,
890 hi_inclusive,
891 })
892}
893
894#[derive(Clone, Copy)]
895enum RangeSide {
896 Lower,
897 Upper,
898}
899
900fn lower_bound_is_tighter(
901 candidate: &ResolvedExpr,
902 candidate_inclusive: bool,
903 current: &ResolvedExpr,
904 current_inclusive: bool,
905) -> bool {
906 match compare_literal_bounds(candidate, current) {
907 Some(std::cmp::Ordering::Greater) => true,
908 Some(std::cmp::Ordering::Equal) => !candidate_inclusive && current_inclusive,
909 _ => false,
910 }
911}
912
913fn upper_bound_is_tighter(
914 candidate: &ResolvedExpr,
915 candidate_inclusive: bool,
916 current: &ResolvedExpr,
917 current_inclusive: bool,
918) -> bool {
919 match compare_literal_bounds(candidate, current) {
920 Some(std::cmp::Ordering::Less) => true,
921 Some(std::cmp::Ordering::Equal) => !candidate_inclusive && current_inclusive,
922 _ => false,
923 }
924}
925
926fn compare_literal_bounds(lhs: &ResolvedExpr, rhs: &ResolvedExpr) -> Option<std::cmp::Ordering> {
927 match (literal_number(lhs), literal_number(rhs)) {
928 (Some(a), Some(b)) => return a.partial_cmp(&b),
929 (Some(_), None) | (None, Some(_)) => return None,
930 (None, None) => {}
931 }
932
933 match (lhs, rhs) {
934 (
935 ResolvedExpr::Literal(LiteralValue::String(a)),
936 ResolvedExpr::Literal(LiteralValue::String(b)),
937 ) => Some(a.cmp(b)),
938 _ => None,
939 }
940}
941
942fn literal_number(expr: &ResolvedExpr) -> Option<f64> {
943 match expr {
944 ResolvedExpr::Literal(LiteralValue::Integer(v)) => Some(*v as f64),
945 ResolvedExpr::Literal(LiteralValue::Float(v)) => Some(*v),
946 ResolvedExpr::Unary {
947 op: lora_ast::UnaryOp::Neg,
948 expr,
949 } => literal_number(expr).map(|v| -v),
950 ResolvedExpr::Unary {
951 op: lora_ast::UnaryOp::Pos,
952 expr,
953 } => literal_number(expr),
954 _ => None,
955 }
956}
957
958fn walk_and_for_range<F>(predicate: &ResolvedExpr, var: VarId, visit: &mut F)
959where
960 F: FnMut(&str, RangeSide, ResolvedExpr, bool),
961{
962 if let ResolvedExpr::Binary {
963 lhs,
964 op: BinaryOp::And,
965 rhs,
966 } = predicate
967 {
968 walk_and_for_range(lhs, var, visit);
969 walk_and_for_range(rhs, var, visit);
970 return;
971 }
972
973 let ResolvedExpr::Binary { lhs, op, rhs } = predicate else {
974 return;
975 };
976
977 let (side, inclusive) = match op {
978 BinaryOp::Gt => (RangeSide::Lower, false),
979 BinaryOp::Ge => (RangeSide::Lower, true),
980 BinaryOp::Lt => (RangeSide::Upper, false),
981 BinaryOp::Le => (RangeSide::Upper, true),
982 _ => return,
983 };
984
985 if let Some(key) = property_access_for_var(lhs, var) {
986 if !collect_vars(rhs).contains(&var) {
987 visit(&key, side, (**rhs).clone(), inclusive);
988 return;
989 }
990 }
991 if let Some(key) = property_access_for_var(rhs, var) {
992 if !collect_vars(lhs).contains(&var) {
993 let flipped = match side {
995 RangeSide::Lower => RangeSide::Upper,
996 RangeSide::Upper => RangeSide::Lower,
997 };
998 visit(&key, flipped, (**lhs).clone(), inclusive);
999 }
1000 }
1001}
1002
1003struct TextCandidate {
1004 key: String,
1005 predicate: TextPredicate,
1006 query: ResolvedExpr,
1007}
1008
1009struct PointCandidate {
1010 key: String,
1011 predicate: PointPredicate,
1012}
1013
1014fn point_predicate_for_var(predicate: &ResolvedExpr, var: VarId) -> Option<PointCandidate> {
1015 if let ResolvedExpr::Binary {
1016 lhs,
1017 op: BinaryOp::And,
1018 rhs,
1019 } = predicate
1020 {
1021 return point_predicate_for_var(lhs, var).or_else(|| point_predicate_for_var(rhs, var));
1022 }
1023
1024 if let ResolvedExpr::Function { name, args, .. } = predicate {
1026 if name.eq_ignore_ascii_case("point.withinbbox") && args.len() == 3 {
1027 if let Some(key) = property_access_for_var(&args[0], var) {
1028 if !collect_vars(&args[1]).contains(&var) && !collect_vars(&args[2]).contains(&var)
1029 {
1030 return Some(PointCandidate {
1031 key,
1032 predicate: PointPredicate::WithinBBox {
1033 lower_left: args[1].clone(),
1034 upper_right: args[2].clone(),
1035 },
1036 });
1037 }
1038 }
1039 }
1040 }
1041
1042 let ResolvedExpr::Binary { lhs, op, rhs } = predicate else {
1044 return None;
1045 };
1046 let inclusive = match op {
1047 BinaryOp::Le => true,
1048 BinaryOp::Lt => false,
1049 BinaryOp::Ge => true,
1051 BinaryOp::Gt => false,
1052 _ => return None,
1053 };
1054
1055 let (call_side, scalar_side) = match op {
1056 BinaryOp::Le | BinaryOp::Lt => ((**lhs).clone(), (**rhs).clone()),
1057 BinaryOp::Ge | BinaryOp::Gt => ((**rhs).clone(), (**lhs).clone()),
1058 _ => return None,
1059 };
1060
1061 let ResolvedExpr::Function { name, args, .. } = &call_side else {
1062 return None;
1063 };
1064 if !(name.eq_ignore_ascii_case("point.distance") || name.eq_ignore_ascii_case("distance")) {
1065 return None;
1066 }
1067 if args.len() != 2 {
1068 return None;
1069 }
1070 let key = property_access_for_var(&args[0], var)?;
1071 if collect_vars(&args[1]).contains(&var) || collect_vars(&scalar_side).contains(&var) {
1072 return None;
1073 }
1074 Some(PointCandidate {
1075 key,
1076 predicate: PointPredicate::WithinDistance {
1077 center: args[1].clone(),
1078 max_distance: scalar_side,
1079 inclusive,
1080 },
1081 })
1082}
1083
1084fn text_predicate_for_var(predicate: &ResolvedExpr, var: VarId) -> Option<TextCandidate> {
1085 let ResolvedExpr::Binary { lhs, op, rhs } = predicate else {
1086 return None;
1087 };
1088
1089 if matches!(op, BinaryOp::And) {
1090 return text_predicate_for_var(lhs, var).or_else(|| text_predicate_for_var(rhs, var));
1091 }
1092
1093 let kind = match op {
1094 BinaryOp::StartsWith => TextPredicate::StartsWith,
1095 BinaryOp::EndsWith => TextPredicate::EndsWith,
1096 BinaryOp::Contains => TextPredicate::Contains,
1097 _ => return None,
1098 };
1099
1100 let key = property_access_for_var(lhs, var)?;
1101 if collect_vars(rhs).contains(&var) {
1102 return None;
1103 }
1104 Some(TextCandidate {
1105 key,
1106 predicate: kind,
1107 query: (**rhs).clone(),
1108 })
1109}
1110
1111fn property_equality_for_var(
1112 predicate: &ResolvedExpr,
1113 var: VarId,
1114) -> Option<(VarId, String, ResolvedExpr)> {
1115 let ResolvedExpr::Binary { lhs, op, rhs } = predicate else {
1116 return None;
1117 };
1118
1119 if matches!(op, BinaryOp::And) {
1120 return property_equality_for_var(lhs, var).or_else(|| property_equality_for_var(rhs, var));
1121 }
1122
1123 if !matches!(op, BinaryOp::Eq) {
1124 return None;
1125 }
1126
1127 property_access_for_var(lhs, var)
1128 .filter(|_| !collect_vars(rhs).contains(&var))
1129 .map(|key| (var, key, (**rhs).clone()))
1130 .or_else(|| {
1131 property_access_for_var(rhs, var)
1132 .filter(|_| !collect_vars(lhs).contains(&var))
1133 .map(|key| (var, key, (**lhs).clone()))
1134 })
1135}
1136
1137fn static_limit_bound(limit: &Limit) -> Option<usize> {
1138 let limit_rows = match &limit.limit {
1139 Some(expr) => static_non_negative_usize(expr)?,
1140 None => return None,
1141 };
1142 let skip_rows = limit
1143 .skip
1144 .as_ref()
1145 .and_then(static_non_negative_usize)
1146 .unwrap_or(0);
1147 Some(skip_rows.saturating_add(limit_rows))
1148}
1149
1150fn limit_sort_bound(op: &LogicalOp) -> Option<(PlanNodeId, usize)> {
1151 let LogicalOp::Limit(limit) = op else {
1152 return None;
1153 };
1154
1155 static_limit_bound(limit).map(|bound| (limit.input, bound))
1156}
1157
1158fn sort_op_mut(op: &mut LogicalOp) -> Option<&mut Sort> {
1159 match op {
1160 LogicalOp::Sort(sort) => Some(sort),
1161 _ => None,
1162 }
1163}
1164
1165fn merge_top_k_bound(current: Option<usize>, bound: usize) -> Option<usize> {
1166 Some(current.map(|current| current.min(bound)).unwrap_or(bound))
1167}
1168
1169fn static_non_negative_usize(expr: &ResolvedExpr) -> Option<usize> {
1170 match expr {
1171 ResolvedExpr::Literal(LiteralValue::Integer(value)) => {
1172 Some((*value).max(0).try_into().unwrap_or(usize::MAX))
1173 }
1174 _ => None,
1175 }
1176}
1177
1178fn property_access_for_var(expr: &ResolvedExpr, var: VarId) -> Option<String> {
1179 match expr {
1180 ResolvedExpr::Property { expr, property } => match &**expr {
1181 ResolvedExpr::Variable(v) if *v == var => Some(property.clone()),
1182 _ => None,
1183 },
1184 _ => None,
1185 }
1186}
1187
1188fn collect_vars_inner(expr: &ResolvedExpr, out: &mut BTreeSet<VarId>) {
1189 match expr {
1190 ResolvedExpr::Variable(v) => {
1191 out.insert(*v);
1192 }
1193 ResolvedExpr::Property { expr, .. } => collect_vars_inner(expr, out),
1194 ResolvedExpr::Binary { lhs, rhs, .. } => {
1195 collect_vars_inner(lhs, out);
1196 collect_vars_inner(rhs, out);
1197 }
1198 ResolvedExpr::Unary { expr, .. } => collect_vars_inner(expr, out),
1199 ResolvedExpr::Function { args, .. } => {
1200 for arg in args {
1201 collect_vars_inner(arg, out);
1202 }
1203 }
1204 ResolvedExpr::List(items) => {
1205 for item in items {
1206 collect_vars_inner(item, out);
1207 }
1208 }
1209 ResolvedExpr::Map(items) => {
1210 for (_, v) in items {
1211 collect_vars_inner(v, out);
1212 }
1213 }
1214 ResolvedExpr::Case {
1215 input,
1216 alternatives,
1217 else_expr,
1218 } => {
1219 if let Some(e) = input {
1220 collect_vars_inner(e, out);
1221 }
1222 for (w, t) in alternatives {
1223 collect_vars_inner(w, out);
1224 collect_vars_inner(t, out);
1225 }
1226 if let Some(e) = else_expr {
1227 collect_vars_inner(e, out);
1228 }
1229 }
1230 ResolvedExpr::ListPredicate {
1231 variable,
1232 list,
1233 predicate,
1234 ..
1235 } => {
1236 out.insert(*variable);
1237 collect_vars_inner(list, out);
1238 collect_vars_inner(predicate, out);
1239 }
1240 ResolvedExpr::ListComprehension {
1241 variable,
1242 list,
1243 filter,
1244 map_expr,
1245 ..
1246 } => {
1247 out.insert(*variable);
1248 collect_vars_inner(list, out);
1249 if let Some(f) = filter {
1250 collect_vars_inner(f, out);
1251 }
1252 if let Some(m) = map_expr {
1253 collect_vars_inner(m, out);
1254 }
1255 }
1256 ResolvedExpr::Reduce {
1257 accumulator,
1258 init,
1259 variable,
1260 list,
1261 expr,
1262 ..
1263 } => {
1264 out.insert(*accumulator);
1265 out.insert(*variable);
1266 collect_vars_inner(init, out);
1267 collect_vars_inner(list, out);
1268 collect_vars_inner(expr, out);
1269 }
1270 ResolvedExpr::Index { expr, index } => {
1271 collect_vars_inner(expr, out);
1272 collect_vars_inner(index, out);
1273 }
1274 ResolvedExpr::Slice { expr, from, to } => {
1275 collect_vars_inner(expr, out);
1276 if let Some(f) = from {
1277 collect_vars_inner(f, out);
1278 }
1279 if let Some(t) = to {
1280 collect_vars_inner(t, out);
1281 }
1282 }
1283 ResolvedExpr::MapProjection { base, selectors } => {
1284 collect_vars_inner(base, out);
1285 for sel in selectors {
1286 if let lora_analyzer::ResolvedMapSelector::Literal(_, e) = sel {
1287 collect_vars_inner(e, out);
1288 }
1289 }
1290 }
1291 _ => {}
1292 }
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::*;
1298 use lora_store::GraphStats;
1299
1300 fn stats_with_label(label: &str, total: usize, distinct: Option<usize>) -> GraphStats {
1301 let mut s = GraphStats {
1302 node_count: total,
1303 ..Default::default()
1304 };
1305 s.nodes_by_label.insert(label.to_string(), total);
1306 if let Some(d) = distinct {
1307 s.node_distinct_values
1308 .insert((label.to_string(), "id".to_string()), d);
1309 }
1310 s
1311 }
1312
1313 fn person_labels() -> Vec<Vec<String>> {
1314 vec![vec!["Person".to_string()]]
1315 }
1316
1317 fn lit_int(v: i64) -> ResolvedExpr {
1318 ResolvedExpr::Literal(LiteralValue::Integer(v))
1319 }
1320
1321 fn lit_str(s: &str) -> ResolvedExpr {
1322 ResolvedExpr::Literal(LiteralValue::String(s.to_string()))
1323 }
1324
1325 fn lit_float(v: f64) -> ResolvedExpr {
1326 ResolvedExpr::Literal(LiteralValue::Float(v))
1327 }
1328
1329 fn point_lonlat(lon: f64, lat: f64) -> ResolvedExpr {
1330 ResolvedExpr::Function {
1331 name: "point".to_string(),
1332 distinct: false,
1333 args: vec![ResolvedExpr::Map(vec![
1334 ("longitude".to_string(), lit_float(lon)),
1335 ("latitude".to_string(), lit_float(lat)),
1336 ])],
1337 }
1338 }
1339
1340 #[test]
1343 fn score_label_scan_returns_label_count() {
1344 let stats = stats_with_label("Person", 1_000, None);
1345 let op = LogicalOp::NodeScan(NodeScan {
1346 input: None,
1347 var: VarId(0),
1348 labels: person_labels(),
1349 });
1350 assert_eq!(score_logical_op(&op, &stats), Some(1_000));
1351 }
1352
1353 #[test]
1354 fn score_property_scan_uses_distinct() {
1355 let stats = stats_with_label("Person", 1_000, Some(100));
1357 let op = LogicalOp::NodeByPropertyScan(NodeByPropertyScan {
1358 input: None,
1359 var: VarId(0),
1360 labels: person_labels(),
1361 key: "id".to_string(),
1362 value: lit_int(7),
1363 });
1364 assert_eq!(score_logical_op(&op, &stats), Some(10));
1365 }
1366
1367 #[test]
1368 fn score_property_scan_high_distinct_beats_label_scan() {
1369 let stats = stats_with_label("Person", 100, Some(100));
1372 let label_score = score_logical_op(
1373 &LogicalOp::NodeScan(NodeScan {
1374 input: None,
1375 var: VarId(0),
1376 labels: person_labels(),
1377 }),
1378 &stats,
1379 );
1380 let property_score = score_logical_op(
1381 &LogicalOp::NodeByPropertyScan(NodeByPropertyScan {
1382 input: None,
1383 var: VarId(0),
1384 labels: person_labels(),
1385 key: "id".to_string(),
1386 value: lit_int(7),
1387 }),
1388 &stats,
1389 );
1390 assert!(property_score < label_score);
1391 }
1392
1393 #[test]
1394 fn score_returns_none_without_label_stats() {
1395 let stats = GraphStats::default();
1399 let op = LogicalOp::NodeScan(NodeScan {
1400 input: None,
1401 var: VarId(0),
1402 labels: person_labels(),
1403 });
1404 assert_eq!(score_logical_op(&op, &stats), None);
1405 }
1406
1407 #[test]
1410 fn improves_over_legacy_fallback_when_both_unknown() {
1411 assert!(improves_over(None, None));
1414 }
1415
1416 #[test]
1417 fn improves_over_keeps_baseline_when_candidate_unknown() {
1418 assert!(!improves_over(None, Some(10)));
1419 }
1420
1421 #[test]
1422 fn improves_over_strictly_better_or_equal_wins() {
1423 assert!(improves_over(Some(5), Some(10)));
1424 assert!(improves_over(Some(10), Some(10)));
1425 assert!(!improves_over(Some(11), Some(10)));
1426 }
1427
1428 #[test]
1431 fn pick_best_candidate_picks_lowest_score() {
1432 let stats = stats_with_label("Person", 1_200, Some(100));
1433 let original = LogicalOp::NodeScan(NodeScan {
1434 input: None,
1435 var: VarId(0),
1436 labels: person_labels(),
1437 });
1438 let candidates = vec![
1440 LogicalOp::NodeByPropertyRangeScan(NodeByPropertyRangeScan {
1441 input: None,
1442 var: VarId(0),
1443 labels: person_labels(),
1444 key: "age".to_string(),
1445 lo: Some(lit_int(30)),
1446 lo_inclusive: false,
1447 hi: None,
1448 hi_inclusive: false,
1449 }),
1450 LogicalOp::NodeByPropertyScan(NodeByPropertyScan {
1451 input: None,
1452 var: VarId(0),
1453 labels: person_labels(),
1454 key: "id".to_string(),
1455 value: lit_int(7),
1456 }),
1457 ];
1458 let pick = pick_best_candidate(&original, candidates, &stats).expect("expected a pick");
1459 assert!(matches!(pick, LogicalOp::NodeByPropertyScan(_)));
1460 }
1461
1462 #[test]
1463 fn pick_best_candidate_returns_none_when_no_candidate_improves() {
1464 let mut stats = stats_with_label("Person", 1, None);
1470 stats.nodes_by_label.insert("Tiny".to_string(), 1);
1474 let original = LogicalOp::NodeScan(NodeScan {
1475 input: None,
1476 var: VarId(0),
1477 labels: vec![vec!["Tiny".to_string()]],
1478 });
1479 let candidates = vec![LogicalOp::NodeByPropertyScan(NodeByPropertyScan {
1483 input: None,
1484 var: VarId(0),
1485 labels: vec![vec!["Missing".to_string()]],
1486 key: "id".to_string(),
1487 value: lit_int(7),
1488 })];
1489 assert!(pick_best_candidate(&original, candidates, &stats).is_none());
1490 }
1491
1492 #[test]
1495 fn unbounded_low_range_is_tautological() {
1496 let bounds = RangeBounds {
1497 key: "age".to_string(),
1498 lo: Some(lit_int(i64::MIN)),
1499 lo_inclusive: false,
1500 hi: None,
1501 hi_inclusive: false,
1502 };
1503 assert!(is_tautological_range(&bounds));
1504 }
1505
1506 #[test]
1507 fn unbounded_high_range_is_tautological() {
1508 let bounds = RangeBounds {
1509 key: "age".to_string(),
1510 lo: None,
1511 lo_inclusive: false,
1512 hi: Some(lit_int(i64::MAX)),
1513 hi_inclusive: false,
1514 };
1515 assert!(is_tautological_range(&bounds));
1516 }
1517
1518 #[test]
1519 fn doubly_unbounded_range_is_tautological() {
1520 let bounds = RangeBounds {
1521 key: "age".to_string(),
1522 lo: Some(lit_int(i64::MIN)),
1523 lo_inclusive: false,
1524 hi: Some(lit_int(i64::MAX)),
1525 hi_inclusive: false,
1526 };
1527 assert!(is_tautological_range(&bounds));
1528 }
1529
1530 #[test]
1531 fn ordinary_range_is_not_tautological() {
1532 let bounds = RangeBounds {
1533 key: "age".to_string(),
1534 lo: Some(lit_int(0)),
1535 lo_inclusive: false,
1536 hi: Some(lit_int(100)),
1537 hi_inclusive: false,
1538 };
1539 assert!(!is_tautological_range(&bounds));
1540 }
1541
1542 #[test]
1543 fn empty_string_starts_with_is_tautological() {
1544 let candidate = TextCandidate {
1545 key: "name".to_string(),
1546 predicate: TextPredicate::StartsWith,
1547 query: lit_str(""),
1548 };
1549 assert!(is_tautological_text(&candidate));
1550 }
1551
1552 #[test]
1553 fn nonempty_string_starts_with_is_not_tautological() {
1554 let candidate = TextCandidate {
1555 key: "name".to_string(),
1556 predicate: TextPredicate::StartsWith,
1557 query: lit_str("A"),
1558 };
1559 assert!(!is_tautological_text(&candidate));
1560 }
1561
1562 #[test]
1563 fn world_bbox_is_tautological() {
1564 let candidate = PointCandidate {
1565 key: "loc".to_string(),
1566 predicate: PointPredicate::WithinBBox {
1567 lower_left: point_lonlat(-180.0, -90.0),
1568 upper_right: point_lonlat(180.0, 90.0),
1569 },
1570 };
1571 assert!(is_tautological_point(&candidate));
1572 }
1573
1574 #[test]
1575 fn city_bbox_is_not_tautological() {
1576 let candidate = PointCandidate {
1577 key: "loc".to_string(),
1578 predicate: PointPredicate::WithinBBox {
1579 lower_left: point_lonlat(4.7, 52.3),
1580 upper_right: point_lonlat(5.0, 52.5),
1581 },
1582 };
1583 assert!(!is_tautological_point(&candidate));
1584 }
1585
1586 #[test]
1587 fn distance_bbox_is_never_tautological() {
1588 let candidate = PointCandidate {
1591 key: "loc".to_string(),
1592 predicate: PointPredicate::WithinDistance {
1593 center: point_lonlat(0.0, 0.0),
1594 max_distance: lit_int(1_000_000_000),
1595 inclusive: true,
1596 },
1597 };
1598 assert!(!is_tautological_point(&candidate));
1599 }
1600}