drive/query/drive_document_count_query/index_picker.rs
1//! Index pickers for the count query.
2//!
3//! Pure functions on the document type's index map + where clauses;
4//! no Drive, no proof. Picks a covering index for a given query
5//! shape, returning `None` if no index can serve the query.
6
7use super::super::conditions::WhereClause;
8use super::DriveDocumentCountQuery;
9use crate::query::index_admissible_for_resolved_time_range;
10use crate::query::ResolvedTimeRange;
11use dpp::data_contract::document_type::Index;
12use std::collections::{BTreeMap, BTreeSet};
13
14impl DriveDocumentCountQuery<'_> {
15 /// Finds a `countable: true` index whose properties **exactly match** the
16 /// indexable (Equal/In) where-clause fields — every index property has a
17 /// corresponding clause AND every clause's field appears in the index —
18 /// or, failing that, a `rangeCountable: true` index whose properties
19 /// match the clause fields **plus one trailing free property** — or,
20 /// failing that, an index with a prefix-level ranking
21 /// (`rankedCountable: { at }`) whose count-bearing chain reaches the
22 /// deepest pinned property, serving contiguous pins of **any** depth.
23 ///
24 /// Exact coverage is the preferred contract for both no-proof and prove
25 /// count paths: a countable index counts exactly what it indexes. The
26 /// prefix-to-last fallback exists because a `rangeCountable` index also
27 /// maintains one aggregate no exact-coverage query can reach — its
28 /// terminal property-name tree is count-bearing, and that tree's own
29 /// element carries the **whole-prefix** total (every last-property value
30 /// tree contributes its count to it). So `count WHERE hashtag == X` on
31 /// `[hashtag, postId]` is one element read at `…/hashtag/X/postId`, not
32 /// a walk — the same shape as the exact form, one level up. Any other
33 /// partial coverage stays rejected: intermediate levels carry no
34 /// aggregates, so there is nothing cheap to read.
35 ///
36 /// Returns `None` if:
37 /// - Any where clause uses an operator other than `Equal` / `In`.
38 /// - The set of indexable where-clause fields neither exactly equals the
39 /// property set of a `countable: true` index nor exactly covers all
40 /// but the last property of a `rangeCountable: true` index.
41 ///
42 /// For the `documents_countable: true` case (total count with no where
43 /// clauses), the dispatcher reads the document-type primary-key tree's
44 /// CountTree directly — that path doesn't use this picker because no
45 /// index is involved.
46 ///
47 /// `resolved_time_ranges` names the fields whose equality clause was
48 /// produced by `IN_TIME_RANGE` resolution (see
49 /// [`crate::query::resolve_time_range_bucket_clause`]); it gates which
50 /// indexes are candidates at all — see
51 /// [`index_admissible_for_resolved_time_range`].
52 pub fn find_countable_index_for_where_clauses<'b>(
53 indexes: &'b BTreeMap<String, Index>,
54 where_clauses: &[WhereClause],
55 resolved_time_ranges: &[ResolvedTimeRange],
56 ) -> Option<&'b Index> {
57 if Self::has_unsupported_operator(where_clauses) {
58 return None;
59 }
60
61 let indexable_fields: BTreeSet<&str> = where_clauses
62 .iter()
63 .filter(|wc| Self::is_indexable_for_count(wc.operator))
64 .map(|wc| wc.field.as_str())
65 .collect();
66
67 // Need a clause for every property of the index, so empty
68 // `indexable_fields` only matches an empty-properties index
69 // (which doesn't exist — indexes always have at least one
70 // property — so empty where clauses never match here).
71 if indexable_fields.is_empty() {
72 return None;
73 }
74
75 for index in indexes.values() {
76 // A time-range index holds one entry per bucket containing the
77 // document, keyed by bucket start: counting over it multi-counts
78 // every document unless the query pins a single bucket, and only
79 // a resolution-produced equality does that. Conversely a raw
80 // clause must never bind to bucket keys.
81 if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) {
82 continue;
83 }
84 if !index.countable.is_countable() {
85 continue;
86 }
87 if index.properties.len() != indexable_fields.len() {
88 continue;
89 }
90 // Every index property must have a matching where-clause
91 // field. Because lengths match, this also implies every
92 // where-clause field appears in the index (no orphan
93 // clauses).
94 let all_covered = index
95 .properties
96 .iter()
97 .all(|prop| indexable_fields.contains(prop.name.as_str()));
98 if all_covered {
99 return Some(index);
100 }
101 }
102
103 // Prefix-to-last fallback: exactly the first `len - 1` properties
104 // are covered and the last one is free — servable only when the
105 // terminal property-name tree is count-bearing, i.e.
106 // `rangeCountable`. The exact form above stays preferred so a
107 // shorter exact index (one merk layer cheaper) keeps winning when
108 // both exist. Position matters here, unlike the set-equality
109 // form: a clause on the LAST property with an earlier one free is
110 // not a prefix and reads nothing meaningful.
111 for index in indexes.values() {
112 if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) {
113 continue;
114 }
115 if !index.range_countable || !index.countable.is_countable() {
116 continue;
117 }
118 // A ranked axis makes the terminal property-name tree an
119 // INDEXED tree, which grovedb's query dispatch refuses to
120 // return as a result element ("path_queries can not refer to
121 // trees") — the element read this form performs would have
122 // nothing legal to select. Skipped until that dispatch admits
123 // indexed elements; a prefix-level ranking
124 // (`rankedCountable: { at }`) keeps its terminal non-indexed
125 // and stays servable.
126 if index.ranked_countable || index.ranked_summable || index.ranked_averageable {
127 continue;
128 }
129 let Some(leading_len) = index.properties.len().checked_sub(1) else {
130 continue;
131 };
132 if leading_len != indexable_fields.len() || leading_len == 0 {
133 continue;
134 }
135 let leading_covered = index.properties[..leading_len]
136 .iter()
137 .all(|prop| indexable_fields.contains(prop.name.as_str()));
138 if leading_covered {
139 return Some(index);
140 }
141 }
142
143 // At-chain value-tree fallback: on an index with a prefix-level
144 // ranking (`rankedCountable: { at }`), every level from the
145 // shallowest `at` property down is count-bearing — its value
146 // trees are `CountTree`s whose count IS the whole-subtree total
147 // — so contiguous pins of ANY depth k landing at or below that
148 // level are servable by reading the deepest pin's value tree
149 // element. This also covers what the loop above cannot: a
150 // ranked-terminal (`at` + boolean) index, since the value-tree
151 // read never touches the indexed property-name tree grovedb
152 // refuses to return. Pins landing ABOVE the shallowest `at`
153 // level stay rejected — those levels are plain trees.
154 for index in indexes.values() {
155 if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) {
156 continue;
157 }
158 if !index.countable.is_countable() {
159 continue;
160 }
161 let pin_depth = indexable_fields.len();
162 if pin_depth == 0 || pin_depth >= index.properties.len() {
163 continue;
164 }
165 let Some(min_at_position) = index
166 .ranked_countable_at
167 .iter()
168 .filter_map(|at| index.properties.iter().position(|p| &p.name == at))
169 .min()
170 else {
171 continue;
172 };
173 // The deepest pinned property (position pin_depth - 1) must
174 // sit at or below the shallowest ranked level.
175 if min_at_position > pin_depth - 1 {
176 continue;
177 }
178 let leading_covered = index.properties[..pin_depth]
179 .iter()
180 .all(|prop| indexable_fields.contains(prop.name.as_str()));
181 if leading_covered {
182 return Some(index);
183 }
184 }
185
186 None
187 }
188
189 /// Finds a `range_countable` index that can serve a range-count query.
190 ///
191 /// Match criteria:
192 /// - All `Equal`/`In` where-clause fields form a prefix of the index
193 /// properties.
194 /// - There is exactly one range-operator where-clause, on a property
195 /// that is the *last* property of the index (the IndexLevel
196 /// terminator). This is the property whose values get walked.
197 /// - The index has `range_countable = true` and `countable.is_countable()`.
198 ///
199 /// Returns `None` if no such index exists or if there's more than one
200 /// range operator in the where clauses (which would require nested range
201 /// walks the current model doesn't support). Pure point-lookup queries
202 /// (no range operator) should fall back to
203 /// [`Self::find_countable_index_for_where_clauses`].
204 ///
205 /// `resolved_time_ranges` gates the candidate set exactly as in
206 /// [`Self::find_countable_index_for_where_clauses`]. A resolved field
207 /// never arrives as a range clause — resolution always produces an
208 /// equality — so with a non-empty list the only bucketed index this can
209 /// return is one whose resolved equality is a prefix property and whose
210 /// range terminator is a different property. That is the intended shape:
211 /// a range over one property within a single time bucket.
212 pub fn find_range_countable_index_for_where_clauses<'b>(
213 indexes: &'b BTreeMap<String, Index>,
214 where_clauses: &[WhereClause],
215 resolved_time_ranges: &[ResolvedTimeRange],
216 ) -> Option<&'b Index> {
217 let range_clauses: Vec<&WhereClause> = where_clauses
218 .iter()
219 .filter(|wc| Self::is_range_operator(wc.operator))
220 .collect();
221 // Accept either:
222 // - 1 range clause (Q7 / G4 / G5 / G7 — the range is the
223 // terminator; prefix props use `==` or `In`).
224 // - 2 range clauses on distinct fields (G8 — outer range on
225 // an index prefix property, inner range on the terminator;
226 // the carrier-aggregate proof shape introduced by grovedb
227 // PR #664).
228 let (outer_range_field, terminator_range_clause) = match range_clauses.len() {
229 1 => (None, range_clauses[0]),
230 2 => {
231 // The two ranges must be on different fields — same-
232 // field two-sided ranges are flattened by the parser
233 // into `between*` and arrive with one clause.
234 if range_clauses[0].field == range_clauses[1].field {
235 return None;
236 }
237 // One of the two must be on an index's terminator; the
238 // other becomes the outer carrier dimension. We pick
239 // the terminator below by walking each candidate
240 // index's property order — defer choosing here.
241 (
242 Some((
243 range_clauses[0].field.as_str(),
244 range_clauses[1].field.as_str(),
245 )),
246 range_clauses[0], // placeholder, refined per-index below
247 )
248 }
249 _ => return None,
250 };
251
252 // Reject any operator that's neither indexable (Equal/In) nor a
253 // range operator — anything else has no defined count semantics.
254 if where_clauses.iter().any(|wc| {
255 !Self::is_indexable_for_count(wc.operator) && !Self::is_range_operator(wc.operator)
256 }) {
257 return None;
258 }
259
260 let prefix_fields: BTreeSet<&str> = where_clauses
261 .iter()
262 .filter(|wc| Self::is_indexable_for_count(wc.operator))
263 .map(|wc| wc.field.as_str())
264 .collect();
265
266 for index in indexes.values() {
267 // Same admissibility rule as the point-lookup picker: bucketed
268 // indexes store one entry per containing bucket, so only a query
269 // pinned to a single bucket by a resolution-produced equality may
270 // walk them, and raw clauses may never bind to bucket keys.
271 if !index_admissible_for_resolved_time_range(index, resolved_time_ranges) {
272 continue;
273 }
274 if !index.range_countable || !index.countable.is_countable() {
275 continue;
276 }
277
278 // For the two-range case, the terminator's field must be
279 // one of the two range fields, and the other range field
280 // must be the index's first property (the carrier
281 // dimension).
282 if let Some((field_a, field_b)) = outer_range_field {
283 let terminator = index.properties.last()?;
284 let first = index.properties.first()?;
285 // Determine which range field is the terminator.
286 let (outer_field, _terminator_field) = if terminator.name == field_a {
287 (field_b, field_a)
288 } else if terminator.name == field_b {
289 (field_a, field_b)
290 } else {
291 continue;
292 };
293 if first.name != outer_field {
294 continue;
295 }
296 // Any Equal/In prefix clauses must sit between the
297 // first (outer-range) and last (terminator-range)
298 // properties. For the widget contract there are no
299 // such middle properties on byBrandColor, but the
300 // builder handles the general case.
301 let intermediate_props = &index.properties[1..index.properties.len() - 1];
302 let mut intermediate_props_ok = true;
303 for prop in intermediate_props {
304 if !prefix_fields.contains(prop.name.as_str()) {
305 intermediate_props_ok = false;
306 break;
307 }
308 }
309 // Strict-coverage check, mirroring sum's picker: every
310 // Equal/In prefix field must appear in the index's
311 // intermediate properties. Without this
312 // `intermediate_props.len() == prefix_fields.len()` guard,
313 // a query with extra prefix fields would silently pick an
314 // index that *doesn't* cover them — the carrier path-query
315 // builder iterates only index properties, so the uncovered
316 // clause would simply be dropped and the per-group counts
317 // would span all its values (an over-broad result that
318 // even verifies, since the verifier rebuilds the same
319 // path query from the same picker).
320 if intermediate_props_ok && intermediate_props.len() == prefix_fields.len() {
321 return Some(index);
322 }
323 continue;
324 }
325
326 // Single-range case (the original logic): prefix matches
327 // must come first, followed by the range property as the
328 // LAST element.
329 let mut prefix_len = 0usize;
330 for prop in &index.properties {
331 if prefix_fields.contains(prop.name.as_str()) {
332 prefix_len += 1;
333 } else {
334 break;
335 }
336 }
337 if prefix_len < prefix_fields.len() {
338 continue;
339 }
340 if prefix_len + 1 != index.properties.len() {
341 // Range property must be the terminator (last property).
342 continue;
343 }
344 let range_prop = &index.properties[prefix_len];
345 if range_prop.name == terminator_range_clause.field {
346 return Some(index);
347 }
348 }
349
350 None
351 }
352}