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 dpp::data_contract::document_type::Index;
10use std::collections::{BTreeMap, BTreeSet};
11
12impl DriveDocumentCountQuery<'_> {
13 /// Finds a `countable: true` index whose properties **exactly match** the
14 /// indexable (Equal/In) where-clause fields — every index property has a
15 /// corresponding clause AND every clause's field appears in the index.
16 ///
17 /// Exact coverage is the contract for both no-proof and prove count
18 /// paths: a countable index counts exactly what it indexes, and queries
19 /// against partially-covered indexes are rejected with a clear error
20 /// directing the caller at the index-design fix. This avoids the
21 /// product-of-uncovered-branching-factors walk that a prefix-match
22 /// approach would silently fall through to, and keeps the storage's
23 /// "count maintained only at the terminal level" trade-off intact (no
24 /// need to maintain counts at intermediate index levels just to serve
25 /// partial-coverage queries cheaply).
26 ///
27 /// Returns `None` if:
28 /// - Any where clause uses an operator other than `Equal` / `In`.
29 /// - The set of indexable where-clause fields doesn't exactly equal the
30 /// set of properties of any single `countable: true` index.
31 ///
32 /// For the `documents_countable: true` case (total count with no where
33 /// clauses), the dispatcher reads the document-type primary-key tree's
34 /// CountTree directly — that path doesn't use this picker because no
35 /// index is involved.
36 pub fn find_countable_index_for_where_clauses<'b>(
37 indexes: &'b BTreeMap<String, Index>,
38 where_clauses: &[WhereClause],
39 ) -> Option<&'b Index> {
40 if Self::has_unsupported_operator(where_clauses) {
41 return None;
42 }
43
44 let indexable_fields: BTreeSet<&str> = where_clauses
45 .iter()
46 .filter(|wc| Self::is_indexable_for_count(wc.operator))
47 .map(|wc| wc.field.as_str())
48 .collect();
49
50 // Need a clause for every property of the index, so empty
51 // `indexable_fields` only matches an empty-properties index
52 // (which doesn't exist — indexes always have at least one
53 // property — so empty where clauses never match here).
54 if indexable_fields.is_empty() {
55 return None;
56 }
57
58 for index in indexes.values() {
59 if !index.countable.is_countable() {
60 continue;
61 }
62 if index.properties.len() != indexable_fields.len() {
63 continue;
64 }
65 // Every index property must have a matching where-clause
66 // field. Because lengths match, this also implies every
67 // where-clause field appears in the index (no orphan
68 // clauses).
69 let all_covered = index
70 .properties
71 .iter()
72 .all(|prop| indexable_fields.contains(prop.name.as_str()));
73 if all_covered {
74 return Some(index);
75 }
76 }
77
78 None
79 }
80
81 /// Finds a `range_countable` index that can serve a range-count query.
82 ///
83 /// Match criteria:
84 /// - All `Equal`/`In` where-clause fields form a prefix of the index
85 /// properties.
86 /// - There is exactly one range-operator where-clause, on a property
87 /// that is the *last* property of the index (the IndexLevel
88 /// terminator). This is the property whose values get walked.
89 /// - The index has `range_countable = true` and `countable.is_countable()`.
90 ///
91 /// Returns `None` if no such index exists or if there's more than one
92 /// range operator in the where clauses (which would require nested range
93 /// walks the current model doesn't support). Pure point-lookup queries
94 /// (no range operator) should fall back to
95 /// [`Self::find_countable_index_for_where_clauses`].
96 pub fn find_range_countable_index_for_where_clauses<'b>(
97 indexes: &'b BTreeMap<String, Index>,
98 where_clauses: &[WhereClause],
99 ) -> Option<&'b Index> {
100 let range_clauses: Vec<&WhereClause> = where_clauses
101 .iter()
102 .filter(|wc| Self::is_range_operator(wc.operator))
103 .collect();
104 // Accept either:
105 // - 1 range clause (Q7 / G4 / G5 / G7 — the range is the
106 // terminator; prefix props use `==` or `In`).
107 // - 2 range clauses on distinct fields (G8 — outer range on
108 // an index prefix property, inner range on the terminator;
109 // the carrier-aggregate proof shape introduced by grovedb
110 // PR #664).
111 let (outer_range_field, terminator_range_clause) = match range_clauses.len() {
112 1 => (None, range_clauses[0]),
113 2 => {
114 // The two ranges must be on different fields — same-
115 // field two-sided ranges are flattened by the parser
116 // into `between*` and arrive with one clause.
117 if range_clauses[0].field == range_clauses[1].field {
118 return None;
119 }
120 // One of the two must be on an index's terminator; the
121 // other becomes the outer carrier dimension. We pick
122 // the terminator below by walking each candidate
123 // index's property order — defer choosing here.
124 (
125 Some((
126 range_clauses[0].field.as_str(),
127 range_clauses[1].field.as_str(),
128 )),
129 range_clauses[0], // placeholder, refined per-index below
130 )
131 }
132 _ => return None,
133 };
134
135 // Reject any operator that's neither indexable (Equal/In) nor a
136 // range operator — anything else has no defined count semantics.
137 if where_clauses.iter().any(|wc| {
138 !Self::is_indexable_for_count(wc.operator) && !Self::is_range_operator(wc.operator)
139 }) {
140 return None;
141 }
142
143 let prefix_fields: BTreeSet<&str> = where_clauses
144 .iter()
145 .filter(|wc| Self::is_indexable_for_count(wc.operator))
146 .map(|wc| wc.field.as_str())
147 .collect();
148
149 for index in indexes.values() {
150 if !index.range_countable || !index.countable.is_countable() {
151 continue;
152 }
153
154 // For the two-range case, the terminator's field must be
155 // one of the two range fields, and the other range field
156 // must be the index's first property (the carrier
157 // dimension).
158 if let Some((field_a, field_b)) = outer_range_field {
159 let terminator = index.properties.last()?;
160 let first = index.properties.first()?;
161 // Determine which range field is the terminator.
162 let (outer_field, _terminator_field) = if terminator.name == field_a {
163 (field_b, field_a)
164 } else if terminator.name == field_b {
165 (field_a, field_b)
166 } else {
167 continue;
168 };
169 if first.name != outer_field {
170 continue;
171 }
172 // Any Equal/In prefix clauses must sit between the
173 // first (outer-range) and last (terminator-range)
174 // properties. For the widget contract there are no
175 // such middle properties on byBrandColor, but the
176 // builder handles the general case.
177 let mut intermediate_props_ok = true;
178 for prop in &index.properties[1..index.properties.len() - 1] {
179 if !prefix_fields.contains(prop.name.as_str()) {
180 intermediate_props_ok = false;
181 break;
182 }
183 }
184 if intermediate_props_ok {
185 return Some(index);
186 }
187 continue;
188 }
189
190 // Single-range case (the original logic): prefix matches
191 // must come first, followed by the range property as the
192 // LAST element.
193 let mut prefix_len = 0usize;
194 for prop in &index.properties {
195 if prefix_fields.contains(prop.name.as_str()) {
196 prefix_len += 1;
197 } else {
198 break;
199 }
200 }
201 if prefix_len < prefix_fields.len() {
202 continue;
203 }
204 if prefix_len + 1 != index.properties.len() {
205 // Range property must be the terminator (last property).
206 continue;
207 }
208 let range_prop = &index.properties[prefix_len];
209 if range_prop.name == terminator_range_clause.field {
210 return Some(index);
211 }
212 }
213
214 None
215 }
216}