safe-migrate 0.8.0

Check PostgreSQL migrations against a synchronized database baseline
Documentation
use super::{AnalysisState, MutationResult};
use crate::_internal::analysis::evidence::{EvidenceCode, EvidenceScope};
use crate::_internal::analysis::mutations::{
    ReleaseSavepointMutation, RollbackToSavepointMutation, SavepointMutation,
};
use crate::_internal::analysis::transaction::TransactionFrame;

impl AnalysisState {
    pub(super) fn apply_begin_transaction(&mut self) -> MutationResult {
        if self.local.transactions.is_empty() {
            self.local.transactions.push(TransactionFrame::root());
            MutationResult::Applied
        } else {
            MutationResult::Skipped
        }
    }

    pub(super) fn apply_commit_transaction(&mut self, chain: bool) -> MutationResult {
        if chain && self.local.transactions.is_empty() {
            self.taint(EvidenceCode::TransactionStateUnknown, EvidenceScope::Chain);
            return MutationResult::Conflict {
                reason: "COMMIT AND CHAIN can only be used in transaction blocks".to_string(),
            };
        }

        if self.local.transaction_aborted {
            while let Some(frame) = self.local.transactions.pop() {
                self.rollback_frame(frame);
            }
        } else {
            while self.local.transactions.pop().is_some() {}
            self.restore_persistent_role_context();
        }
        self.local.transaction_aborted = false;
        if chain {
            self.local.transactions.push(TransactionFrame::root());
        }
        MutationResult::Applied
    }

    pub(super) fn apply_rollback_transaction(&mut self, chain: bool) -> MutationResult {
        if chain && self.local.transactions.is_empty() {
            self.taint(EvidenceCode::TransactionStateUnknown, EvidenceScope::Chain);
            return MutationResult::Conflict {
                reason: "ROLLBACK AND CHAIN can only be used in transaction blocks".to_string(),
            };
        }
        while let Some(frame) = self.local.transactions.pop() {
            self.rollback_frame(frame);
        }
        self.local.transaction_aborted = false;
        if chain {
            self.local.transactions.push(TransactionFrame::root());
        }
        MutationResult::Applied
    }

    pub(super) fn apply_rollback_to_savepoint(
        &mut self,
        rollback: &RollbackToSavepointMutation,
    ) -> MutationResult {
        let Some(position) = self
            .local
            .transactions
            .iter()
            .rposition(|frame| frame.is_named_savepoint(&rollback.name))
        else {
            self.taint(EvidenceCode::TransactionStateUnknown, EvidenceScope::Chain);
            if !self.local.transactions.is_empty() {
                self.local.transaction_aborted = true;
            }
            return MutationResult::Conflict {
                reason: format!("savepoint '{}' does not exist", rollback.name),
            };
        };
        let rolled_back = self.local.transactions.split_off(position + 1);
        for frame in rolled_back.into_iter().rev() {
            self.rollback_frame(frame);
        }
        let undo_log = std::mem::take(&mut self.local.transactions[position].undo_log);
        self.rollback_undo_log(undo_log);
        self.local.transaction_aborted = false;
        MutationResult::Applied
    }

    pub(super) fn apply_savepoint(&mut self, savepoint: &SavepointMutation) -> MutationResult {
        if self.local.transactions.is_empty() {
            self.taint(EvidenceCode::TransactionStateUnknown, EvidenceScope::Chain);
            return MutationResult::Conflict {
                reason: "SAVEPOINT can only be used in transaction blocks".to_string(),
            };
        }
        self.local
            .transactions
            .push(TransactionFrame::savepoint(savepoint.name.clone()));
        MutationResult::Applied
    }

    pub(super) fn apply_release_savepoint(
        &mut self,
        release: &ReleaseSavepointMutation,
    ) -> MutationResult {
        let Some(position) = self
            .local
            .transactions
            .iter()
            .rposition(|frame| frame.is_named_savepoint(&release.name))
        else {
            self.taint(EvidenceCode::TransactionStateUnknown, EvidenceScope::Chain);
            if !self.local.transactions.is_empty() {
                self.local.transaction_aborted = true;
            }
            return MutationResult::Conflict {
                reason: format!("savepoint '{}' does not exist", release.name),
            };
        };
        if position == 0 {
            self.taint(EvidenceCode::TransactionStateUnknown, EvidenceScope::Chain);
            return MutationResult::Conflict {
                reason: format!("savepoint '{}' is not inside a transaction", release.name),
            };
        }

        let released = self.local.transactions.split_off(position);
        let Some(outer) = self.local.transactions.last_mut() else {
            self.taint(EvidenceCode::TransactionStateUnknown, EvidenceScope::Chain);
            return MutationResult::Conflict {
                reason: format!("savepoint '{}' is not inside a transaction", release.name),
            };
        };
        for frame in released {
            outer.undo_log.extend(frame.undo_log);
        }
        MutationResult::Applied
    }
}