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