icydb-core 0.204.0

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! Module: db::schema::mutation
//! Responsibility: catalog-native schema mutation contracts.
//! Does not own: SQL DDL parsing, physical rebuild execution, or schema-store writes.
//! Boundary: describes accepted snapshot changes before reconciliation persists them.

use crate::db::{
    data::CanonicalSlotReader,
    index::{
        IndexEntryValue, IndexId, IndexKey, IndexState, IndexStore, IndexStoreVisit,
        RawIndexStoreKey,
    },
    key_taxonomy::PrimaryKeyValue,
    predicate::PredicateProgram,
    schema::PersistedFieldSnapshot,
};
use crate::error::InternalError;
use crate::types::EntityTag;

#[cfg(feature = "sql")]
mod field;
#[cfg(feature = "sql")]
pub(in crate::db) use field::{
    SchemaDdlFieldDefaultCandidateError, SchemaDdlFieldDropCandidateError,
    SchemaDdlFieldNullabilityCandidateError, SchemaDdlFieldRenameCandidateError,
    SchemaFieldAdditionTarget, SchemaFieldDefaultTarget, SchemaFieldDropTarget,
    SchemaFieldNullabilityTarget, SchemaFieldRenameTarget,
    derive_sql_ddl_field_addition_accepted_after, derive_sql_ddl_field_default_accepted_after,
    derive_sql_ddl_field_drop_accepted_after, derive_sql_ddl_field_nullability_accepted_after,
    derive_sql_ddl_field_rename_accepted_after, resolve_sql_ddl_field_drop_candidate,
    resolve_sql_ddl_field_drop_default_candidate, resolve_sql_ddl_field_nullability_candidate,
    resolve_sql_ddl_field_rename_candidate, resolve_sql_ddl_field_set_default_candidate,
    validate_sql_ddl_field_default_change_candidate,
};
#[cfg(all(test, feature = "sql"))]
pub(in crate::db) use field::{
    admit_sql_ddl_field_addition_candidate, admit_sql_ddl_field_default_candidate,
    admit_sql_ddl_field_drop_candidate, admit_sql_ddl_field_nullability_candidate,
    admit_sql_ddl_field_rename_candidate,
};

#[cfg(feature = "sql")]
mod field_allocation;
#[cfg(feature = "sql")]
pub(in crate::db) use field_allocation::{
    SchemaDdlFieldAdditionCandidateError, build_sql_ddl_field_addition_candidate,
    resolve_sql_ddl_field_addition_name_candidate,
};

#[cfg(feature = "sql")]
mod field_default_encoding;
#[cfg(feature = "sql")]
pub(in crate::db) use field_default_encoding::{
    encode_sql_ddl_add_column_default, encode_sql_ddl_alter_column_default,
};

#[cfg(feature = "sql")]
mod field_type;
#[cfg(feature = "sql")]
pub(in crate::db) use field_type::{
    SchemaDdlFieldTypeContract, resolve_sql_ddl_field_type_contract,
};

#[cfg(feature = "sql")]
mod ddl_admission;
#[cfg(feature = "sql")]
#[cfg_attr(
    not(test),
    expect(
        unused_imports,
        reason = "schema root re-exports DDL schema-version admission diagnostics"
    )
)]
pub(in crate::db) use ddl_admission::{
    SchemaDdlAcceptedSnapshotDerivation, SchemaDdlIndexDropCandidateError,
    SchemaDdlMutationAdmission, SchemaDdlMutationAdmissionError, SchemaDdlMutationTarget,
    SchemaDdlSchemaVersionAdmissionError, SchemaDdlVersionContractPreflightError,
    validate_schema_ddl_version_contract_preflight,
};

mod delta;
#[cfg_attr(
    not(test),
    expect(
        unused_imports,
        reason = "mutation planning tests consume delta classifiers through the module root"
    )
)]
pub(in crate::db::schema) use delta::{
    SchemaMutationDelta, classify_schema_mutation_delta, schema_mutation_request_for_snapshots,
};

#[cfg(feature = "sql")]
mod index_candidate;
#[cfg(feature = "sql")]
pub(in crate::db) use index_candidate::{
    SchemaDdlSecondaryIndexAdditionCandidate, SchemaDdlSecondaryIndexAdditionCandidateError,
    SchemaDdlSecondaryIndexExpressionIntent, SchemaDdlSecondaryIndexExpressionOpIntent,
    SchemaDdlSecondaryIndexFieldPathIntent, SchemaDdlSecondaryIndexKeyCandidateError,
    SchemaDdlSecondaryIndexKeyIntent, build_sql_ddl_secondary_index_candidate,
    resolve_sql_ddl_secondary_index_addition_candidate,
};

mod index;
#[cfg(feature = "sql")]
pub(in crate::db) use index::SchemaSecondaryIndexDropCleanupTarget;
#[cfg(any(test, feature = "sql"))]
pub(in crate::db) use index::{
    SchemaExpressionIndexRebuildExpression, SchemaExpressionIndexRebuildKey,
};
pub(in crate::db) use index::{
    SchemaExpressionIndexRebuildTarget, SchemaFieldPathIndexRebuildKey,
    SchemaFieldPathIndexRebuildTarget,
};
#[cfg(all(test, feature = "sql"))]
pub(in crate::db) use index::{
    admit_sql_ddl_expression_index_candidate, admit_sql_ddl_field_path_index_candidate,
    admit_sql_ddl_secondary_index_drop_candidate,
};
#[cfg(feature = "sql")]
pub(in crate::db) use index::{
    derive_sql_ddl_expression_index_accepted_after, derive_sql_ddl_field_path_index_accepted_after,
    derive_sql_ddl_secondary_index_drop_accepted_after,
    resolve_sql_ddl_secondary_index_drop_candidate,
};

mod staged_index_validation;
pub(in crate::db::schema) use staged_index_validation::{
    SchemaStagedIndexValidationError, staged_index_keys_have_duplicate_unique_components,
};

///
/// SchemaMutationRequest
///
/// Internal request vocabulary that lowers catalog-level mutation intent into
/// a deterministic `MutationPlan`. SQL DDL and generated proposal comparison
/// must route through this type instead of constructing plans ad hoc.
///

#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db::schema) enum SchemaMutationRequest<'a> {
    ExactMatch,
    AppendOnlyFields(&'a [PersistedFieldSnapshot]),
    AddFieldPathIndex {
        target: SchemaFieldPathIndexRebuildTarget,
    },
    AddExpressionIndex {
        target: SchemaExpressionIndexRebuildTarget,
    },
}

///
/// AcceptedSchemaMutationError
///
/// Fail-closed reason produced while lowering accepted schema metadata into a
/// mutation request. These errors mean the mutation framework cannot describe
/// a safe catalog operation yet; callers must not compensate with generated
/// metadata.
///

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) enum AcceptedSchemaMutationError {
    UnsupportedIndexKeyShape,
    EmptyIndexKey,
    ExpressionIndexRequiresExpressionKey,
}

mod runner;
pub(in crate::db::schema) use self::runner::*;

mod field_path;
pub(in crate::db::schema) use self::field_path::*;

mod expression;
#[cfg(any(test, feature = "sql"))]
pub(in crate::db::schema) use self::expression::*;

///
/// MutationPlan
///
/// Deterministic schema-owned plan for moving one accepted snapshot to the
/// next. Every variant is a current publication or physical-execution shape;
/// rejected snapshot deltas never become mutation plans.
///

#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db::schema) enum MutationPlan {
    MetadataOnly,
    FieldPathIndexRebuild {
        target: SchemaFieldPathIndexRebuildTarget,
    },
    ExpressionIndexRebuild {
        target: SchemaExpressionIndexRebuildTarget,
    },
}

/// Schema-owned publication boundary for a current mutation plan.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db::schema) enum MutationPublicationPreflight {
    PublishableNow,
    RequiresPhysicalWork,
}

impl MutationPlan {
    /// Build the no-op plan for equal accepted snapshots.
    pub(in crate::db::schema) const fn exact_match() -> Self {
        Self::MetadataOnly
    }

    /// Plan a field-path index rebuild from accepted index metadata.
    const fn field_path_index_addition(target: SchemaFieldPathIndexRebuildTarget) -> Self {
        Self::FieldPathIndexRebuild { target }
    }

    /// Plan an accepted deterministic expression-index rebuild.
    const fn expression_index_addition(target: SchemaExpressionIndexRebuildTarget) -> Self {
        Self::ExpressionIndexRebuild { target }
    }

    /// Return the sole publication decision for this plan.
    #[must_use]
    pub(in crate::db::schema) const fn publication_preflight(
        &self,
    ) -> MutationPublicationPreflight {
        match self {
            Self::MetadataOnly => MutationPublicationPreflight::PublishableNow,
            Self::FieldPathIndexRebuild { .. } | Self::ExpressionIndexRebuild { .. } => {
                MutationPublicationPreflight::RequiresPhysicalWork
            }
        }
    }

    #[must_use]
    pub(in crate::db::schema) const fn field_path_index_target(
        &self,
    ) -> Option<&SchemaFieldPathIndexRebuildTarget> {
        match self {
            Self::FieldPathIndexRebuild { target } => Some(target),
            Self::MetadataOnly | Self::ExpressionIndexRebuild { .. } => None,
        }
    }

    #[cfg(any(test, feature = "sql"))]
    #[must_use]
    pub(in crate::db::schema) const fn expression_index_target(
        &self,
    ) -> Option<&SchemaExpressionIndexRebuildTarget> {
        match self {
            Self::ExpressionIndexRebuild { target } => Some(target),
            Self::MetadataOnly | Self::FieldPathIndexRebuild { .. } => None,
        }
    }
}

impl From<SchemaMutationRequest<'_>> for MutationPlan {
    fn from(request: SchemaMutationRequest<'_>) -> Self {
        match request {
            SchemaMutationRequest::ExactMatch => Self::exact_match(),
            SchemaMutationRequest::AppendOnlyFields(_) => Self::MetadataOnly,
            SchemaMutationRequest::AddFieldPathIndex { target } => {
                Self::field_path_index_addition(target)
            }
            SchemaMutationRequest::AddExpressionIndex { target } => {
                Self::expression_index_addition(target)
            }
        }
    }
}

#[cfg(test)]
mod tests;