icydb_core/db/query/plan/
fingerprint.rs1#![allow(clippy::cast_possible_truncation)]
3
4use super::ExplainPlan;
5use crate::{db::query::plan::hash_parts, traits::FieldValue};
6use sha2::{Digest, Sha256};
7
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub struct PlanFingerprint([u8; 32]);
16
17impl PlanFingerprint {
18 pub(crate) const fn from_bytes(bytes: [u8; 32]) -> Self {
19 Self(bytes)
20 }
21
22 #[must_use]
23 pub fn as_hex(&self) -> String {
24 crate::db::cursor::encode_cursor(&self.0)
25 }
26}
27
28impl std::fmt::Display for PlanFingerprint {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.write_str(&self.as_hex())
31 }
32}
33
34impl<K> super::LogicalPlan<K>
35where
36 K: FieldValue,
37{
38 #[must_use]
40 pub fn fingerprint(&self) -> PlanFingerprint {
41 self.explain().fingerprint()
42 }
43}
44
45impl ExplainPlan {
46 #[must_use]
48 pub fn fingerprint(&self) -> PlanFingerprint {
49 let mut hasher = Sha256::new();
50 hasher.update(b"planfp:v2");
51 hash_parts::hash_explain_plan_profile(
52 &mut hasher,
53 self,
54 hash_parts::ExplainHashProfile::FingerprintV2,
55 );
56 let digest = hasher.finalize();
57 let mut out = [0u8; 32];
58 out.copy_from_slice(&digest);
59 PlanFingerprint(out)
60 }
61}
62
63#[cfg(test)]
68mod tests {
69 use crate::db::query::intent::{KeyAccess, access_plan_from_keys_value};
70 use crate::db::query::plan::{AccessPath, DeleteLimitSpec, LogicalPlan};
71 use crate::db::query::predicate::Predicate;
72 use crate::db::query::{FieldRef, QueryMode, ReadConsistency};
73 use crate::model::index::IndexModel;
74 use crate::types::Ulid;
75 use crate::value::Value;
76
77 #[test]
78 fn fingerprint_is_deterministic_for_equivalent_predicates() {
79 let id = Ulid::default();
80
81 let predicate_a = Predicate::And(vec![
82 FieldRef::new("id").eq(id),
83 FieldRef::new("other").eq(Value::Text("x".to_string())),
84 ]);
85 let predicate_b = Predicate::And(vec![
86 FieldRef::new("other").eq(Value::Text("x".to_string())),
87 FieldRef::new("id").eq(id),
88 ]);
89
90 let mut plan_a: LogicalPlan<Value> =
91 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
92 plan_a.predicate = Some(predicate_a);
93
94 let mut plan_b: LogicalPlan<Value> =
95 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
96 plan_b.predicate = Some(predicate_b);
97
98 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
99 }
100
101 #[test]
102 fn fingerprint_is_deterministic_for_by_keys() {
103 let a = Ulid::from_u128(1);
104 let b = Ulid::from_u128(2);
105
106 let access_a = access_plan_from_keys_value(&KeyAccess::Many(vec![a, b, a]));
107 let access_b = access_plan_from_keys_value(&KeyAccess::Many(vec![b, a]));
108
109 let plan_a: LogicalPlan<Value> = LogicalPlan {
110 mode: QueryMode::Load(crate::db::query::LoadSpec::new()),
111 access: access_a,
112 predicate: None,
113 order: None,
114 delete_limit: None,
115 page: None,
116 consistency: ReadConsistency::MissingOk,
117 };
118 let plan_b: LogicalPlan<Value> = LogicalPlan {
119 mode: QueryMode::Load(crate::db::query::LoadSpec::new()),
120 access: access_b,
121 predicate: None,
122 order: None,
123 delete_limit: None,
124 page: None,
125 consistency: ReadConsistency::MissingOk,
126 };
127
128 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
129 }
130
131 #[test]
132 fn fingerprint_changes_with_index_choice() {
133 const INDEX_FIELDS: [&str; 1] = ["idx_a"];
134 const INDEX_A: IndexModel = IndexModel::new(
135 "fingerprint::idx_a",
136 "fingerprint::store",
137 &INDEX_FIELDS,
138 false,
139 );
140 const INDEX_B: IndexModel = IndexModel::new(
141 "fingerprint::idx_b",
142 "fingerprint::store",
143 &INDEX_FIELDS,
144 false,
145 );
146
147 let plan_a: LogicalPlan<Value> = LogicalPlan::new(
148 AccessPath::IndexPrefix {
149 index: INDEX_A,
150 values: vec![Value::Text("alpha".to_string())],
151 },
152 crate::db::query::ReadConsistency::MissingOk,
153 );
154 let plan_b: LogicalPlan<Value> = LogicalPlan::new(
155 AccessPath::IndexPrefix {
156 index: INDEX_B,
157 values: vec![Value::Text("alpha".to_string())],
158 },
159 crate::db::query::ReadConsistency::MissingOk,
160 );
161
162 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
163 }
164
165 #[test]
166 fn fingerprint_changes_with_pagination() {
167 let mut plan_a: LogicalPlan<Value> =
168 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
169 let mut plan_b: LogicalPlan<Value> =
170 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
171 plan_a.page = Some(crate::db::query::plan::PageSpec {
172 limit: Some(10),
173 offset: 0,
174 });
175 plan_b.page = Some(crate::db::query::plan::PageSpec {
176 limit: Some(10),
177 offset: 1,
178 });
179
180 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
181 }
182
183 #[test]
184 fn fingerprint_changes_with_delete_limit() {
185 let mut plan_a: LogicalPlan<Value> =
186 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
187 let mut plan_b: LogicalPlan<Value> =
188 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
189 plan_a.mode = QueryMode::Delete(crate::db::query::DeleteSpec::new());
190 plan_b.mode = QueryMode::Delete(crate::db::query::DeleteSpec::new());
191 plan_a.delete_limit = Some(DeleteLimitSpec { max_rows: 2 });
192 plan_b.delete_limit = Some(DeleteLimitSpec { max_rows: 3 });
193
194 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
195 }
196
197 #[test]
198 fn fingerprint_is_stable_for_full_scan() {
199 let plan: LogicalPlan<Value> =
200 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
201 let fingerprint_a = plan.fingerprint();
202 let fingerprint_b = plan.fingerprint();
203 assert_eq!(fingerprint_a, fingerprint_b);
204 }
205}