Skip to main content

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}