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