icydb-core 0.120.2

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: query::explain::access_projection
//! Responsibility: access-path projection adapters for EXPLAIN.
//! Does not own: logical plan policy or execution descriptor rendering.
//! Boundary: planner access path -> explain access DTOs/json adapters.

use crate::{
    db::{
        access::AccessPlan,
        query::{
            explain::{ExplainAccessPath, ExplainExecutionNodeType, writer::JsonWriter},
            plan::{
                AccessPlanProjection, explain_access_kind_label, project_access_plan,
                project_explain_access_path,
            },
        },
    },
    value::Value,
};
use std::fmt::Write;

///
/// ExplainAccessProjection
///
/// Local EXPLAIN adapter that consumes the planner-owned access traversal
/// contract and projects it into the transport-facing `ExplainAccessPath` DTO.
///

struct ExplainAccessProjection;

impl<K> AccessPlanProjection<K> for ExplainAccessProjection
where
    K: crate::traits::FieldValue,
{
    type Output = ExplainAccessPath;

    fn by_key(&mut self, key: &K) -> Self::Output {
        ExplainAccessPath::ByKey {
            key: key.to_value(),
        }
    }

    fn by_keys(&mut self, keys: &[K]) -> Self::Output {
        ExplainAccessPath::ByKeys {
            keys: keys
                .iter()
                .map(crate::traits::FieldValue::to_value)
                .collect(),
        }
    }

    fn key_range(&mut self, start: &K, end: &K) -> Self::Output {
        ExplainAccessPath::KeyRange {
            start: start.to_value(),
            end: end.to_value(),
        }
    }

    fn index_prefix(
        &mut self,
        index_name: &'static str,
        index_fields: &[&'static str],
        prefix_len: usize,
        values: &[Value],
    ) -> Self::Output {
        ExplainAccessPath::IndexPrefix {
            name: index_name,
            fields: index_fields.to_vec(),
            prefix_len,
            values: values.to_vec(),
        }
    }

    fn index_multi_lookup(
        &mut self,
        index_name: &'static str,
        index_fields: &[&'static str],
        values: &[Value],
    ) -> Self::Output {
        ExplainAccessPath::IndexMultiLookup {
            name: index_name,
            fields: index_fields.to_vec(),
            values: values.to_vec(),
        }
    }

    fn index_range(
        &mut self,
        index_name: &'static str,
        index_fields: &[&'static str],
        prefix_len: usize,
        prefix: &[Value],
        lower: &std::ops::Bound<Value>,
        upper: &std::ops::Bound<Value>,
    ) -> Self::Output {
        ExplainAccessPath::IndexRange {
            name: index_name,
            fields: index_fields.to_vec(),
            prefix_len,
            prefix: prefix.to_vec(),
            lower: lower.clone(),
            upper: upper.clone(),
        }
    }

    fn full_scan(&mut self) -> Self::Output {
        ExplainAccessPath::FullScan
    }

    fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
        ExplainAccessPath::Union(children)
    }

    fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
        ExplainAccessPath::Intersection(children)
    }
}

pub(in crate::db::query::explain) fn write_access_json(
    access: &ExplainAccessPath,
    out: &mut String,
) {
    match access {
        ExplainAccessPath::ByKey { key } => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "ByKey");
            object.field_value_debug("key", key);
            object.finish();
        }
        ExplainAccessPath::ByKeys { keys } => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "ByKeys");
            object.field_debug_slice("keys", keys);
            object.finish();
        }
        ExplainAccessPath::KeyRange { start, end } => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "KeyRange");
            object.field_value_debug("start", start);
            object.field_value_debug("end", end);
            object.finish();
        }
        ExplainAccessPath::IndexPrefix {
            name,
            fields,
            prefix_len,
            values,
        } => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "IndexPrefix");
            object.field_str("name", name);
            object.field_str_slice("fields", fields);
            object.field_u64("prefix_len", *prefix_len as u64);
            object.field_debug_slice("values", values);
            object.finish();
        }
        ExplainAccessPath::IndexMultiLookup {
            name,
            fields,
            values,
        } => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "IndexMultiLookup");
            object.field_str("name", name);
            object.field_str_slice("fields", fields);
            object.field_debug_slice("values", values);
            object.finish();
        }
        ExplainAccessPath::IndexRange {
            name,
            fields,
            prefix_len,
            prefix,
            lower,
            upper,
        } => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "IndexRange");
            object.field_str("name", name);
            object.field_str_slice("fields", fields);
            object.field_u64("prefix_len", *prefix_len as u64);
            object.field_debug_slice("prefix", prefix);
            object.field_value_debug("lower", lower);
            object.field_value_debug("upper", upper);
            object.finish();
        }
        ExplainAccessPath::FullScan => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "FullScan");
            object.finish();
        }
        ExplainAccessPath::Union(children) => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "Union");
            object.field_with("children", |out| {
                out.push('[');
                for (index, child) in children.iter().enumerate() {
                    if index > 0 {
                        out.push(',');
                    }
                    write_access_json(child, out);
                }
                out.push(']');
            });
            object.finish();
        }
        ExplainAccessPath::Intersection(children) => {
            let mut object = JsonWriter::begin_object(out);
            object.field_str("type", "Intersection");
            object.field_with("children", |out| {
                out.push('[');
                for (index, child) in children.iter().enumerate() {
                    if index > 0 {
                        out.push(',');
                    }
                    write_access_json(child, out);
                }
                out.push(']');
            });
            object.finish();
        }
    }
}

///
/// ExplainAccessStrategyLabelProjection
///
/// Shared EXPLAIN-side label projection over the canonical explain-access DTO.
/// This keeps transport-label rendering on one projection contract instead of
/// rebuilding local variant ladders at each consumer boundary.
///

struct ExplainAccessStrategyLabelProjection;

impl AccessPlanProjection<Value> for ExplainAccessStrategyLabelProjection {
    type Output = String;

    fn by_key(&mut self, _key: &Value) -> Self::Output {
        "ByKey".to_string()
    }

    fn by_keys(&mut self, _keys: &[Value]) -> Self::Output {
        "ByKeys".to_string()
    }

    fn key_range(&mut self, _start: &Value, _end: &Value) -> Self::Output {
        "KeyRange".to_string()
    }

    fn index_prefix(
        &mut self,
        index_name: &'static str,
        _index_fields: &[&'static str],
        _prefix_len: usize,
        _values: &[Value],
    ) -> Self::Output {
        let mut label = String::new();
        let _ = write!(&mut label, "IndexPrefix({index_name})");

        label
    }

    fn index_multi_lookup(
        &mut self,
        index_name: &'static str,
        _index_fields: &[&'static str],
        _values: &[Value],
    ) -> Self::Output {
        let mut label = String::new();
        let _ = write!(&mut label, "IndexMultiLookup({index_name})");

        label
    }

    fn index_range(
        &mut self,
        index_name: &'static str,
        _index_fields: &[&'static str],
        _prefix_len: usize,
        _prefix: &[Value],
        _lower: &std::ops::Bound<Value>,
        _upper: &std::ops::Bound<Value>,
    ) -> Self::Output {
        let mut label = String::new();
        let _ = write!(&mut label, "IndexRange({index_name})");

        label
    }

    fn full_scan(&mut self) -> Self::Output {
        "FullScan".to_string()
    }

    fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
        let mut label = String::new();
        let _ = write!(&mut label, "Union({})", children.len());

        label
    }

    fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
        let mut label = String::new();
        let _ = write!(&mut label, "Intersection({})", children.len());

        label
    }
}

/// Render one stable EXPLAIN access label from the canonical explain-access DTO.
pub(in crate::db) fn explain_access_strategy_label(access: &ExplainAccessPath) -> String {
    project_explain_access_path(access, &mut ExplainAccessStrategyLabelProjection)
}

/// Project one execution-node type from the canonical explain-access DTO.
pub(in crate::db) fn explain_access_execution_node_type(
    access: &ExplainAccessPath,
) -> ExplainExecutionNodeType {
    match explain_access_kind_label(access) {
        "by_key" => ExplainExecutionNodeType::ByKeyLookup,
        "by_keys" | "empty_access_contract" => ExplainExecutionNodeType::ByKeysLookup,
        "key_range" => ExplainExecutionNodeType::PrimaryKeyRangeScan,
        "index_prefix" => ExplainExecutionNodeType::IndexPrefixScan,
        "index_multi_lookup" => ExplainExecutionNodeType::IndexMultiLookup,
        "index_range" => ExplainExecutionNodeType::IndexRangeScan,
        "full_scan" => ExplainExecutionNodeType::FullScan,
        "union" => ExplainExecutionNodeType::Union,
        "intersection" => ExplainExecutionNodeType::Intersection,
        other => unreachable!("unexpected explain access kind label: {other}"),
    }
}

pub(in crate::db) fn explain_access_plan<K>(access: &AccessPlan<K>) -> ExplainAccessPath
where
    K: crate::traits::FieldValue,
{
    project_access_plan(access, &mut ExplainAccessProjection)
}

///
/// TESTS
///

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value::Value;

    #[test]
    fn explain_access_execution_node_type_uses_shared_access_classifier() {
        assert_eq!(
            explain_access_execution_node_type(&ExplainAccessPath::ByKey {
                key: Value::Uint(1),
            }),
            ExplainExecutionNodeType::ByKeyLookup,
        );
        assert_eq!(
            explain_access_execution_node_type(&ExplainAccessPath::Union(vec![
                ExplainAccessPath::FullScan,
                ExplainAccessPath::ByKeys {
                    keys: vec![Value::Uint(2)],
                },
            ])),
            ExplainExecutionNodeType::Union,
        );
    }
}