icydb-core 0.259.5

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: predicate::fingerprint
//! Responsibility: deterministic predicate hashing for plan signatures.
//! Does not own: predicate normalization or runtime execution.
//! Boundary: used by planner/continuation fingerprinting.

#[cfg(test)]
mod admission_tests;

use crate::{
    db::{
        codec::new_hash_sha256,
        predicate::{
            Predicate,
            encoding::{
                normalized_predicate_key_capacity, raw_predicate_key_capacity,
                write_normalized_predicate_sort_key, write_predicate_sort_key,
            },
            normalize,
        },
        query::construction::ConstructionBudget,
    },
    error::InternalError,
};
use sha2::{Digest, Sha256};

/// Hash canonical predicate structure into the plan hash stream.
pub(in crate::db) fn hash_predicate(
    hasher: &mut Sha256,
    predicate: &Predicate,
    budget: &dyn ConstructionBudget,
) -> Result<(), InternalError> {
    // Copy and final encoding use the caller's authority. Boolean normalization
    // still has separate construction/comparison work to qualify.
    let normalized = normalize(budget.copy_predicate(predicate)?);
    hash_predicate_structural(hasher, &normalized, budget)
}

/// Return one canonical SHA-256 predicate digest for cache and plan identity.
#[cfg(test)]
pub(in crate::db) fn predicate_fingerprint(predicate: &Predicate) -> [u8; 32] {
    let mut hasher = new_hash_sha256();
    crate::db::query::preparation::with_preparation_work(|work| {
        hash_predicate(&mut hasher, predicate, work).expect("fixture hash fits");
    });

    crate::db::codec::finalize_hash_sha256(hasher)
}

/// Return one canonical SHA-256 digest for a predicate that is already normalized.
pub(in crate::db) fn predicate_fingerprint_normalized(
    predicate: &Predicate,
    budget: &dyn ConstructionBudget,
) -> Result<[u8; 32], InternalError> {
    let mut hasher = new_hash_sha256();
    let capacity = normalized_predicate_key_capacity(predicate, budget)?;
    let mut encoded = budget.vec_with_capacity(capacity)?;
    write_normalized_predicate_sort_key(&mut encoded, predicate);
    debug_assert!(encoded.len() <= capacity);
    hasher.update(encoded);

    Ok(crate::db::codec::finalize_hash_sha256(hasher))
}

// Hash structural predicate bytes without running normalization.
//
// Predicate sort-key encoding already owns the canonical structural traversal
// for deterministic ordering. Reuse that same byte surface for hashing so the
// predicate subsystem does not carry a second recursive encoding tree.
fn hash_predicate_structural(
    hasher: &mut Sha256,
    predicate: &Predicate,
    budget: &dyn ConstructionBudget,
) -> Result<(), InternalError> {
    let capacity = raw_predicate_key_capacity(predicate, budget)?;
    let mut encoded = budget.vec_with_capacity(capacity)?;
    write_predicate_sort_key(&mut encoded, predicate);
    debug_assert!(encoded.len() <= capacity);
    hasher.update(encoded);
    Ok(())
}

///
/// TESTS
///

#[cfg(test)]
mod tests {
    use super::{hash_predicate_structural, predicate_fingerprint};
    use crate::{
        db::predicate::{CompareOp, ComparePredicate, Predicate, coercion::CoercionId, normalize},
        value::Value,
    };

    #[test]
    fn hash_predicate_preserves_raw_and_child_order_before_normalization() {
        let left = Predicate::And(vec![
            Predicate::Compare(ComparePredicate::eq("a".to_string(), Value::Int64(1))),
            Predicate::Compare(ComparePredicate::eq("b".to_string(), Value::Int64(2))),
        ]);
        let right = Predicate::And(vec![
            Predicate::Compare(ComparePredicate::eq("b".to_string(), Value::Int64(2))),
            Predicate::Compare(ComparePredicate::eq("a".to_string(), Value::Int64(1))),
        ]);

        assert_ne!(digest_structural(&left), digest_structural(&right));
    }

    #[test]
    fn canonical_hash_is_order_insensitive_for_and() {
        let left = Predicate::And(vec![
            Predicate::Compare(ComparePredicate::eq("a".to_string(), Value::Int64(1))),
            Predicate::Compare(ComparePredicate::eq("b".to_string(), Value::Int64(2))),
        ]);
        let right = Predicate::And(vec![
            Predicate::Compare(ComparePredicate::eq("b".to_string(), Value::Int64(2))),
            Predicate::Compare(ComparePredicate::eq("a".to_string(), Value::Int64(1))),
        ]);

        assert_eq!(normalize(left.clone()), normalize(right.clone()));
        assert_eq!(digest(&left), digest(&right));
    }

    #[test]
    fn canonical_hash_is_order_insensitive_for_or() {
        let left = Predicate::Or(vec![
            Predicate::Compare(ComparePredicate::eq("a".to_string(), Value::Int64(1))),
            Predicate::Compare(ComparePredicate::eq("b".to_string(), Value::Int64(2))),
        ]);
        let right = Predicate::Or(vec![
            Predicate::Compare(ComparePredicate::eq("b".to_string(), Value::Int64(2))),
            Predicate::Compare(ComparePredicate::eq("a".to_string(), Value::Int64(1))),
        ]);

        assert_eq!(normalize(left.clone()), normalize(right.clone()));
        assert_eq!(digest(&left), digest(&right));
    }

    #[test]
    fn canonical_hash_treats_same_field_or_eq_and_in_as_equivalent() {
        let or_eq = Predicate::Or(vec![
            Predicate::Compare(ComparePredicate::with_coercion(
                "rank",
                CompareOp::Eq,
                Value::Nat64(3),
                CoercionId::Strict,
            )),
            Predicate::Compare(ComparePredicate::with_coercion(
                "rank",
                CompareOp::Eq,
                Value::Nat64(1),
                CoercionId::Strict,
            )),
            Predicate::Compare(ComparePredicate::with_coercion(
                "rank",
                CompareOp::Eq,
                Value::Nat64(3),
                CoercionId::Strict,
            )),
        ]);
        let in_list = Predicate::Compare(ComparePredicate::with_coercion(
            "rank",
            CompareOp::In,
            Value::List(vec![Value::Nat64(1), Value::Nat64(3)]),
            CoercionId::Strict,
        ));

        assert_eq!(normalize(or_eq.clone()), normalize(in_list.clone()));
        assert_eq!(digest(&or_eq), digest(&in_list));
    }

    #[test]
    fn canonical_hash_is_order_insensitive_for_in_list_literals() {
        let left = Predicate::Compare(ComparePredicate::in_(
            "rank".to_string(),
            vec![Value::Nat64(3), Value::Nat64(1), Value::Nat64(2)],
        ));
        let right = Predicate::Compare(ComparePredicate::in_(
            "rank".to_string(),
            vec![Value::Nat64(1), Value::Nat64(2), Value::Nat64(3)],
        ));

        assert_ne!(normalize(left.clone()), normalize(right.clone()));
        assert_eq!(digest(&left), digest(&right));
    }

    #[test]
    fn canonical_hash_normalizes_in_list_duplicate_literals() {
        let left = Predicate::Compare(ComparePredicate::in_(
            "rank".to_string(),
            vec![
                Value::Nat64(3),
                Value::Nat64(1),
                Value::Nat64(3),
                Value::Nat64(2),
            ],
        ));
        let right = Predicate::Compare(ComparePredicate::in_(
            "rank".to_string(),
            vec![Value::Nat64(1), Value::Nat64(2), Value::Nat64(3)],
        ));

        assert_ne!(normalize(left.clone()), normalize(right.clone()));
        assert_eq!(digest(&left), digest(&right));
    }

    #[test]
    fn canonical_hash_treats_implicit_and_explicit_strict_coercion_as_equivalent() {
        let left = Predicate::Compare(ComparePredicate::eq("rank".to_string(), Value::Int64(7)));
        let right = Predicate::Compare(ComparePredicate::with_coercion(
            "rank",
            CompareOp::Eq,
            Value::Int64(7),
            CoercionId::Strict,
        ));

        assert_eq!(normalize(left.clone()), normalize(right.clone()));
        assert_eq!(digest(&left), digest(&right));
    }

    #[test]
    fn canonical_hash_distinguishes_different_coercion_ids() {
        let strict = Predicate::Compare(ComparePredicate::with_coercion(
            "rank",
            CompareOp::Eq,
            Value::Int64(7),
            CoercionId::Strict,
        ));
        let numeric_widen = Predicate::Compare(ComparePredicate::with_coercion(
            "rank",
            CompareOp::Eq,
            Value::Int64(7),
            CoercionId::NumericWiden,
        ));

        assert_ne!(normalize(strict.clone()), normalize(numeric_widen.clone()));
        assert_ne!(digest(&strict), digest(&numeric_widen));
    }

    fn digest(predicate: &Predicate) -> [u8; 32] {
        predicate_fingerprint(predicate)
    }

    fn digest_structural(predicate: &Predicate) -> [u8; 32] {
        let mut hasher = crate::db::codec::new_hash_sha256();
        crate::db::query::preparation::with_preparation_work(|work| {
            hash_predicate_structural(&mut hasher, predicate, work).unwrap();
        });
        crate::db::codec::finalize_hash_sha256(hasher)
    }
}