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