icydb-core 0.188.0

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: db::session::sql::projection::labels
//! Responsibility: derive stable outward SQL projection column labels from
//! structural plans, prepared projection specs, and computed SQL surfaces.
//! Does not own: projection execution or projection payload storage.
//! Boundary: keeps SQL projection naming policy at the session boundary.

use super::SqlProjectionContract;
use crate::db::{
    query::builder::scalar_projection::render_scalar_projection_expr_plan_label,
    query::plan::expr::{Expr, ProjectionField, ProjectionSpec},
};
#[cfg(feature = "sql-explain")]
use crate::{
    db::query::{
        explain::{ExplainExecutionNodeDescriptor, property_keys, property_values},
        plan::AccessPlannedQuery,
    },
    value::Value,
};

// Render canonical projection labels from one projection spec regardless of
// whether the caller arrived from a typed or structural query shell.
pub(in crate::db::session::sql) fn projection_labels_from_projection_spec(
    projection: &ProjectionSpec,
) -> Vec<String> {
    let mut labels = Vec::with_capacity(projection.len());

    // Derive outward labels directly from the frozen projection spec so the
    // session boundary does not bounce through one-expression helper wrappers.
    for field in projection.fields() {
        match field {
            ProjectionField::Scalar {
                expr: _,
                alias: Some(alias),
            } => labels.push(alias.as_str().to_string()),
            ProjectionField::Scalar { expr, alias: None } => {
                labels.push(match expr {
                    Expr::Field(field) => field.as_str().to_string(),
                    Expr::Aggregate(aggregate) => {
                        let kind = aggregate.kind().canonical_label();
                        let distinct = if aggregate.is_distinct() {
                            "DISTINCT "
                        } else {
                            ""
                        };
                        if let Some(input_expr) = aggregate.input_expr() {
                            let input = render_scalar_projection_expr_plan_label(input_expr);

                            format!("{kind}({distinct}{input})")
                        } else {
                            format!("{kind}({distinct}*)")
                        }
                    }
                    #[cfg(test)]
                    Expr::Alias { name, .. } => name.as_str().to_string(),
                    Expr::FieldPath(_)
                    | Expr::Literal(_)
                    | Expr::FunctionCall { .. }
                    | Expr::Case { .. }
                    | Expr::Binary { .. }
                    | Expr::Unary { .. } => render_scalar_projection_expr_plan_label(expr),
                });
            }
        }
    }

    labels
}

// Derive fixed decimal display scales for outward SQL projection columns.
// This preserves `ROUND(..., scale)` display semantics even when the outward
// SQL column label is aliased and no longer exposes the original function
// text to downstream renderers.
pub(in crate::db::session::sql) fn projection_fixed_scales_from_projection_spec(
    projection: &ProjectionSpec,
) -> Vec<Option<u32>> {
    projection
        .fields()
        .map(|field| match field {
            // Recover fixed ROUND(...) display scales directly from the frozen
            // projection expression instead of bouncing through a one-call
            // extractor helper.
            ProjectionField::Scalar { expr, .. } => {
                let Expr::FunctionCall { function, args } = expr else {
                    return None;
                };
                function.fixed_decimal_scale(args)
            }
        })
        .collect()
}

// Build the outward SQL projection contract from one frozen projection spec in
// one owner-local place so SELECT and aggregate execution do not duplicate the
// label/fixed-scale pairing.
pub(in crate::db::session::sql) fn projection_contract_from_projection_spec(
    projection: &ProjectionSpec,
) -> SqlProjectionContract {
    SqlProjectionContract::new(
        projection_labels_from_projection_spec(projection),
        projection_fixed_scales_from_projection_spec(projection),
    )
}

// Attach SQL-facing projection labels and shell-facing projection runtime hints
// only at the session SQL boundary so executor-owned EXPLAIN assembly stays
// structural.
#[cfg(feature = "sql-explain")]
pub(in crate::db::session::sql) fn annotate_sql_projection_debug_on_execution_descriptor(
    descriptor: &mut ExplainExecutionNodeDescriptor,
    plan: &AccessPlannedQuery,
    projection: &ProjectionSpec,
) {
    let labels = projection_labels_from_projection_spec(projection)
        .into_iter()
        .map(Value::from)
        .collect();
    descriptor
        .node_properties
        .insert(property_keys::PROJECTION_FIELDS, Value::List(labels));

    // Classify the materialization mode from the frozen planner contract
    // directly here so SQL EXPLAIN does not round-trip through a single-use
    // wrapper just to attach one stable debug label.
    let materialization = if !plan.scalar_plan().mode.is_load()
        || plan.grouped_plan().is_some()
        || plan.scalar_projection_plan().is_none()
    {
        None
    } else if descriptor.covering_scan() == Some(true) {
        Some(property_values::COVERING_READ)
    } else {
        // Recognize the retained-slot direct projection shape directly from
        // the planner-frozen projection metadata instead of routing through a
        // single-use predicate helper.
        let direct_slot_projection =
            plan.frozen_direct_projection_slots()
                .is_some_and(|direct_projection_slots| {
                    projection.len() == direct_projection_slots.len()
                        && projection
                            .fields()
                            .all(|field| field.direct_field_name().is_some())
                });

        if direct_slot_projection {
            Some(property_values::DIRECT_SLOT_ROW)
        } else {
            Some(property_values::SCALAR_PROJECTION)
        }
    };

    if let Some(materialization) = materialization {
        descriptor.node_properties.insert(
            property_keys::PROJECTION_MATERIALIZATION,
            Value::from(materialization),
        );
    }
}