use crate::analysis::mutations::Mutation;
use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
use crate::ast::identifiers::ObjectId;
use crate::engine::config::Config;
use crate::report::violations::{ObjectKind, OperationKind, Violation, ViolationTier};
use crate::rules::Rule;
pub struct DriftDetectionRule;
impl Rule for DriftDetectionRule {
fn id(&self) -> &'static str {
"schema-drift"
}
fn default_tier(&self) -> ViolationTier {
ViolationTier::Tier1
}
fn recipe(&self) -> &'static str {
"This migration references a database object that does not exist in the production baseline. If this object exists in production, sync the cache with `safe-migrate sync`. If it does not, this migration may fail."
}
fn evaluate(
&self,
mutation: &Mutation,
_result: &MutationResult,
pre_state: &crate::analysis::state::PreState,
state: &AnalysisState,
_config: &Config,
_cascade_closure: Option<&CascadeResult>,
) -> Vec<Violation> {
let mut violations = Vec::new();
match mutation {
Mutation::Opaque(crate::analysis::mutations::OpaqueMutation::UnresolvedReference {
object_kind,
object_name,
}) => {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::Other("unresolved_reference".to_string()),
object_kind: object_kind.clone(),
object_name: object_name.clone(),
tier: self.default_tier(),
reason: format!(
"Migration references {} \"{}\" which does not exist in the production baseline",
object_kind,
object_name
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
Mutation::DropTable(d) => {
if !pre_state.relations.contains_key(&d.id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::DropTable,
object_kind: ObjectKind::Table,
object_name: d.id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration DROPs table \"{}\" which does not exist in the production baseline",
d.id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
Mutation::AlterTable(a) => {
if !pre_state.relations.contains_key(&a.id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::Other("alter_table".to_string()),
object_kind: ObjectKind::Table,
object_name: a.id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration ALTERs table \"{}\" which does not exist in the production baseline",
a.id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
Mutation::DropView(d) => {
for id in &d.ids {
if !pre_state.relations.contains_key(id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::DropView,
object_kind: ObjectKind::View,
object_name: id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration DROPs view \"{}\" which does not exist in the production baseline",
id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
}
Mutation::DropMaterializedView(d) => {
for id in &d.ids {
if !pre_state.relations.contains_key(id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::DropMaterializedView,
object_kind: ObjectKind::MaterializedView,
object_name: id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration DROPs materialized view \"{}\" which does not exist in the production baseline",
id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
}
Mutation::DropSequence(d) => {
for id in &d.ids {
if !pre_state.sequences.contains_key(id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::DropSequence,
object_kind: ObjectKind::Sequence,
object_name: id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration DROPs sequence \"{}\" which does not exist in the production baseline",
id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
}
Mutation::DropFunction(d) => {
for sig in &d.signatures {
let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
let schema = state.resolve_function_schema(&sig.name, &sig_str);
let id = ObjectId::new(schema, sig_str);
if !pre_state.functions.contains_key(&id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::DropFunction,
object_kind: ObjectKind::Function,
object_name: id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration DROPs function \"{}\" which does not exist in the production baseline",
id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
}
Mutation::DropProcedure(d) => {
for sig in &d.signatures {
let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
let schema = state.resolve_function_schema(&sig.name, &sig_str);
let id = ObjectId::new(schema, sig_str);
if !pre_state.functions.contains_key(&id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::DropProcedure,
object_kind: ObjectKind::Procedure,
object_name: id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration DROPs procedure \"{}\" which does not exist in the production baseline",
id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
}
Mutation::DropIndex(d) => {
if !pre_state.indexes.iter().any(|idx| idx.index_id == d.id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::DropIndex,
object_kind: ObjectKind::Index,
object_name: d.id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration DROPs index \"{}\" which does not exist in the production baseline",
d.id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
Mutation::DropDomain(d) => {
for id in &d.ids {
if !pre_state.types.contains_key(id) {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::DropDomain,
object_kind: ObjectKind::Domain,
object_name: id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration DROPs domain \"{}\" which does not exist in the production baseline",
id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
}
Mutation::AlterType(a) if !pre_state.types.contains_key(&a.id) => {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::AlterType,
object_kind: ObjectKind::Type,
object_name: a.id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration ALTERs type \"{}\" which does not exist in the production baseline",
a.id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
Mutation::AlterFunction(f) if !pre_state.functions.contains_key(&f.id) => {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::AlterFunction,
object_kind: ObjectKind::Function,
object_name: f.id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration ALTERs function \"{}\" which does not exist in the production baseline",
f.id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
Mutation::AlterProcedure(p) if !pre_state.functions.contains_key(&p.id) => {
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::AlterProcedure,
object_kind: ObjectKind::Procedure,
object_name: p.id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration ALTERs procedure \"{}\" which does not exist in the production baseline",
p.id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
Mutation::CreateTable(c) => {
if let Some(parent_id) = &c.partition_of
&& !pre_state.relations.contains_key(parent_id)
{
violations.push(Violation { source_range: None,
rule_id: self.id(),
operation_kind: OperationKind::CreateTable,
object_kind: ObjectKind::Table,
object_name: c.id.to_string(),
tier: self.default_tier(),
reason: format!(
"Migration creates {} as a partition of parent \"{}\" which does not exist in the production baseline. Parent must be created first.",
c.id, parent_id
),
recipe: self.recipe(),
dedup_key: None,
sql: None,
});
}
}
_ => {}
}
violations
}
}