icydb-core 0.257.20

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: query::fingerprint::shape_signature
//! Responsibility: deterministic query-shape signature derivation from planned
//! query contracts.
//! Does not own: continuation token decoding/validation.
//! Boundary: shared query-shape hashing surface used by execution identity and
//! cursor token checks.

use crate::db::{
    cursor::ContinuationSignature,
    query::fingerprint::{
        finalize_sha256_digest, hash_sections, new_continuation_signature_hasher,
    },
    query::{construction::ConstructionBudget, plan::AccessPlannedQuery},
};

use crate::error::InternalError;

impl AccessPlannedQuery {
    /// Compute a continuation signature bound to the entity path.
    ///
    /// This is used to validate that a continuation token belongs to the
    /// same canonical query shape.
    pub(in crate::db) fn continuation_signature(
        &self,
        entity_path: &str,
        budget: &dyn ConstructionBudget,
    ) -> Result<ContinuationSignature, InternalError> {
        let projection = self.projection_spec_for_identity();

        continuation_signature_for_plan_with_projection(self, entity_path, &projection, budget)
    }
}

fn continuation_signature_for_plan_with_projection(
    plan: &AccessPlannedQuery,
    entity_path: &str,
    projection: &crate::db::query::plan::expr::ProjectionSpec,
    budget: &dyn ConstructionBudget,
) -> Result<ContinuationSignature, InternalError> {
    let mut hasher = new_continuation_signature_hasher();
    hash_sections::hash_continuation_with_projection(
        &mut hasher,
        plan,
        entity_path,
        projection,
        budget,
    )?;
    Ok(ContinuationSignature::from_bytes(finalize_sha256_digest(
        hasher,
    )))
}

#[cfg(test)]
mod tests {
    use crate::db::query::preparation::with_preparation_work;
    use crate::{
        db::{
            Predicate,
            access::AccessPath,
            predicate::MissingRowPolicy,
            query::plan::{AccessPlannedQuery, LogicalPlan},
        },
        value::Value,
    };

    fn plan_with_bound_value(value: &str) -> AccessPlannedQuery {
        let mut plan =
            AccessPlannedQuery::new(AccessPath::<Value>::FullScan, MissingRowPolicy::Ignore);
        let LogicalPlan::Scalar(scalar) = &mut plan.logical else {
            panic!("test plan should remain scalar");
        };
        scalar.predicate = Some(Predicate::eq(
            "label".to_string(),
            Value::Text(value.to_string()),
        ));

        plan
    }

    #[test]
    fn continuation_signature_binds_current_parameter_values() {
        let first = plan_with_bound_value("first");
        let second = plan_with_bound_value("second");

        assert_ne!(
            with_preparation_work(|work| first.continuation_signature("tests::Entity", work))
                .unwrap(),
            with_preparation_work(|work| second.continuation_signature("tests::Entity", work))
                .unwrap(),
            "one template must not admit a cursor issued for different bound values",
        );
    }

    #[test]
    fn predicate_continuation_admission_is_cumulative_and_retryable() {
        use crate::db::{
            QueryError, RequestExecutionRoot,
            executor::budget::{HardExecutionBudget, HardExecutionFailureHeadroom},
            query::preparation::PreparationWork,
        };
        use icydb_diagnostic_code::{
            DiagnosticExecutionBudgetResource as Resource, DiagnosticExecutionLane as Lane,
            DiagnosticFactTag,
        };
        let request = |resource, limit| {
            RequestExecutionRoot::new_for_tests(
                HardExecutionBudget::uniform_for_tests(
                    32_000_000,
                    HardExecutionFailureHeadroom::new(500_000_000, 64 * 1024),
                )
                .with_limit_for_tests(resource, limit),
            )
        };
        let plan = plan_with_bound_value("account");
        let snapshot = plan.clone();
        for lane in [Lane::PublicRead, Lane::TrustedRead, Lane::Diagnostic] {
            let build = |root: &RequestExecutionRoot| {
                PreparationWork::run(&root.scope(), lane, |work| {
                    plan.planned_continuation_contract_with_accepted_identity(
                        "tests::Entity",
                        None,
                        work,
                    )
                    .map_err(QueryError::execute)
                })
            };
            let measured = request(Resource::TemporaryBytes, 32_000_000);
            let expected = build(&measured).unwrap().unwrap().continuation_signature();
            for resource in [
                Resource::TemporaryBytes,
                Resource::PredicateExpressionSteps,
                Resource::NestedValueSteps,
            ] {
                let cost = measured.observed(resource);
                assert!(cost > 0);
                let repeated = request(resource, 2 * cost);
                for _ in 0..2 {
                    assert_eq!(
                        build(&repeated).unwrap().unwrap().continuation_signature(),
                        expected
                    );
                }
                let error = build(&repeated).unwrap_err();
                assert!(
                    error
                        .diagnostic_facts()
                        .contains(&(DiagnosticFactTag::BudgetResource, resource.raw(),))
                );
                assert!(
                    error
                        .diagnostic_facts()
                        .contains(&(DiagnosticFactTag::ExecutionLane, lane.raw(),))
                );
                assert_eq!(repeated.observed(Resource::RowsVisited), 0);
                let fresh = request(resource, cost);
                assert_eq!(
                    build(&fresh).unwrap().unwrap().continuation_signature(),
                    expected
                );
                assert_eq!(plan, snapshot);
            }
        }
    }

    #[test]
    fn expression_owned_and_absent_filters_skip_predicate_copy_admission() {
        use crate::db::{
            QueryError, RequestExecutionRoot,
            executor::budget::{HardExecutionBudget, HardExecutionFailureHeadroom},
            query::{plan::expr::Expr, preparation::PreparationWork},
        };
        use icydb_diagnostic_code::{
            DiagnosticExecutionBudgetResource as Resource, DiagnosticExecutionLane as Lane,
        };
        let root = RequestExecutionRoot::new_for_tests(
            HardExecutionBudget::uniform_for_tests(
                16_000_000,
                HardExecutionFailureHeadroom::new(500_000_000, 64 * 1024),
            )
            .with_limit_for_tests(Resource::TemporaryBytes, 0),
        );
        let signature = |plan: &AccessPlannedQuery| {
            PreparationWork::run(&root.scope(), Lane::PublicRead, |work| {
                plan.continuation_signature("tests::Entity", work)
                    .map_err(QueryError::execute)
            })
        };
        let mut plan = plan_with_bound_value("account");
        let LogicalPlan::Scalar(scalar) = &mut plan.logical else {
            unreachable!()
        };
        scalar.filter_expr = Some(Expr::Literal(Value::Bool(true)));
        signature(&plan).unwrap();
        let LogicalPlan::Scalar(scalar) = &mut plan.logical else {
            unreachable!()
        };
        scalar.filter_expr = None;
        scalar.predicate = None;
        signature(&plan).unwrap();
        assert_eq!(root.observed(Resource::TemporaryBytes), 0);
    }

    #[test]
    fn continuation_construction_preserves_hash_failure_and_allows_retry() {
        use crate::value::{test_hash_budget_error, with_test_hash_override};
        let mut plan = plan_with_bound_value("account");
        let LogicalPlan::Scalar(scalar) = &mut plan.logical else {
            unreachable!()
        };
        scalar.filter_expr = Some(crate::db::query::plan::expr::Expr::Literal(Value::Nat64(7)));
        let expected =
            with_preparation_work(|work| plan.continuation_signature("tests::Entity", work))
                .unwrap();
        for _ in 0..2 {
            with_test_hash_override(Err(test_hash_budget_error), || {
                let error = with_preparation_work(|work| {
                    plan.planned_continuation_contract_with_accepted_identity(
                        "tests::Entity",
                        None,
                        work,
                    )
                })
                .expect_err("failed hash must not return a continuation contract");
                assert_eq!(error.diagnostic(), test_hash_budget_error().diagnostic());
                assert_eq!(
                    error.diagnostic_facts(),
                    test_hash_budget_error().diagnostic_facts()
                );
            });
        }
        assert_eq!(
            with_preparation_work(|work| plan.continuation_signature("tests::Entity", work))
                .unwrap(),
            expected
        );
    }
}