icydb-core 0.94.0

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: query::intent::mutation
//! Responsibility: query-intent mutation helpers for scalar/grouped/load/delete intent state.
//! Does not own: final planner validation or executor route/runtime semantics.
//! Boundary: applies fluent/query API mutations to internal intent state contracts.

#[cfg(feature = "sql")]
use crate::db::query::plan::expr::ProjectionSelection;
use crate::db::{
    predicate::Predicate,
    query::{
        intent::{
            IntentError, KeyAccess, KeyAccessKind, KeyAccessState,
            state::{GroupedIntent, QueryIntent},
        },
        plan::{
            FieldSlot, GroupAggregateSpec, GroupHavingClause, GroupSpec, GroupedExecutionConfig,
            OrderDirection, OrderSpec,
            expr::{BinaryOp, Expr},
            grouped_having_clause_expr_for_group,
        },
    },
};

impl<K> QueryIntent<K> {
    /// Append one filter predicate to scalar intent, implicitly AND-ing chains.
    pub(in crate::db::query::intent) fn append_predicate(&mut self, predicate: Predicate) {
        let scalar = self.scalar_mut();
        scalar.predicate = match scalar.predicate.take() {
            Some(existing) => Some(Predicate::And(vec![existing, predicate])),
            None => Some(predicate),
        };
    }

    /// Append one ascending ORDER BY key to scalar intent.
    pub(in crate::db::query::intent) fn push_order_ascending(&mut self, field: &str) {
        self.push_order_field(field, OrderDirection::Asc);
    }

    /// Append one descending ORDER BY key to scalar intent.
    pub(in crate::db::query::intent) fn push_order_descending(&mut self, field: &str) {
        self.push_order_field(field, OrderDirection::Desc);
    }

    /// Override scalar ORDER BY with one validated order specification.
    pub(in crate::db::query::intent) fn set_order_spec(&mut self, order: OrderSpec) {
        self.scalar_mut().order = Some(order);
    }

    /// Enable DISTINCT semantics in scalar intent state.
    pub(in crate::db::query::intent) const fn set_distinct(&mut self) {
        self.scalar_mut().distinct = true;
    }

    /// Override scalar projection selection with one explicit planner contract.
    #[cfg(feature = "sql")]
    pub(in crate::db::query::intent) fn set_projection_selection(
        &mut self,
        projection_selection: ProjectionSelection,
    ) {
        self.scalar_mut().projection_selection = projection_selection;
    }

    /// Set key access to one single-key lookup.
    pub(in crate::db::query::intent) fn set_by_id(&mut self, id: K) {
        self.set_key_access(KeyAccessKind::Single, KeyAccess::Single(id));
    }

    /// Set key access to one many-key lookup set.
    pub(in crate::db::query::intent) fn set_by_ids<I>(&mut self, ids: I)
    where
        I: IntoIterator<Item = K>,
    {
        self.set_key_access(
            KeyAccessKind::Many,
            KeyAccess::Many(ids.into_iter().collect()),
        );
    }

    /// Set key access to the singleton key path.
    pub(in crate::db::query::intent) fn set_only(&mut self, id: K) {
        self.set_key_access(KeyAccessKind::Only, KeyAccess::Single(id));
    }

    /// Record one grouped key slot while preserving grouped-delete policy semantics.
    pub(in crate::db::query::intent) fn push_group_field_slot(&mut self, field_slot: FieldSlot) {
        let Some(grouped) = self.grouped_mutation_target() else {
            return;
        };

        let group = &mut grouped.group;
        if !group
            .group_fields
            .iter()
            .any(|existing| existing.index() == field_slot.index())
        {
            group.group_fields.push(field_slot);
        }
    }

    /// Record one grouped aggregate terminal while preserving delete policy flags.
    pub(in crate::db::query::intent) fn push_group_aggregate(
        &mut self,
        aggregate: GroupAggregateSpec,
    ) {
        let Some(grouped) = self.grouped_mutation_target() else {
            return;
        };

        grouped.group.aggregates.push(aggregate);
    }

    /// Override grouped hard limits while preserving delete-grouping policy flags.
    pub(in crate::db::query::intent) fn set_grouped_limits(
        &mut self,
        max_groups: u64,
        max_group_bytes: u64,
    ) {
        let Some(grouped) = self.grouped_mutation_target() else {
            return;
        };

        grouped.group.execution =
            GroupedExecutionConfig::with_hard_limits(max_groups, max_group_bytes);
    }

    /// Record one HAVING clause when grouped shape is present.
    ///
    /// Delete mode never materializes grouped shape, so grouped-delete policy is
    /// tracked through delete flags instead of storing grouped clause state.
    pub(in crate::db::query::intent) fn push_having_clause(
        &mut self,
        clause: GroupHavingClause,
    ) -> Result<(), IntentError> {
        if matches!(self, Self::Delete(_)) {
            if self.is_grouped() {
                self.mark_delete_grouping_requested();
                return Ok(());
            }

            return Err(IntentError::having_requires_group_by());
        }

        let Some(grouped) = self.grouped_mut() else {
            return Err(IntentError::having_requires_group_by());
        };

        let clause = grouped_having_clause_expr(&grouped.group, &clause)?;
        grouped.having_expr = Some(match grouped.having_expr.take() {
            Some(existing) => Expr::Binary {
                op: BinaryOp::And,
                left: Box::new(existing),
                right: Box::new(clause),
            },
            None => clause,
        });

        Ok(())
    }

    /// Record one widened grouped HAVING expression when grouped shape is present.
    pub(in crate::db::query::intent) fn push_having_expr(
        &mut self,
        expr: Expr,
    ) -> Result<(), IntentError> {
        if matches!(self, Self::Delete(_)) {
            if self.is_grouped() {
                self.mark_delete_grouping_requested();
                return Ok(());
            }

            return Err(IntentError::having_requires_group_by());
        }

        let Some(grouped) = self.grouped_mut() else {
            return Err(IntentError::having_requires_group_by());
        };

        grouped.having_expr = Some(match grouped.having_expr.take() {
            Some(existing) => Expr::Binary {
                op: BinaryOp::And,
                left: Box::new(existing),
                right: Box::new(expr),
            },
            None => expr,
        });

        Ok(())
    }

    // Record key-access origin and detect conflicting key-only builder usage.
    fn set_key_access(&mut self, kind: KeyAccessKind, access: KeyAccess<K>) {
        let scalar = self.scalar_mut();
        if let Some(existing) = &scalar.key_access
            && existing.kind != kind
        {
            scalar.key_access_conflict = true;
        }

        scalar.key_access = Some(KeyAccessState { kind, access });
    }

    // Append one ORDER BY field while preserving any previously-declared order.
    fn push_order_field(&mut self, field: &str, direction: OrderDirection) {
        let scalar = self.scalar_mut();
        scalar.order = Some(match scalar.order.take() {
            Some(mut spec) => {
                spec.fields.push((field.to_string(), direction));
                spec
            }
            None => OrderSpec {
                fields: vec![(field.to_string(), direction)],
            },
        });
    }

    // Route grouped declaration mutations onto one materialized grouped shape,
    // or preserve delete-mode grouping policy when grouped state is forbidden.
    fn grouped_mutation_target(&mut self) -> Option<&mut GroupedIntent<K>> {
        if matches!(self, Self::Delete(_)) {
            self.mark_delete_grouping_requested();
            return None;
        }

        Some(self.ensure_grouped_mut())
    }
}

fn grouped_having_clause_expr(
    group: &GroupSpec,
    clause: &GroupHavingClause,
) -> Result<Expr, IntentError> {
    grouped_having_clause_expr_for_group(group, clause)
        .ok_or_else(IntentError::having_references_unknown_aggregate)
}