1#![allow(clippy::cast_possible_truncation)]
3
4use super::{
5 ExplainAccessPath, ExplainDeleteLimit, ExplainOrderBy, ExplainPagination, ExplainPlan,
6 ExplainPredicate,
7};
8use crate::db::index::fingerprint::hash_value;
9use crate::db::query::QueryMode;
10use crate::db::query::{ReadConsistency, predicate::coercion::CoercionId};
11use crate::key::Key;
12use sha2::{Digest, Sha256};
13
14#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
21pub struct PlanFingerprint([u8; 32]);
22
23impl PlanFingerprint {
24 pub(crate) const fn from_bytes(bytes: [u8; 32]) -> Self {
25 Self(bytes)
26 }
27
28 #[must_use]
29 pub fn as_hex(&self) -> String {
30 let mut out = String::with_capacity(64);
31 for byte in self.0 {
32 use std::fmt::Write as _;
33 let _ = write!(out, "{byte:02x}");
34 }
35 out
36 }
37}
38
39impl std::fmt::Display for PlanFingerprint {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.write_str(&self.as_hex())
42 }
43}
44
45impl super::LogicalPlan {
46 #[must_use]
48 pub fn fingerprint(&self) -> PlanFingerprint {
49 self.explain().fingerprint()
50 }
51}
52
53impl ExplainPlan {
54 #[must_use]
56 pub fn fingerprint(&self) -> PlanFingerprint {
57 let mut hasher = Sha256::new();
58 hasher.update(b"planfp:v2");
59 hash_explain_plan(&mut hasher, self);
60 let digest = hasher.finalize();
61 let mut out = [0u8; 32];
62 out.copy_from_slice(&digest);
63 PlanFingerprint(out)
64 }
65}
66
67fn hash_explain_plan(hasher: &mut Sha256, plan: &ExplainPlan) {
68 write_tag(hasher, 0x01);
69 hash_access(hasher, &plan.access);
70
71 write_tag(hasher, 0x02);
72 hash_predicate(hasher, &plan.predicate);
73
74 write_tag(hasher, 0x03);
75 hash_order(hasher, &plan.order_by);
76
77 write_tag(hasher, 0x04);
78 hash_page(hasher, &plan.page);
79
80 write_tag(hasher, 0x05);
81 hash_delete_limit(hasher, &plan.delete_limit);
82
83 write_tag(hasher, 0x06);
84 hash_consistency(hasher, plan.consistency);
85
86 write_tag(hasher, 0x07);
87 hash_mode(hasher, plan.mode);
88}
89
90fn hash_access(hasher: &mut Sha256, access: &ExplainAccessPath) {
91 match access {
92 ExplainAccessPath::ByKey { key } => {
93 write_tag(hasher, 0x10);
94 write_key(hasher, key);
95 }
96 ExplainAccessPath::ByKeys { keys } => {
97 write_tag(hasher, 0x11);
98 write_u32(hasher, keys.len() as u32);
99 for key in keys {
100 write_key(hasher, key);
101 }
102 }
103 ExplainAccessPath::KeyRange { start, end } => {
104 write_tag(hasher, 0x12);
105 write_key(hasher, start);
106 write_key(hasher, end);
107 }
108 ExplainAccessPath::IndexPrefix {
109 name,
110 fields,
111 prefix_len,
112 values,
113 } => {
114 write_tag(hasher, 0x13);
115 write_str(hasher, name);
116 write_u32(hasher, fields.len() as u32);
117 for field in fields {
118 write_str(hasher, field);
119 }
120 write_u32(hasher, *prefix_len as u32);
121 write_u32(hasher, values.len() as u32);
122 for value in values {
123 write_value(hasher, value);
124 }
125 }
126 ExplainAccessPath::FullScan => {
127 write_tag(hasher, 0x14);
128 }
129 ExplainAccessPath::Union(children) => {
130 write_tag(hasher, 0x15);
131 write_u32(hasher, children.len() as u32);
132 for child in children {
133 hash_access(hasher, child);
134 }
135 }
136 ExplainAccessPath::Intersection(children) => {
137 write_tag(hasher, 0x16);
138 write_u32(hasher, children.len() as u32);
139 for child in children {
140 hash_access(hasher, child);
141 }
142 }
143 }
144}
145
146fn hash_predicate(hasher: &mut Sha256, predicate: &ExplainPredicate) {
147 match predicate {
148 ExplainPredicate::None => write_tag(hasher, 0x20),
149 ExplainPredicate::True => write_tag(hasher, 0x21),
150 ExplainPredicate::False => write_tag(hasher, 0x22),
151 ExplainPredicate::And(children) => {
152 write_tag(hasher, 0x23);
153 write_u32(hasher, children.len() as u32);
154 for child in children {
155 hash_predicate(hasher, child);
156 }
157 }
158 ExplainPredicate::Or(children) => {
159 write_tag(hasher, 0x24);
160 write_u32(hasher, children.len() as u32);
161 for child in children {
162 hash_predicate(hasher, child);
163 }
164 }
165 ExplainPredicate::Not(inner) => {
166 write_tag(hasher, 0x25);
167 hash_predicate(hasher, inner);
168 }
169 ExplainPredicate::Compare {
170 field,
171 op,
172 value,
173 coercion,
174 } => {
175 write_tag(hasher, 0x26);
176 write_str(hasher, field);
177 write_tag(hasher, op.tag());
178 write_value(hasher, value);
179 hash_coercion(hasher, coercion.id, &coercion.params);
180 }
181 ExplainPredicate::IsNull { field } => {
182 write_tag(hasher, 0x27);
183 write_str(hasher, field);
184 }
185 ExplainPredicate::IsMissing { field } => {
186 write_tag(hasher, 0x28);
187 write_str(hasher, field);
188 }
189 ExplainPredicate::IsEmpty { field } => {
190 write_tag(hasher, 0x29);
191 write_str(hasher, field);
192 }
193 ExplainPredicate::IsNotEmpty { field } => {
194 write_tag(hasher, 0x2a);
195 write_str(hasher, field);
196 }
197 ExplainPredicate::MapContainsKey {
198 field,
199 key,
200 coercion,
201 } => {
202 write_tag(hasher, 0x2b);
203 write_str(hasher, field);
204 write_value(hasher, key);
205 hash_coercion(hasher, coercion.id, &coercion.params);
206 }
207 ExplainPredicate::MapContainsValue {
208 field,
209 value,
210 coercion,
211 } => {
212 write_tag(hasher, 0x2c);
213 write_str(hasher, field);
214 write_value(hasher, value);
215 hash_coercion(hasher, coercion.id, &coercion.params);
216 }
217 ExplainPredicate::MapContainsEntry {
218 field,
219 key,
220 value,
221 coercion,
222 } => {
223 write_tag(hasher, 0x2d);
224 write_str(hasher, field);
225 write_value(hasher, key);
226 write_value(hasher, value);
227 hash_coercion(hasher, coercion.id, &coercion.params);
228 }
229 ExplainPredicate::TextContains { field, value } => {
230 write_tag(hasher, 0x2e);
231 write_str(hasher, field);
232 write_value(hasher, value);
233 }
234 ExplainPredicate::TextContainsCi { field, value } => {
235 write_tag(hasher, 0x2f);
236 write_str(hasher, field);
237 write_value(hasher, value);
238 }
239 }
240}
241
242fn hash_order(hasher: &mut Sha256, order: &ExplainOrderBy) {
243 match order {
244 ExplainOrderBy::None => write_tag(hasher, 0x30),
245 ExplainOrderBy::Fields(fields) => {
246 write_tag(hasher, 0x31);
247 write_u32(hasher, fields.len() as u32);
248 for field in fields {
249 write_str(hasher, &field.field);
250 write_tag(hasher, order_direction_tag(field.direction));
251 }
252 }
253 }
254}
255
256fn hash_page(hasher: &mut Sha256, page: &ExplainPagination) {
257 match page {
258 ExplainPagination::None => write_tag(hasher, 0x40),
259 ExplainPagination::Page { limit, offset } => {
260 write_tag(hasher, 0x41);
261 match limit {
262 Some(limit) => {
263 write_tag(hasher, 0x01);
264 write_u32(hasher, *limit);
265 }
266 None => write_tag(hasher, 0x00),
267 }
268 write_u32(hasher, *offset);
269 }
270 }
271}
272
273fn hash_delete_limit(hasher: &mut Sha256, limit: &ExplainDeleteLimit) {
274 match limit {
275 ExplainDeleteLimit::None => write_tag(hasher, 0x42),
276 ExplainDeleteLimit::Limit { max_rows } => {
277 write_tag(hasher, 0x43);
278 write_u32(hasher, *max_rows);
279 }
280 }
281}
282
283fn hash_consistency(hasher: &mut Sha256, consistency: ReadConsistency) {
284 match consistency {
285 ReadConsistency::MissingOk => write_tag(hasher, 0x50),
286 ReadConsistency::Strict => write_tag(hasher, 0x51),
287 }
288}
289
290fn hash_mode(hasher: &mut Sha256, mode: QueryMode) {
291 match mode {
292 QueryMode::Load(_) => write_tag(hasher, 0x60),
293 QueryMode::Delete(_) => write_tag(hasher, 0x61),
294 }
295}
296
297fn hash_coercion(
298 hasher: &mut Sha256,
299 id: CoercionId,
300 params: &std::collections::BTreeMap<String, String>,
301) {
302 write_tag(hasher, coercion_id_tag(id));
303 write_u32(hasher, params.len() as u32);
304 for (key, value) in params {
305 write_str(hasher, key);
306 write_str(hasher, value);
307 }
308}
309
310fn write_key(hasher: &mut Sha256, key: &Key) {
311 match key.to_bytes() {
312 Ok(bytes) => hasher.update(bytes),
313 Err(err) => {
314 write_tag(hasher, 0xED);
315 write_str(hasher, &err.to_string());
316 }
317 }
318}
319
320fn write_value(hasher: &mut Sha256, value: &crate::value::Value) {
321 match hash_value(value) {
322 Ok(digest) => hasher.update(digest),
323 Err(err) => {
324 write_tag(hasher, 0xEE);
325 write_str(hasher, &err.display_with_class());
326 }
327 }
328}
329
330fn write_str(hasher: &mut Sha256, value: &str) {
331 write_u32(hasher, value.len() as u32);
332 hasher.update(value.as_bytes());
333}
334
335fn write_u32(hasher: &mut Sha256, value: u32) {
336 hasher.update(value.to_be_bytes());
337}
338
339fn write_tag(hasher: &mut Sha256, tag: u8) {
340 hasher.update([tag]);
341}
342
343const fn order_direction_tag(direction: crate::db::query::plan::OrderDirection) -> u8 {
344 match direction {
345 crate::db::query::plan::OrderDirection::Asc => 0x01,
346 crate::db::query::plan::OrderDirection::Desc => 0x02,
347 }
348}
349
350const fn coercion_id_tag(id: CoercionId) -> u8 {
351 match id {
352 CoercionId::Strict => 0x01,
353 CoercionId::NumericWiden => 0x02,
354 CoercionId::TextCasefold => 0x04,
355 CoercionId::CollectionElement => 0x05,
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use crate::db::query::plan::{AccessPath, DeleteLimitSpec, LogicalPlan};
362 use crate::db::query::{FieldRef, plan::planner::PlannerEntity};
363 use crate::db::query::{Query, QueryMode, ReadConsistency};
364 use crate::model::index::IndexModel;
365 use crate::types::Ulid;
366 use crate::value::Value;
367
368 #[test]
369 fn fingerprint_is_deterministic_for_equivalent_predicates() {
370 let id = Ulid::default();
371
372 let query_a = Query::<PlannerEntity>::new(ReadConsistency::MissingOk)
373 .filter(FieldRef::new("id").eq(id))
374 .filter(FieldRef::new("other").eq("x"));
375
376 let query_b = Query::<PlannerEntity>::new(ReadConsistency::MissingOk)
377 .filter(FieldRef::new("other").eq("x"))
378 .filter(FieldRef::new("id").eq(id));
379
380 let plan_a = query_a.plan().expect("plan a");
381 let plan_b = query_b.plan().expect("plan b");
382
383 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
384 }
385
386 #[test]
387 fn fingerprint_changes_with_index_choice() {
388 const INDEX_FIELDS: [&str; 1] = ["idx_a"];
389 const INDEX_A: IndexModel = IndexModel::new(
390 "fingerprint::idx_a",
391 "fingerprint::store",
392 &INDEX_FIELDS,
393 false,
394 );
395 const INDEX_B: IndexModel = IndexModel::new(
396 "fingerprint::idx_b",
397 "fingerprint::store",
398 &INDEX_FIELDS,
399 false,
400 );
401
402 let plan_a = LogicalPlan::new(
403 AccessPath::IndexPrefix {
404 index: INDEX_A,
405 values: vec![Value::Text("alpha".to_string())],
406 },
407 crate::db::query::ReadConsistency::MissingOk,
408 );
409 let plan_b = LogicalPlan::new(
410 AccessPath::IndexPrefix {
411 index: INDEX_B,
412 values: vec![Value::Text("alpha".to_string())],
413 },
414 crate::db::query::ReadConsistency::MissingOk,
415 );
416
417 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
418 }
419
420 #[test]
421 fn fingerprint_changes_with_pagination() {
422 let mut plan_a = LogicalPlan::new(
423 AccessPath::FullScan,
424 crate::db::query::ReadConsistency::MissingOk,
425 );
426 let mut plan_b = LogicalPlan::new(
427 AccessPath::FullScan,
428 crate::db::query::ReadConsistency::MissingOk,
429 );
430 plan_a.page = Some(crate::db::query::plan::PageSpec {
431 limit: Some(10),
432 offset: 0,
433 });
434 plan_b.page = Some(crate::db::query::plan::PageSpec {
435 limit: Some(10),
436 offset: 1,
437 });
438
439 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
440 }
441
442 #[test]
443 fn fingerprint_changes_with_delete_limit() {
444 let mut plan_a = LogicalPlan::new(
445 AccessPath::FullScan,
446 crate::db::query::ReadConsistency::MissingOk,
447 );
448 let mut plan_b = LogicalPlan::new(
449 AccessPath::FullScan,
450 crate::db::query::ReadConsistency::MissingOk,
451 );
452 plan_a.mode = QueryMode::Delete(crate::db::query::DeleteSpec::new());
453 plan_b.mode = QueryMode::Delete(crate::db::query::DeleteSpec::new());
454 plan_a.delete_limit = Some(DeleteLimitSpec { max_rows: 2 });
455 plan_b.delete_limit = Some(DeleteLimitSpec { max_rows: 3 });
456
457 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
458 }
459
460 #[test]
461 fn fingerprint_is_stable_for_full_scan() {
462 let plan = LogicalPlan::new(
463 AccessPath::FullScan,
464 crate::db::query::ReadConsistency::MissingOk,
465 );
466 let fingerprint_a = plan.fingerprint();
467 let fingerprint_b = plan.fingerprint();
468 assert_eq!(fingerprint_a, fingerprint_b);
469 }
470}