1use super::value_range::{compare_histogram_value, range_bound_matches, ValueRangeBound};
16use crate::cardinality_defaults::{
17 NULL_SELECTIVITY_DIVISOR_CAP, RANGE_SELECTIVITY_DIVISOR, SAMPLED_HISTOGRAM_MATCH_PSEUDOCOUNT,
18 SAMPLED_HISTOGRAM_TOTAL_PSEUDOCOUNT,
19};
20use hawdb_core::{Catalog, GraphStatistics, IndexKind, Value};
21use hawdb_plan_cypher::ComparisonOp;
22use std::collections::{BTreeMap, BTreeSet};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub(super) struct ExpandEstimate {
26 pub(super) path_count: Option<u64>,
27 pub(super) rel_count: u64,
28 pub(super) source_count: u64,
29 pub(super) average_fanout: u64,
30 pub(super) property_distinct_product: u64,
31 pub(super) estimated_rows: u64,
32 pub(super) hop_estimates: Vec<HopEstimate>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub(super) struct HopEstimate {
37 pub(super) hop: usize,
38 pub(super) rows: u64,
39 pub(super) exact: bool,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct OptimizerIndexStatistics {
44 pub index_size: u64,
45 pub distinct_count: u64,
46}
47
48#[derive(Debug, Default, Clone, PartialEq, Eq)]
49pub struct OptimizerCatalog {
50 pub(super) assume_all_indexes: bool,
51 pub(super) equality_property_indexes: BTreeSet<(String, String)>,
52 pub(super) composite_property_indexes: BTreeSet<(String, Vec<String>)>,
53 pub(super) range_property_indexes: BTreeSet<(String, String)>,
54 pub(super) ordered_range_indexes: BTreeSet<(String, String)>,
55 pub(super) full_text_property_indexes: BTreeSet<(String, String)>,
56 pub(super) label_counts: BTreeMap<String, u64>,
57 pub(super) rel_type_counts: BTreeMap<String, u64>,
58 pub(super) rel_type_source_counts: BTreeMap<String, u64>,
59 pub(super) rel_type_target_counts: BTreeMap<String, u64>,
60 pub(super) path_counts: BTreeMap<(String, String, String), u64>,
61 pub(super) path_source_distinct_counts: BTreeMap<(String, String, String), u64>,
62 pub(super) path_target_distinct_counts: BTreeMap<(String, String, String), u64>,
63 pub(super) bounded_path_counts: BTreeMap<(String, String, String, usize), u64>,
64 pub(super) bounded_path_source_distinct_counts: BTreeMap<(String, String, String, usize), u64>,
65 pub(super) bounded_path_target_distinct_counts: BTreeMap<(String, String, String, usize), u64>,
66 pub(super) property_index_statistics: BTreeMap<(String, String), OptimizerIndexStatistics>,
67 pub(super) composite_index_statistics:
68 BTreeMap<(String, Vec<String>), OptimizerIndexStatistics>,
69 pub(super) property_distinct_counts: BTreeMap<(String, String), u64>,
70 pub(super) rel_property_distinct_counts: BTreeMap<(String, String), u64>,
71 pub(super) property_histograms: BTreeMap<(String, String), Vec<Value>>,
72 pub(super) rel_property_histograms: BTreeMap<(String, String), Vec<Value>>,
73 pub(super) sampled_property_histograms: BTreeMap<(String, String), bool>,
74 pub(super) sampled_rel_property_histograms: BTreeMap<(String, String), bool>,
75}
76
77#[derive(Debug, Default, Clone, PartialEq, Eq)]
78pub struct OptimizerCatalogIndexes {
79 pub(super) equality_property_indexes: BTreeSet<(String, String)>,
80 pub(super) composite_property_indexes: BTreeSet<(String, Vec<String>)>,
81 pub(super) range_property_indexes: BTreeSet<(String, String)>,
82 pub(super) full_text_property_indexes: BTreeSet<(String, String)>,
83}
84
85#[derive(Debug, Default, Clone, PartialEq, Eq)]
86pub struct OptimizerCatalogStatistics {
87 pub(super) label_counts: BTreeMap<String, u64>,
88 pub(super) rel_type_counts: BTreeMap<String, u64>,
89 pub(super) rel_type_source_counts: BTreeMap<String, u64>,
90 pub(super) rel_type_target_counts: BTreeMap<String, u64>,
91 pub(super) path_counts: BTreeMap<(String, String, String), u64>,
92 pub(super) path_source_distinct_counts: BTreeMap<(String, String, String), u64>,
93 pub(super) path_target_distinct_counts: BTreeMap<(String, String, String), u64>,
94 pub(super) bounded_path_counts: BTreeMap<(String, String, String, usize), u64>,
95 pub(super) bounded_path_source_distinct_counts: BTreeMap<(String, String, String, usize), u64>,
96 pub(super) bounded_path_target_distinct_counts: BTreeMap<(String, String, String, usize), u64>,
97 pub(super) property_index_statistics: BTreeMap<(String, String), OptimizerIndexStatistics>,
98 pub(super) composite_index_statistics:
99 BTreeMap<(String, Vec<String>), OptimizerIndexStatistics>,
100 pub(super) property_distinct_counts: BTreeMap<(String, String), u64>,
101 pub(super) rel_property_distinct_counts: BTreeMap<(String, String), u64>,
102 pub(super) property_histograms: BTreeMap<(String, String), Vec<Value>>,
103 pub(super) rel_property_histograms: BTreeMap<(String, String), Vec<Value>>,
104 pub(super) sampled_property_histograms: BTreeMap<(String, String), bool>,
105 pub(super) sampled_rel_property_histograms: BTreeMap<(String, String), bool>,
106}
107
108#[doc(hidden)]
114pub fn optimizer_catalog_from_graph_statistics(
115 catalog: &Catalog,
116 statistics: &GraphStatistics,
117) -> OptimizerCatalog {
118 let advanced_statistics = statistics
119 .advanced_statistics_complete
120 .then_some(statistics);
121 let equality_property_indexes = catalog.property_indexes().filter_map(|index| {
122 if index.kind != IndexKind::Equality {
123 return None;
124 }
125 catalog
126 .label_name(index.label_id)
127 .map(|label| (label.to_string(), index.property.clone()))
128 });
129 let composite_property_indexes = catalog.composite_property_indexes().filter_map(|index| {
130 catalog
131 .label_name(index.label_id)
132 .map(|label| (label.to_string(), index.properties.clone()))
133 });
134 let range_property_indexes = catalog.property_indexes().filter_map(|index| {
135 if index.kind != IndexKind::Range {
136 return None;
137 }
138 catalog
139 .label_name(index.label_id)
140 .map(|label| (label.to_string(), index.property.clone()))
141 });
142 let full_text_property_indexes = catalog.property_indexes().filter_map(|index| {
143 if index.kind != IndexKind::FullText {
144 return None;
145 }
146 catalog
147 .label_name(index.label_id)
148 .map(|label| (label.to_string(), index.property.clone()))
149 });
150 let label_counts = statistics
151 .label_counts
152 .iter()
153 .filter_map(|(label_id, count)| {
154 catalog
155 .label_name(*label_id)
156 .map(|label| (label.to_string(), *count))
157 });
158 let rel_type_counts = statistics
159 .rel_type_counts
160 .iter()
161 .filter_map(|(rel_type_id, count)| {
162 catalog
163 .rel_type_name(*rel_type_id)
164 .map(|rel_type| (rel_type.to_string(), *count))
165 });
166 let rel_type_source_counts = advanced_statistics
167 .into_iter()
168 .flat_map(|statistics| statistics.rel_type_source_counts.iter())
169 .filter_map(|(rel_type_id, count)| {
170 catalog
171 .rel_type_name(*rel_type_id)
172 .map(|rel_type| (rel_type.to_string(), *count))
173 });
174 let rel_type_target_counts = advanced_statistics
175 .into_iter()
176 .flat_map(|statistics| statistics.rel_type_target_counts.iter())
177 .filter_map(|(rel_type_id, count)| {
178 catalog
179 .rel_type_name(*rel_type_id)
180 .map(|rel_type| (rel_type.to_string(), *count))
181 });
182 let path_counts = advanced_statistics
183 .into_iter()
184 .flat_map(|statistics| statistics.path_counts.iter())
185 .filter_map(|((source_label_id, rel_type_id, target_label_id), count)| {
186 Some((
187 (
188 catalog.label_name(*source_label_id)?.to_string(),
189 catalog.rel_type_name(*rel_type_id)?.to_string(),
190 catalog.label_name(*target_label_id)?.to_string(),
191 ),
192 *count,
193 ))
194 });
195 let path_source_distinct_counts = advanced_statistics
196 .into_iter()
197 .flat_map(|statistics| statistics.path_source_distinct_counts.iter())
198 .filter_map(|((source_label_id, rel_type_id, target_label_id), count)| {
199 Some((
200 (
201 catalog.label_name(*source_label_id)?.to_string(),
202 catalog.rel_type_name(*rel_type_id)?.to_string(),
203 catalog.label_name(*target_label_id)?.to_string(),
204 ),
205 *count,
206 ))
207 });
208 let path_target_distinct_counts = advanced_statistics
209 .into_iter()
210 .flat_map(|statistics| statistics.path_target_distinct_counts.iter())
211 .filter_map(|((source_label_id, rel_type_id, target_label_id), count)| {
212 Some((
213 (
214 catalog.label_name(*source_label_id)?.to_string(),
215 catalog.rel_type_name(*rel_type_id)?.to_string(),
216 catalog.label_name(*target_label_id)?.to_string(),
217 ),
218 *count,
219 ))
220 });
221 let bounded_path_counts = advanced_statistics
222 .into_iter()
223 .flat_map(|statistics| statistics.bounded_path_counts.iter())
224 .filter_map(
225 |((source_label_id, rel_type_id, target_label_id, hops), count)| {
226 Some((
227 (
228 catalog.label_name(*source_label_id)?.to_string(),
229 catalog.rel_type_name(*rel_type_id)?.to_string(),
230 catalog.label_name(*target_label_id)?.to_string(),
231 *hops,
232 ),
233 *count,
234 ))
235 },
236 );
237 let bounded_path_source_distinct_counts = advanced_statistics
238 .into_iter()
239 .flat_map(|statistics| statistics.bounded_path_source_distinct_counts.iter())
240 .filter_map(
241 |((source_label_id, rel_type_id, target_label_id, hops), count)| {
242 Some((
243 (
244 catalog.label_name(*source_label_id)?.to_string(),
245 catalog.rel_type_name(*rel_type_id)?.to_string(),
246 catalog.label_name(*target_label_id)?.to_string(),
247 *hops,
248 ),
249 *count,
250 ))
251 },
252 );
253 let bounded_path_target_distinct_counts = advanced_statistics
254 .into_iter()
255 .flat_map(|statistics| statistics.bounded_path_target_distinct_counts.iter())
256 .filter_map(
257 |((source_label_id, rel_type_id, target_label_id, hops), count)| {
258 Some((
259 (
260 catalog.label_name(*source_label_id)?.to_string(),
261 catalog.rel_type_name(*rel_type_id)?.to_string(),
262 catalog.label_name(*target_label_id)?.to_string(),
263 *hops,
264 ),
265 *count,
266 ))
267 },
268 );
269 let property_distinct_counts = advanced_statistics
270 .into_iter()
271 .flat_map(|statistics| statistics.property_distinct_counts.iter())
272 .filter_map(|((label_id, property), count)| {
273 catalog
274 .label_name(*label_id)
275 .map(|label| ((label.to_string(), property.clone()), *count))
276 });
277 let property_histograms = advanced_statistics
278 .into_iter()
279 .flat_map(|statistics| statistics.property_histograms.iter())
280 .filter_map(|((label_id, property), values)| {
281 catalog
282 .label_name(*label_id)
283 .map(|label| ((label.to_string(), property.clone()), values.clone()))
284 });
285 let sampled_property_histograms = advanced_statistics
286 .into_iter()
287 .flat_map(|statistics| statistics.sampled_property_histograms.iter())
288 .filter_map(|((label_id, property), sampled)| {
289 catalog
290 .label_name(*label_id)
291 .map(|label| ((label.to_string(), property.clone()), *sampled))
292 });
293 let rel_property_distinct_counts = advanced_statistics
294 .into_iter()
295 .flat_map(|statistics| statistics.rel_property_distinct_counts.iter())
296 .filter_map(|((rel_type_id, property), count)| {
297 catalog
298 .rel_type_name(*rel_type_id)
299 .map(|rel_type| ((rel_type.to_string(), property.clone()), *count))
300 });
301 let rel_property_histograms = advanced_statistics
302 .into_iter()
303 .flat_map(|statistics| statistics.rel_property_histograms.iter())
304 .filter_map(|((rel_type_id, property), values)| {
305 catalog
306 .rel_type_name(*rel_type_id)
307 .map(|rel_type| ((rel_type.to_string(), property.clone()), values.clone()))
308 });
309 let sampled_rel_property_histograms = advanced_statistics
310 .into_iter()
311 .flat_map(|statistics| statistics.sampled_rel_property_histograms.iter())
312 .filter_map(|((rel_type_id, property), sampled)| {
313 catalog
314 .rel_type_name(*rel_type_id)
315 .map(|rel_type| ((rel_type.to_string(), property.clone()), *sampled))
316 });
317 let property_index_statistics = catalog.property_indexes().filter_map(|index| {
318 if index.kind == IndexKind::FullText {
319 return None;
320 }
321 let statistics = advanced_statistics?;
322 let sample = statistics.index_samples.get(&index.id)?;
323 let distinct_count = sample.estimated_unique_values()?;
324 let label = catalog.label_name(index.label_id)?;
325 Some((
326 (label.to_string(), index.property.clone()),
327 OptimizerIndexStatistics {
328 index_size: sample.index_size,
329 distinct_count,
330 },
331 ))
332 });
333 let composite_index_statistics = catalog.composite_property_indexes().filter_map(|index| {
334 let statistics = advanced_statistics?;
335 let sample = statistics.index_samples.get(&index.id)?;
336 let distinct_count = sample.estimated_unique_values()?;
337 let label = catalog.label_name(index.label_id)?;
338 Some((
339 (label.to_string(), index.properties.clone()),
340 OptimizerIndexStatistics {
341 index_size: sample.index_size,
342 distinct_count,
343 },
344 ))
345 });
346 OptimizerCatalog::new(
347 OptimizerCatalogIndexes::new(
348 equality_property_indexes,
349 composite_property_indexes,
350 range_property_indexes,
351 full_text_property_indexes,
352 ),
353 OptimizerCatalogStatistics::new(
354 label_counts,
355 rel_type_counts,
356 rel_type_source_counts,
357 path_counts,
358 bounded_path_counts,
359 property_distinct_counts,
360 property_histograms,
361 )
362 .with_property_index_statistics(property_index_statistics)
363 .with_composite_index_statistics(composite_index_statistics)
364 .with_relationship_type_target_counts(rel_type_target_counts)
365 .with_path_source_distinct_counts(path_source_distinct_counts)
366 .with_path_target_distinct_counts(path_target_distinct_counts)
367 .with_bounded_path_source_distinct_counts(bounded_path_source_distinct_counts)
368 .with_bounded_path_target_distinct_counts(bounded_path_target_distinct_counts)
369 .with_relationship_property_distinct_counts(rel_property_distinct_counts)
370 .with_relationship_property_histograms(rel_property_histograms)
371 .with_sampled_property_histograms(sampled_property_histograms)
372 .with_sampled_relationship_property_histograms(sampled_rel_property_histograms),
373 )
374}
375
376impl OptimizerCatalog {
377 pub fn new(indexes: OptimizerCatalogIndexes, statistics: OptimizerCatalogStatistics) -> Self {
378 Self {
379 assume_all_indexes: false,
380 equality_property_indexes: indexes.equality_property_indexes,
381 composite_property_indexes: indexes.composite_property_indexes,
382 range_property_indexes: indexes.range_property_indexes,
383 ordered_range_indexes: BTreeSet::new(),
384 full_text_property_indexes: indexes.full_text_property_indexes,
385 label_counts: statistics.label_counts,
386 rel_type_counts: statistics.rel_type_counts,
387 rel_type_source_counts: statistics.rel_type_source_counts,
388 rel_type_target_counts: statistics.rel_type_target_counts,
389 path_counts: statistics.path_counts,
390 path_source_distinct_counts: statistics.path_source_distinct_counts,
391 path_target_distinct_counts: statistics.path_target_distinct_counts,
392 bounded_path_counts: statistics.bounded_path_counts,
393 bounded_path_source_distinct_counts: statistics.bounded_path_source_distinct_counts,
394 bounded_path_target_distinct_counts: statistics.bounded_path_target_distinct_counts,
395 property_index_statistics: statistics.property_index_statistics,
396 composite_index_statistics: statistics.composite_index_statistics,
397 property_distinct_counts: statistics.property_distinct_counts,
398 rel_property_distinct_counts: statistics.rel_property_distinct_counts,
399 property_histograms: statistics.property_histograms,
400 rel_property_histograms: statistics.rel_property_histograms,
401 sampled_property_histograms: statistics.sampled_property_histograms,
402 sampled_rel_property_histograms: statistics.sampled_rel_property_histograms,
403 }
404 }
405
406 pub fn with_ordered_range_indexes(
410 mut self,
411 indexes: impl IntoIterator<Item = (String, String)>,
412 ) -> Self {
413 self.ordered_range_indexes = indexes
414 .into_iter()
415 .filter(|index| self.range_property_indexes.contains(index))
416 .collect();
417 self
418 }
419
420 pub(super) fn optimistic() -> Self {
421 Self {
422 assume_all_indexes: true,
423 ..Self::default()
424 }
425 }
426
427 pub(super) fn has_property_index(&self, label: &str, property: &str) -> bool {
428 self.assume_all_indexes
429 || self
430 .equality_property_indexes
431 .contains(&(label.to_string(), property.to_string()))
432 }
433
434 pub(super) fn has_range_property_index(&self, label: &str, property: &str) -> bool {
435 self.assume_all_indexes
436 || self
437 .range_property_indexes
438 .contains(&(label.to_string(), property.to_string()))
439 }
440
441 pub(super) fn has_full_text_property_index(&self, label: &str, property: &str) -> bool {
442 self.assume_all_indexes
443 || self
444 .full_text_property_indexes
445 .contains(&(label.to_string(), property.to_string()))
446 }
447
448 pub(super) fn has_composite_property_index(&self, label: &str, properties: &[String]) -> bool {
449 self.assume_all_indexes
450 || self
451 .composite_property_indexes
452 .contains(&(label.to_string(), properties.to_vec()))
453 }
454
455 pub(super) fn composite_property_indexes_for_label(&self, label: &str) -> Vec<Vec<String>> {
456 if self.assume_all_indexes {
457 return Vec::new();
458 }
459 self.composite_property_indexes
460 .iter()
461 .filter_map(|(candidate_label, properties)| {
462 if candidate_label == label {
463 Some(properties.clone())
464 } else {
465 None
466 }
467 })
468 .collect()
469 }
470
471 pub(super) fn label_count(&self, label: &str) -> u64 {
472 self.label_counts.get(label).copied().unwrap_or(1).max(1)
473 }
474
475 pub(super) fn relationship_count(&self, rel_type: &str) -> u64 {
476 self.rel_type_counts
477 .get(rel_type)
478 .copied()
479 .unwrap_or(1)
480 .max(1)
481 }
482
483 pub(super) fn path_source_distinct_count(
484 &self,
485 source_label: &str,
486 rel_type: &str,
487 target_label: &str,
488 ) -> Option<u64> {
489 self.path_source_distinct_counts
490 .get(&(
491 source_label.to_string(),
492 rel_type.to_string(),
493 target_label.to_string(),
494 ))
495 .copied()
496 }
497
498 pub(super) fn path_target_distinct_count(
499 &self,
500 source_label: &str,
501 rel_type: &str,
502 target_label: &str,
503 ) -> Option<u64> {
504 self.path_target_distinct_counts
505 .get(&(
506 source_label.to_string(),
507 rel_type.to_string(),
508 target_label.to_string(),
509 ))
510 .copied()
511 }
512
513 pub(super) fn bounded_path_source_distinct_count(
514 &self,
515 source_label: &str,
516 rel_type: &str,
517 target_label: &str,
518 min_hops: usize,
519 max_hops: usize,
520 ) -> Option<u64> {
521 self.bounded_path_distinct_count(
522 &self.bounded_path_source_distinct_counts,
523 source_label,
524 rel_type,
525 target_label,
526 min_hops,
527 max_hops,
528 )
529 }
530
531 pub(super) fn bounded_path_target_distinct_count(
532 &self,
533 source_label: &str,
534 rel_type: &str,
535 target_label: &str,
536 min_hops: usize,
537 max_hops: usize,
538 ) -> Option<u64> {
539 self.bounded_path_distinct_count(
540 &self.bounded_path_target_distinct_counts,
541 source_label,
542 rel_type,
543 target_label,
544 min_hops,
545 max_hops,
546 )
547 }
548
549 pub(super) fn bounded_path_distinct_count(
550 &self,
551 counts: &BTreeMap<(String, String, String, usize), u64>,
552 source_label: &str,
553 rel_type: &str,
554 target_label: &str,
555 min_hops: usize,
556 max_hops: usize,
557 ) -> Option<u64> {
558 let mut total = 0_u64;
559 let mut found = false;
560 for hop in min_hops..=max_hops.max(min_hops) {
561 if let Some(count) = counts.get(&(
562 source_label.to_string(),
563 rel_type.to_string(),
564 target_label.to_string(),
565 hop,
566 )) {
567 found = true;
568 total = total.saturating_add(*count);
569 }
570 }
571 found.then_some(total.max(1))
572 }
573
574 pub(super) fn distinct_count(&self, label: &str, property: &str) -> u64 {
575 self.property_index_statistics
576 .get(&(label.to_string(), property.to_string()))
577 .map(|statistics| statistics.distinct_count)
578 .or_else(|| {
579 self.property_distinct_counts
580 .get(&(label.to_string(), property.to_string()))
581 .copied()
582 })
583 .unwrap_or_else(|| self.label_count(label).max(1))
584 }
585
586 pub(super) fn estimate_property_index_eq_rows(&self, label: &str, property: &str) -> u64 {
587 self.property_index_statistics
588 .get(&(label.to_string(), property.to_string()))
589 .map(|statistics| {
590 statistics
591 .index_size
592 .div_ceil(statistics.distinct_count.max(1))
593 .min(self.label_count(label))
594 .max(1)
595 })
596 .unwrap_or_else(|| {
597 self.label_count(label)
598 .div_ceil(self.distinct_count(label, property).max(1))
599 .max(1)
600 })
601 }
602
603 pub(super) fn estimate_property_index_in_rows(
604 &self,
605 label: &str,
606 property: &str,
607 value_count: u64,
608 ) -> u64 {
609 let upper_bound = self
610 .property_index_statistics
611 .get(&(label.to_string(), property.to_string()))
612 .map_or_else(
613 || self.label_count(label),
614 |statistics| statistics.index_size.min(self.label_count(label)),
615 );
616 self.estimate_property_index_eq_rows(label, property)
617 .saturating_mul(value_count)
618 .min(upper_bound)
619 .max(1)
620 }
621
622 pub(super) fn composite_distinct_count(&self, label: &str, properties: &[String]) -> u64 {
623 self.composite_index_statistics
624 .get(&(label.to_string(), properties.to_vec()))
625 .map(|statistics| statistics.distinct_count)
626 .unwrap_or_else(|| {
627 properties
628 .iter()
629 .map(|property| self.distinct_count(label, property).max(1))
630 .fold(1_u64, |product, distinct| product.saturating_mul(distinct))
631 .max(1)
632 })
633 }
634
635 pub(super) fn estimate_composite_property_index_rows(
636 &self,
637 label: &str,
638 properties: &[String],
639 ) -> u64 {
640 self.composite_index_statistics
641 .get(&(label.to_string(), properties.to_vec()))
642 .map(|statistics| {
643 statistics
644 .index_size
645 .div_ceil(statistics.distinct_count.max(1))
646 .min(self.label_count(label))
647 .max(1)
648 })
649 .unwrap_or_else(|| {
650 self.label_count(label)
651 .div_ceil(self.composite_distinct_count(label, properties))
652 .max(1)
653 })
654 }
655
656 pub(super) fn estimate_composite_prefix_range_rows(
657 &self,
658 label: &str,
659 index_properties: &[String],
660 equality_properties: &[String],
661 range_property: &str,
662 lower: Option<&ValueRangeBound>,
663 upper: Option<&ValueRangeBound>,
664 ) -> u64 {
665 let label_count = self.label_count(label);
666 let equality_distinct = equality_properties
667 .iter()
668 .map(|property| self.distinct_count(label, property).max(1))
669 .fold(1u64, u64::saturating_mul);
670 let equality_rows = label_count.div_ceil(equality_distinct).max(1);
671 let range_rows = self.estimate_range_bounds_rows(label, range_property, lower, upper);
672 let estimated = equality_rows
673 .saturating_mul(range_rows)
674 .div_ceil(label_count)
675 .max(1);
676 self.composite_index_statistics
677 .get(&(label.to_string(), index_properties.to_vec()))
678 .map_or(estimated, |statistics| {
679 estimated.min(statistics.index_size.max(1))
680 })
681 }
682
683 pub(super) fn known_distinct_count(&self, label: &str, property: &str) -> Option<u64> {
684 self.property_index_statistics
685 .get(&(label.to_string(), property.to_string()))
686 .map(|statistics| statistics.distinct_count)
687 .or_else(|| {
688 self.property_distinct_counts
689 .get(&(label.to_string(), property.to_string()))
690 .copied()
691 })
692 }
693
694 pub(super) fn known_rel_property_distinct_count(
695 &self,
696 rel_type: &str,
697 property: &str,
698 ) -> Option<u64> {
699 self.rel_property_distinct_counts
700 .get(&(rel_type.to_string(), property.to_string()))
701 .copied()
702 }
703
704 pub(super) fn rel_property_distinct_count(&self, rel_type: &str, property: &str) -> u64 {
705 self.rel_property_distinct_counts
706 .get(&(rel_type.to_string(), property.to_string()))
707 .copied()
708 .unwrap_or_else(|| {
709 self.rel_type_counts
710 .get(rel_type)
711 .copied()
712 .unwrap_or(1)
713 .max(1)
714 })
715 }
716
717 pub(super) fn estimate_rel_property_eq_rows(
718 &self,
719 rel_type: &str,
720 property: &str,
721 input_rows: u64,
722 ) -> u64 {
723 input_rows
724 .div_ceil(self.rel_property_distinct_count(rel_type, property).max(1))
725 .max(1)
726 }
727
728 pub(super) fn estimate_rel_property_not_eq_rows(
729 &self,
730 rel_type: &str,
731 property: &str,
732 input_rows: u64,
733 ) -> u64 {
734 input_rows
735 .saturating_sub(self.estimate_rel_property_eq_rows(rel_type, property, input_rows))
736 }
737
738 pub(super) fn estimate_rel_property_in_rows(
739 &self,
740 rel_type: &str,
741 property: &str,
742 values: &[Value],
743 input_rows: u64,
744 ) -> u64 {
745 if values.is_empty() {
746 return 0;
747 }
748 let distinct_count = self.rel_property_distinct_count(rel_type, property).max(1);
749 let value_count = values
750 .iter()
751 .collect::<BTreeSet<_>>()
752 .len()
753 .min(distinct_count as usize) as u64;
754 input_rows
755 .saturating_mul(value_count)
756 .div_ceil(distinct_count)
757 .max(1)
758 }
759
760 pub(super) fn estimate_rel_property_string_match_rows(
761 &self,
762 rel_type: &str,
763 property: &str,
764 input_rows: u64,
765 max_divisor: u64,
766 ) -> u64 {
767 let divisor = self
768 .rel_property_distinct_count(rel_type, property)
769 .max(1)
770 .min(max_divisor)
771 .max(1);
772 input_rows.div_ceil(divisor).max(1)
773 }
774
775 pub(super) fn estimate_rel_property_null_rows(
776 &self,
777 rel_type: &str,
778 property: &str,
779 input_rows: u64,
780 ) -> u64 {
781 let distinct_count = self.rel_property_distinct_count(rel_type, property).max(1);
782 input_rows
783 .div_ceil(distinct_count.min(NULL_SELECTIVITY_DIVISOR_CAP))
784 .max(1)
785 }
786
787 pub(super) fn estimate_rel_property_not_null_rows(
788 &self,
789 rel_type: &str,
790 property: &str,
791 input_rows: u64,
792 ) -> u64 {
793 input_rows
794 .saturating_sub(self.estimate_rel_property_null_rows(rel_type, property, input_rows))
795 .max(1)
796 }
797
798 pub(super) fn estimate_rel_property_range_rows(
799 &self,
800 rel_type: &str,
801 property: &str,
802 op: ComparisonOp,
803 value: &Value,
804 input_rows: u64,
805 ) -> u64 {
806 let key = (rel_type.to_string(), property.to_string());
807 let Some(histogram) = self
808 .rel_property_histograms
809 .get(&key)
810 .filter(|values| !values.is_empty())
811 else {
812 return input_rows.div_ceil(RANGE_SELECTIVITY_DIVISOR).max(1);
813 };
814 let matching_values = histogram
815 .iter()
816 .filter(|candidate| compare_histogram_value(candidate, op, value))
817 .count() as u64;
818 estimate_histogram_rows(
819 input_rows,
820 matching_values,
821 histogram.len() as u64,
822 self.sampled_rel_property_histograms
823 .get(&key)
824 .copied()
825 .unwrap_or(false),
826 )
827 }
828
829 pub(super) fn estimate_range_rows(
830 &self,
831 label: &str,
832 property: &str,
833 op: ComparisonOp,
834 value: &Value,
835 ) -> u64 {
836 let label_count = self.label_count(label);
837 let key = (label.to_string(), property.to_string());
838 let Some(histogram) = self
839 .property_histograms
840 .get(&key)
841 .filter(|values| !values.is_empty())
842 else {
843 return label_count.div_ceil(RANGE_SELECTIVITY_DIVISOR).max(1);
844 };
845 let matching_values = histogram
846 .iter()
847 .filter(|candidate| compare_histogram_value(candidate, op, value))
848 .count() as u64;
849 estimate_histogram_rows(
850 label_count,
851 matching_values,
852 histogram.len() as u64,
853 self.sampled_property_histograms
854 .get(&key)
855 .copied()
856 .unwrap_or(false),
857 )
858 }
859
860 pub(super) fn estimate_property_eq_rows(
861 &self,
862 label: &str,
863 property: &str,
864 input_rows: u64,
865 ) -> u64 {
866 input_rows
867 .div_ceil(self.distinct_count(label, property).max(1))
868 .max(1)
869 }
870
871 pub(super) fn estimate_property_not_eq_rows(
872 &self,
873 label: &str,
874 property: &str,
875 input_rows: u64,
876 ) -> u64 {
877 input_rows.saturating_sub(self.estimate_property_eq_rows(label, property, input_rows))
878 }
879
880 pub(super) fn estimate_property_in_rows(
881 &self,
882 label: &str,
883 property: &str,
884 values: &[Value],
885 input_rows: u64,
886 ) -> u64 {
887 if values.is_empty() {
888 return 0;
889 }
890 let distinct_count = self.distinct_count(label, property).max(1);
891 let value_count = values
892 .iter()
893 .collect::<BTreeSet<_>>()
894 .len()
895 .min(distinct_count as usize) as u64;
896 input_rows
897 .saturating_mul(value_count)
898 .div_ceil(distinct_count)
899 .max(1)
900 }
901
902 pub(super) fn estimate_property_string_match_rows(
903 &self,
904 label: &str,
905 property: &str,
906 input_rows: u64,
907 max_divisor: u64,
908 ) -> u64 {
909 let divisor = self
910 .distinct_count(label, property)
911 .max(1)
912 .min(max_divisor)
913 .max(1);
914 input_rows.div_ceil(divisor).max(1)
915 }
916
917 pub(super) fn estimate_property_null_rows(
918 &self,
919 label: &str,
920 property: &str,
921 input_rows: u64,
922 ) -> u64 {
923 let distinct_count = self.distinct_count(label, property).max(1);
924 input_rows
925 .div_ceil(distinct_count.min(NULL_SELECTIVITY_DIVISOR_CAP))
926 .max(1)
927 }
928
929 pub(super) fn estimate_property_not_null_rows(
930 &self,
931 label: &str,
932 property: &str,
933 input_rows: u64,
934 ) -> u64 {
935 input_rows
936 .saturating_sub(self.estimate_property_null_rows(label, property, input_rows))
937 .max(1)
938 }
939
940 pub(super) fn estimate_property_range_rows(
941 &self,
942 label: &str,
943 property: &str,
944 op: ComparisonOp,
945 value: &Value,
946 input_rows: u64,
947 ) -> u64 {
948 let key = (label.to_string(), property.to_string());
949 let Some(histogram) = self
950 .property_histograms
951 .get(&key)
952 .filter(|values| !values.is_empty())
953 else {
954 return input_rows.div_ceil(RANGE_SELECTIVITY_DIVISOR).max(1);
955 };
956 let matching_values = histogram
957 .iter()
958 .filter(|candidate| compare_histogram_value(candidate, op, value))
959 .count() as u64;
960 estimate_histogram_rows(
961 input_rows,
962 matching_values,
963 histogram.len() as u64,
964 self.sampled_property_histograms
965 .get(&key)
966 .copied()
967 .unwrap_or(false),
968 )
969 }
970
971 pub(super) fn estimate_range_bounds_rows(
972 &self,
973 label: &str,
974 property: &str,
975 lower: Option<&ValueRangeBound>,
976 upper: Option<&ValueRangeBound>,
977 ) -> u64 {
978 let label_count = self.label_count(label);
979 let key = (label.to_string(), property.to_string());
980 let Some(histogram) = self
981 .property_histograms
982 .get(&key)
983 .filter(|values| !values.is_empty())
984 else {
985 return label_count.div_ceil(RANGE_SELECTIVITY_DIVISOR).max(1);
986 };
987 let matching_values = histogram
988 .iter()
989 .filter(|candidate| range_bound_matches(candidate, lower, upper))
990 .count() as u64;
991 estimate_histogram_rows(
992 label_count,
993 matching_values,
994 histogram.len() as u64,
995 self.sampled_property_histograms
996 .get(&key)
997 .copied()
998 .unwrap_or(false),
999 )
1000 }
1001
1002 pub(super) fn estimate_expand_rows(
1003 &self,
1004 source_label: &str,
1005 rel_type: &str,
1006 rel_properties: &BTreeMap<String, Value>,
1007 target_label: &str,
1008 min_hops: usize,
1009 max_hops: usize,
1010 ) -> ExpandEstimate {
1011 let path_count = self
1012 .path_counts
1013 .get(&(
1014 source_label.to_string(),
1015 rel_type.to_string(),
1016 target_label.to_string(),
1017 ))
1018 .copied();
1019 let rel_count = self.rel_type_counts.get(rel_type).copied().unwrap_or(1);
1020 let source_count = self
1021 .rel_type_source_counts
1022 .get(rel_type)
1023 .copied()
1024 .unwrap_or_else(|| self.label_count(source_label).max(1));
1025 let average_fanout = rel_count.div_ceil(source_count.max(1)).max(1);
1026 let one_hop = path_count.unwrap_or_else(|| {
1027 self.label_count(source_label)
1028 .min(self.label_count(target_label))
1029 .max(1)
1030 });
1031 let property_distinct_product = rel_properties
1032 .keys()
1033 .map(|property| self.rel_property_distinct_count(rel_type, property).max(1))
1034 .fold(1_u64, |acc, value| acc.saturating_mul(value))
1035 .max(1);
1036 let mut estimated_rows = 0_u64;
1037 let mut hop_rows = one_hop.max(1);
1038 let mut hop_estimates = Vec::new();
1039 for hop in 1..=max_hops.max(1) {
1040 let exact_hop_rows = self
1041 .bounded_path_counts
1042 .get(&(
1043 source_label.to_string(),
1044 rel_type.to_string(),
1045 target_label.to_string(),
1046 hop,
1047 ))
1048 .copied();
1049 let current_hop_rows = exact_hop_rows
1050 .unwrap_or(hop_rows)
1051 .div_ceil(property_distinct_product)
1052 .max(1);
1053 hop_estimates.push(HopEstimate {
1054 hop,
1055 rows: current_hop_rows,
1056 exact: exact_hop_rows.is_some(),
1057 });
1058 if hop >= min_hops {
1059 estimated_rows = estimated_rows.saturating_add(current_hop_rows);
1060 }
1061 hop_rows = current_hop_rows.max(1).saturating_mul(average_fanout);
1062 }
1063 ExpandEstimate {
1064 path_count,
1065 rel_count,
1066 source_count,
1067 average_fanout,
1068 property_distinct_product,
1069 estimated_rows: estimated_rows.max(1),
1070 hop_estimates,
1071 }
1072 }
1073}
1074
1075fn estimate_histogram_rows(
1076 input_rows: u64,
1077 matching_values: u64,
1078 histogram_values: u64,
1079 sampled: bool,
1080) -> u64 {
1081 let (matching_values, histogram_values) = if sampled {
1085 (
1086 matching_values.saturating_add(SAMPLED_HISTOGRAM_MATCH_PSEUDOCOUNT),
1087 histogram_values.saturating_add(SAMPLED_HISTOGRAM_TOTAL_PSEUDOCOUNT),
1088 )
1089 } else {
1090 (matching_values, histogram_values)
1091 };
1092 input_rows
1093 .saturating_mul(matching_values)
1094 .div_ceil(histogram_values.max(1))
1095 .max(1)
1096}
1097
1098impl OptimizerCatalogIndexes {
1099 pub fn new(
1100 equality_property_indexes: impl IntoIterator<Item = (String, String)>,
1101 composite_property_indexes: impl IntoIterator<Item = (String, Vec<String>)>,
1102 range_property_indexes: impl IntoIterator<Item = (String, String)>,
1103 full_text_property_indexes: impl IntoIterator<Item = (String, String)>,
1104 ) -> Self {
1105 Self {
1106 equality_property_indexes: equality_property_indexes.into_iter().collect(),
1107 composite_property_indexes: composite_property_indexes.into_iter().collect(),
1108 range_property_indexes: range_property_indexes.into_iter().collect(),
1109 full_text_property_indexes: full_text_property_indexes.into_iter().collect(),
1110 }
1111 }
1112}
1113
1114impl OptimizerCatalogStatistics {
1115 pub fn new(
1116 label_counts: impl IntoIterator<Item = (String, u64)>,
1117 rel_type_counts: impl IntoIterator<Item = (String, u64)>,
1118 rel_type_source_counts: impl IntoIterator<Item = (String, u64)>,
1119 path_counts: impl IntoIterator<Item = ((String, String, String), u64)>,
1120 bounded_path_counts: impl IntoIterator<Item = ((String, String, String, usize), u64)>,
1121 property_distinct_counts: impl IntoIterator<Item = ((String, String), u64)>,
1122 property_histograms: impl IntoIterator<Item = ((String, String), Vec<Value>)>,
1123 ) -> Self {
1124 Self {
1125 label_counts: label_counts.into_iter().collect(),
1126 rel_type_counts: rel_type_counts.into_iter().collect(),
1127 rel_type_source_counts: rel_type_source_counts.into_iter().collect(),
1128 rel_type_target_counts: BTreeMap::new(),
1129 path_counts: path_counts.into_iter().collect(),
1130 path_source_distinct_counts: BTreeMap::new(),
1131 path_target_distinct_counts: BTreeMap::new(),
1132 bounded_path_counts: bounded_path_counts.into_iter().collect(),
1133 bounded_path_source_distinct_counts: BTreeMap::new(),
1134 bounded_path_target_distinct_counts: BTreeMap::new(),
1135 property_index_statistics: BTreeMap::new(),
1136 composite_index_statistics: BTreeMap::new(),
1137 property_distinct_counts: property_distinct_counts.into_iter().collect(),
1138 rel_property_distinct_counts: BTreeMap::new(),
1139 property_histograms: property_histograms.into_iter().collect(),
1140 rel_property_histograms: BTreeMap::new(),
1141 sampled_property_histograms: BTreeMap::new(),
1142 sampled_rel_property_histograms: BTreeMap::new(),
1143 }
1144 }
1145
1146 pub fn with_property_index_statistics(
1147 mut self,
1148 statistics: impl IntoIterator<Item = ((String, String), OptimizerIndexStatistics)>,
1149 ) -> Self {
1150 self.property_index_statistics = statistics.into_iter().collect();
1151 self
1152 }
1153
1154 pub fn with_composite_index_statistics(
1155 mut self,
1156 statistics: impl IntoIterator<Item = ((String, Vec<String>), OptimizerIndexStatistics)>,
1157 ) -> Self {
1158 self.composite_index_statistics = statistics.into_iter().collect();
1159 self
1160 }
1161
1162 pub fn with_relationship_property_distinct_counts(
1163 mut self,
1164 rel_property_distinct_counts: impl IntoIterator<Item = ((String, String), u64)>,
1165 ) -> Self {
1166 self.rel_property_distinct_counts = rel_property_distinct_counts.into_iter().collect();
1167 self
1168 }
1169
1170 pub fn with_relationship_type_target_counts(
1171 mut self,
1172 rel_type_target_counts: impl IntoIterator<Item = (String, u64)>,
1173 ) -> Self {
1174 self.rel_type_target_counts = rel_type_target_counts.into_iter().collect();
1175 self
1176 }
1177
1178 pub fn with_path_source_distinct_counts(
1179 mut self,
1180 path_source_distinct_counts: impl IntoIterator<Item = ((String, String, String), u64)>,
1181 ) -> Self {
1182 self.path_source_distinct_counts = path_source_distinct_counts.into_iter().collect();
1183 self
1184 }
1185
1186 pub fn with_path_target_distinct_counts(
1187 mut self,
1188 path_target_distinct_counts: impl IntoIterator<Item = ((String, String, String), u64)>,
1189 ) -> Self {
1190 self.path_target_distinct_counts = path_target_distinct_counts.into_iter().collect();
1191 self
1192 }
1193
1194 pub fn with_bounded_path_source_distinct_counts(
1195 mut self,
1196 bounded_path_source_distinct_counts: impl IntoIterator<
1197 Item = ((String, String, String, usize), u64),
1198 >,
1199 ) -> Self {
1200 self.bounded_path_source_distinct_counts =
1201 bounded_path_source_distinct_counts.into_iter().collect();
1202 self
1203 }
1204
1205 pub fn with_bounded_path_target_distinct_counts(
1206 mut self,
1207 bounded_path_target_distinct_counts: impl IntoIterator<
1208 Item = ((String, String, String, usize), u64),
1209 >,
1210 ) -> Self {
1211 self.bounded_path_target_distinct_counts =
1212 bounded_path_target_distinct_counts.into_iter().collect();
1213 self
1214 }
1215
1216 pub fn with_relationship_property_histograms(
1217 mut self,
1218 rel_property_histograms: impl IntoIterator<Item = ((String, String), Vec<Value>)>,
1219 ) -> Self {
1220 self.rel_property_histograms = rel_property_histograms.into_iter().collect();
1221 self
1222 }
1223
1224 pub fn with_sampled_property_histograms(
1225 mut self,
1226 sampled_property_histograms: impl IntoIterator<Item = ((String, String), bool)>,
1227 ) -> Self {
1228 self.sampled_property_histograms = sampled_property_histograms.into_iter().collect();
1229 self
1230 }
1231
1232 pub fn with_sampled_relationship_property_histograms(
1233 mut self,
1234 sampled_rel_property_histograms: impl IntoIterator<Item = ((String, String), bool)>,
1235 ) -> Self {
1236 self.sampled_rel_property_histograms =
1237 sampled_rel_property_histograms.into_iter().collect();
1238 self
1239 }
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244 use super::*;
1245
1246 #[test]
1247 fn graph_statistics_catalog_keeps_basic_counts_when_advanced_stats_are_incomplete() {
1248 let mut source_catalog = Catalog::default();
1249 let memory = source_catalog.get_or_create_label("Memory");
1250 let mut statistics = GraphStatistics {
1251 advanced_statistics_complete: false,
1252 label_counts: BTreeMap::from([(memory, 64)]),
1253 property_distinct_counts: BTreeMap::from([((memory, "kind".to_string()), 4)]),
1254 property_histograms: BTreeMap::from([(
1255 (memory, "kind".to_string()),
1256 vec![Value::String("note".to_string())],
1257 )]),
1258 ..GraphStatistics::default()
1259 };
1260
1261 let incomplete = optimizer_catalog_from_graph_statistics(&source_catalog, &statistics);
1262 assert_eq!(incomplete.label_count("Memory"), 64);
1263 assert_eq!(incomplete.known_distinct_count("Memory", "kind"), None);
1264 assert!(incomplete.property_histograms.is_empty());
1265
1266 statistics.advanced_statistics_complete = true;
1267 let complete = optimizer_catalog_from_graph_statistics(&source_catalog, &statistics);
1268 assert_eq!(complete.label_count("Memory"), 64);
1269 assert_eq!(complete.known_distinct_count("Memory", "kind"), Some(4));
1270 assert_eq!(
1271 complete.property_histograms,
1272 BTreeMap::from([(
1273 ("Memory".to_string(), "kind".to_string()),
1274 vec![Value::String("note".to_string())],
1275 )])
1276 );
1277 }
1278
1279 #[test]
1280 fn range_cardinality_honors_half_open_histogram_bounds() {
1281 let catalog = OptimizerCatalog::new(
1282 OptimizerCatalogIndexes::default(),
1283 OptimizerCatalogStatistics::new(
1284 [("Memory".to_string(), 1_000)],
1285 [],
1286 [],
1287 [],
1288 [],
1289 [],
1290 [(
1291 ("Memory".to_string(), "created_at".to_string()),
1292 (0..100).map(Value::Int).collect::<Vec<_>>(),
1293 )],
1294 ),
1295 );
1296
1297 assert_eq!(
1298 catalog.estimate_range_bounds_rows(
1299 "Memory",
1300 "created_at",
1301 Some(&(Value::Int(10), true)),
1302 Some(&(Value::Int(20), false)),
1303 ),
1304 100
1305 );
1306 }
1307
1308 #[test]
1309 fn sampled_histograms_shrink_range_selectivity_toward_default() {
1310 let property_key = ("Memory".to_string(), "score".to_string());
1311 let rel_property_key = ("MENTIONS".to_string(), "score".to_string());
1312 let histogram = (0..100).map(Value::Int).collect::<Vec<_>>();
1313 let statistics = OptimizerCatalogStatistics::new(
1314 [("Memory".to_string(), 1_000)],
1315 [],
1316 [],
1317 [],
1318 [],
1319 [(property_key.clone(), 100)],
1320 [(property_key.clone(), histogram.clone())],
1321 )
1322 .with_relationship_property_histograms([(rel_property_key.clone(), histogram)]);
1323 let exhaustive =
1324 OptimizerCatalog::new(OptimizerCatalogIndexes::default(), statistics.clone());
1325 let sampled = OptimizerCatalog::new(
1326 OptimizerCatalogIndexes::default(),
1327 statistics
1328 .with_sampled_property_histograms([(property_key, true)])
1329 .with_sampled_relationship_property_histograms([(rel_property_key, true)]),
1330 );
1331
1332 assert_eq!(
1333 exhaustive.estimate_range_rows("Memory", "score", ComparisonOp::Lt, &Value::Int(10)),
1334 100
1335 );
1336 assert_eq!(
1337 sampled.estimate_range_rows("Memory", "score", ComparisonOp::Lt, &Value::Int(10)),
1338 108
1339 );
1340 assert_eq!(
1341 sampled.estimate_property_range_rows(
1342 "Memory",
1343 "score",
1344 ComparisonOp::Lt,
1345 &Value::Int(10),
1346 1_000,
1347 ),
1348 108
1349 );
1350 assert_eq!(
1351 sampled.estimate_range_bounds_rows(
1352 "Memory",
1353 "score",
1354 None,
1355 Some(&(Value::Int(10), false)),
1356 ),
1357 108
1358 );
1359 assert_eq!(
1360 exhaustive.estimate_rel_property_range_rows(
1361 "MENTIONS",
1362 "score",
1363 ComparisonOp::Lt,
1364 &Value::Int(10),
1365 1_000,
1366 ),
1367 100
1368 );
1369 assert_eq!(
1370 sampled.estimate_rel_property_range_rows(
1371 "MENTIONS",
1372 "score",
1373 ComparisonOp::Lt,
1374 &Value::Int(10),
1375 1_000,
1376 ),
1377 108
1378 );
1379 }
1380
1381 #[test]
1382 fn index_cardinality_uses_sparse_index_size_and_joint_ndv() {
1383 let statistics = OptimizerCatalogStatistics {
1384 label_counts: BTreeMap::from([("Memory".to_string(), 100)]),
1385 property_index_statistics: BTreeMap::from([(
1386 ("Memory".to_string(), "kind".to_string()),
1387 OptimizerIndexStatistics {
1388 index_size: 10,
1389 distinct_count: 2,
1390 },
1391 )]),
1392 composite_index_statistics: BTreeMap::from([(
1393 (
1394 "Memory".to_string(),
1395 vec!["kind".to_string(), "source_id".to_string()],
1396 ),
1397 OptimizerIndexStatistics {
1398 index_size: 8,
1399 distinct_count: 4,
1400 },
1401 )]),
1402 ..OptimizerCatalogStatistics::default()
1403 };
1404 let catalog = OptimizerCatalog::new(OptimizerCatalogIndexes::default(), statistics);
1405
1406 assert_eq!(catalog.estimate_property_index_eq_rows("Memory", "kind"), 5);
1407 assert_eq!(
1408 catalog.estimate_property_index_in_rows("Memory", "kind", 10),
1409 10
1410 );
1411 assert_eq!(
1412 catalog.estimate_composite_property_index_rows(
1413 "Memory",
1414 &["kind".to_string(), "source_id".to_string()],
1415 ),
1416 2
1417 );
1418 }
1419}