Skip to main content

icydb_core/db/query/plan/
fingerprint.rs

1//! Deterministic plan fingerprinting derived from the explain projection.
2#![allow(clippy::cast_possible_truncation)]
3
4use super::ExplainPlan;
5use crate::{db::query::plan::hash_parts, traits::FieldValue};
6use sha2::{Digest, Sha256};
7
8///
9/// PlanFingerprint
10///
11/// Stable, deterministic fingerprint for logical plans.
12///
13
14#[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    /// Compute a stable fingerprint for this logical plan.
39    #[must_use]
40    pub fn fingerprint(&self) -> PlanFingerprint {
41        self.explain().fingerprint()
42    }
43}
44
45impl ExplainPlan {
46    /// Compute a stable fingerprint for this explain plan.
47    #[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///
64/// TESTS
65///
66
67#[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}