Skip to main content

icydb_core/db/schema_evolution/
execution.rs

1//! Module: db::schema_evolution::execution
2//! Responsibility: guard schema migration execution through the completed registry.
3//! Does not own: migration row-op execution or commit durability.
4//! Boundary: registry + planner -> migration execution engine.
5
6use crate::{
7    db::{
8        Db,
9        migration::{self, MigrationRunOutcome, MigrationRunState},
10        schema_evolution::{MigrationRegistry, SchemaMigrationDescriptor, SchemaMigrationPlanner},
11    },
12    error::InternalError,
13    traits::CanisterKind,
14};
15
16///
17/// SchemaMigrationExecutionOutcome
18///
19/// SchemaMigrationExecutionOutcome reports whether schema evolution skipped an
20/// already-applied migration or delegated a planned migration to `db::migration`.
21/// The lower migration outcome is preserved unchanged when execution occurs.
22///
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum SchemaMigrationExecutionOutcome {
26    AlreadyApplied,
27    Executed(MigrationRunOutcome),
28}
29
30impl SchemaMigrationExecutionOutcome {
31    /// Return whether schema evolution skipped execution because the registry
32    /// already contains this migration id/version.
33    #[must_use]
34    pub const fn already_applied(self) -> bool {
35        matches!(self, Self::AlreadyApplied)
36    }
37
38    /// Return the lower migration-run outcome when execution occurred.
39    #[must_use]
40    pub const fn migration_outcome(self) -> Option<MigrationRunOutcome> {
41        match self {
42            Self::AlreadyApplied => None,
43            Self::Executed(outcome) => Some(outcome),
44        }
45    }
46}
47
48/// Execute one schema migration descriptor through the derivation layer.
49pub(in crate::db) fn execute_schema_migration_descriptor<C: CanisterKind>(
50    db: &Db<C>,
51    registry: &mut MigrationRegistry,
52    planner: &SchemaMigrationPlanner,
53    descriptor: &SchemaMigrationDescriptor,
54    max_steps: usize,
55) -> Result<SchemaMigrationExecutionOutcome, InternalError> {
56    // Phase 1: enforce completed-migration idempotency before deriving a row-op
57    // plan or touching the lower migration engine.
58    if registry.is_applied(descriptor.migration_id(), descriptor.version()) {
59        return Ok(SchemaMigrationExecutionOutcome::AlreadyApplied);
60    }
61
62    // Phase 2: derive and execute through the existing migration engine. This
63    // function intentionally does not duplicate step execution semantics.
64    let plan = planner.plan(descriptor)?;
65    let outcome = migration::execute_migration_plan(db, &plan, max_steps)?;
66
67    // Phase 3: record completion only after the lower engine reports a completed
68    // run. Bounded runs that need resume must remain executable.
69    if matches!(outcome.state(), MigrationRunState::Complete) {
70        registry.record_applied(descriptor.migration_id(), descriptor.version());
71    }
72
73    Ok(SchemaMigrationExecutionOutcome::Executed(outcome))
74}