rushdb_query/planner/
explain.rs1use rushdb_lmpg::reader::TryRead;
5use rushdb_types::model::LogicalType;
6
7use super::{
8 collect_leaves, expansions, family_id, is_range_leaf, labels_for, leaf_estimate, normalize,
9 property_id, target_estimate,
10};
11use crate::{Expr, FieldPred, QueryError};
12
13pub fn explain_cardinality<R: TryRead>(
21 snap: &R,
22 tenant: rushdb_types::ids::TenantId,
23 labels: &[String],
24 where_: &Expr,
25) -> Result<Option<u64>, QueryError> {
26 let mut leaves = Vec::new();
27 collect_leaves(where_, &mut leaves);
28 let mut best: Option<u64> = None;
29 for leaf in leaves {
30 if is_range_leaf(leaf) {
36 continue;
37 }
38 if let Some(estimate) = leaf_estimate(snap, tenant, leaf, labels)? {
39 if estimate != u64::MAX {
42 best = Some(best.map_or(estimate, |current| current.min(estimate)));
43 }
44 }
45 }
46 Ok(best)
47}
48
49pub fn explain_walk<R: TryRead>(
57 snap: &R,
58 tenant: rushdb_types::ids::TenantId,
59 labels: &[String],
60 where_: &Expr,
61) -> Result<Option<(String, u64, Vec<String>)>, QueryError> {
62 let mut leaves = Vec::new();
63 collect_leaves(where_, &mut leaves);
64 let mut eligible = Vec::new();
65 for leaf in leaves {
66 if let Some(estimate) = leaf_estimate(snap, tenant, leaf, labels)? {
67 eligible.push((estimate, leaf));
68 }
69 }
70 eligible.sort_by_key(|(estimate, _)| *estimate);
71 let Some((_, leaf)) = eligible.first() else {
72 return Ok(None);
73 };
74 if crate::index_access::answers(snap, tenant, leaf, labels)? {
75 return Ok(None);
76 }
77 let Expr::Field(
78 FieldPred::Eq(name, _)
79 | FieldPred::In(name, _)
80 | FieldPred::Gt(name, _)
81 | FieldPred::Gte(name, _)
82 | FieldPred::Lt(name, _)
83 | FieldPred::Lte(name, _),
84 ) = leaf
85 else {
86 return Ok(None);
87 };
88 let norm = normalize(name);
89 let Some(fid) = family_id(snap, tenant, &norm)? else {
90 return Ok(None);
91 };
92 let kind = rushdb_types::ids::OwnerKind::Record;
93 let mut owners = 0u64;
94 let mut walked_labels = std::collections::BTreeSet::new();
95 for ty in [
96 LogicalType::Utf8,
97 LogicalType::Int64,
98 LogicalType::Float64,
99 LogicalType::Boolean,
100 LogicalType::DateTime,
101 ] {
102 let Some(pid) = property_id(snap, tenant, fid, ty)? else {
103 continue;
104 };
105 for label in labels_for(snap, tenant, pid, labels)? {
106 owners = owners.saturating_add(rushdb_lmpg::derived::prop_support_label(
107 snap, tenant, kind, pid, &label,
108 )?);
109 walked_labels.insert(label);
110 }
111 }
112 Ok(Some((norm, owners, walked_labels.into_iter().collect())))
113}
114
115pub fn explain_expansion<R: TryRead>(
123 snap: &R,
124 tenant: rushdb_types::ids::TenantId,
125 labels: &[String],
126 where_: &Expr,
127) -> Result<Option<u64>, QueryError> {
128 let mut leaves = Vec::new();
129 collect_leaves(where_, &mut leaves);
130 let mut eligible = Vec::new();
131 let mut traversals = Vec::new();
132 for leaf in leaves {
133 match leaf {
134 Expr::Related(rel) if !rel.cycle => traversals.push(rel),
135 _ => {
136 if let Some(estimate) = leaf_estimate(snap, tenant, leaf, labels)? {
137 eligible.push((estimate, leaf));
138 }
139 }
140 }
141 }
142 let mut best: Option<u64> = None;
143 for (_, rel) in expansions(snap, tenant, labels, &eligible, traversals)?.expand {
144 let targets = target_estimate(snap, tenant, rel)?;
145 best = Some(best.map_or(targets, |current| current.min(targets)));
146 }
147 Ok(best)
148}