Skip to main content

safe_migrate/analysis/
state.rs

1// FILE: src/analysis/state.rs
2use crate::analysis::facts::{SearchPathTarget, TableConstraintFact};
3use crate::analysis::graph::{DependencyEdge, DependencyGraph, DependencyKind};
4use crate::analysis::mutations::{
5    AlterTableActionMutation, AlterTypeActionMutation, Mutation, PersistenceMutation,
6};
7use crate::analysis::transaction::{StateChange, TransactionFrame};
8use crate::ast::identifiers::ObjectId;
9use crate::db::cache::DbCache;
10use crate::model::constraint::{ConstraintKind, ConstraintState};
11pub use crate::model::relation::RelationOverlay;
12use crate::model::relation::{ColumnAction, Persistence, Privilege, RelationKind, RelationState};
13use crate::model::sequence::{SequenceOverlay, SequenceState};
14use crate::model::trigger::TriggerOverlay;
15use crate::model::types::{TypeKind, TypeOverlay, TypeState};
16use std::collections::{HashMap, HashSet};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum Confidence {
20    Exact,
21    Tainted,
22}
23
24#[derive(Debug, PartialEq, Eq)]
25pub enum MutationResult {
26    Applied,
27    Skipped,
28    Conflict { reason: String },
29}
30
31#[derive(Debug, Default, Clone)]
32pub struct CascadeResult {
33    pub dropped_relations: HashSet<ObjectId>,
34    pub dropped_indexes: HashSet<ObjectId>,
35    pub dropped_constraints: HashSet<(ObjectId, String)>,
36}
37
38pub struct LocalState {
39    pub relations: HashMap<ObjectId, RelationOverlay>,
40    pub types: HashMap<ObjectId, TypeOverlay>,
41    pub functions: HashMap<ObjectId, crate::model::function::FunctionOverlay>,
42    pub sequences: HashMap<ObjectId, SequenceOverlay>,
43    pub publications: HashMap<String, crate::model::replication::PublicationOverlay>,
44    pub subscriptions: HashMap<String, crate::model::replication::SubscriptionOverlay>,
45    pub roles: HashMap<ObjectId, crate::model::role::RoleOverlay>,
46    pub triggers: HashMap<ObjectId, TriggerOverlay>,
47    pub constraints: HashMap<(ObjectId, String), ConstraintState>,
48    pub graph: DependencyGraph,
49    pub search_path: Vec<String>,
50    pub default_search_path: Vec<String>,
51    pub current_role: String,
52    pub confidence: Confidence,
53    pub transactions: Vec<TransactionFrame>,
54    pub pending_validation: HashSet<(ObjectId, String)>,
55    pub generation_counter: u64,
56}
57
58#[derive(Clone, Debug)]
59pub struct PreState {
60    pub relations: HashMap<ObjectId, crate::model::relation::RelationState>,
61    pub functions: HashMap<ObjectId, crate::model::function::FunctionState>,
62    pub roles: HashMap<ObjectId, crate::model::role::RoleState>,
63    pub publications: HashMap<String, crate::model::replication::PublicationState>,
64    pub subscriptions: HashMap<String, crate::model::replication::SubscriptionState>,
65    pub sequences: HashMap<ObjectId, crate::model::sequence::SequenceState>,
66    pub types: HashMap<ObjectId, crate::model::types::TypeState>,
67    pub indexes: Vec<crate::analysis::graph::DependencyEdge>,
68}
69
70pub struct AnalysisState {
71    pub pg_version_num: Option<u32>,
72    pub baseline_relations: HashSet<ObjectId>,
73    pub baseline_indexes: HashSet<ObjectId>,
74    pub baseline_foreign_keys: HashSet<(ObjectId, String)>,
75    pub baseline_fk_dependencies: HashSet<ObjectId>,
76    pub local: LocalState,
77}
78
79impl AnalysisState {
80    pub fn new(cache: DbCache) -> Self {
81        let default_search_path = cache.search_path.clone();
82        let mut relations: HashMap<ObjectId, RelationOverlay> = HashMap::new();
83        let mut baseline_relations = HashSet::new();
84        let mut baseline_indexes = HashSet::new();
85        let mut baseline_foreign_keys = HashSet::new();
86        let mut baseline_fk_dependencies = HashSet::new();
87        let mut triggers = HashMap::new();
88        let mut constraints = HashMap::new();
89        let mut types = HashMap::new();
90        let mut graph = DependencyGraph::new();
91
92        for (id, rel_state) in cache.baseline_relations() {
93            if rel_state.is_fk_dependency {
94                baseline_fk_dependencies.insert(id.clone());
95            }
96            relations.insert(id.clone(), RelationOverlay::Present(rel_state.clone()));
97            baseline_relations.insert(id.clone());
98        }
99
100        for (id, type_state) in &cache.types {
101            types.insert(id.clone(), TypeOverlay::Present(type_state.clone()));
102        }
103
104        for fk in cache.foreign_keys {
105            baseline_foreign_keys.insert((fk.from_table.clone(), fk.constraint_name.clone()));
106            graph.edges.push(DependencyEdge::new(
107                fk.from_table,
108                fk.to_table,
109                DependencyKind::ForeignKey {
110                    constraint_name: Some(fk.constraint_name),
111                    from_columns: Vec::new(),
112                    to_columns: Vec::new(),
113                    from_generation: 0,
114                },
115            ));
116        }
117
118        for idx in cache.indexes {
119            // BUG-008: index ObjectIds go into baseline_indexes, not baseline_relations
120            baseline_indexes.insert(idx.index_id.clone());
121            graph.edges.push(DependencyEdge::new(
122                idx.index_id,
123                idx.table_id,
124                DependencyKind::IndexOnRelation {
125                    using_method: None,
126                    has_predicate: false,
127                    is_concurrent: false,
128                    is_unique: false,
129                },
130            ));
131        }
132
133        for dependency in cache.dependencies {
134            if dependency.deptype != "view" {
135                continue;
136            }
137            let (Some(obj_schema), Some(obj_name), Some(ref_schema), Some(ref_name)) = (
138                dependency.obj_schema,
139                dependency.obj_name,
140                dependency.ref_schema,
141                dependency.ref_name,
142            ) else {
143                continue;
144            };
145            let dependent = ObjectId::new(obj_schema, obj_name);
146            let referenced = ObjectId::new(ref_schema, ref_name);
147            let is_view = relations.get(&dependent).is_some_and(|relation| {
148                matches!(
149                    relation,
150                    RelationOverlay::Present(state)
151                        if matches!(
152                            state.kind,
153                            crate::model::relation::RelationKind::View
154                                | crate::model::relation::RelationKind::MaterializedView
155                        )
156                )
157            });
158            if is_view && relations.contains_key(&referenced) {
159                graph.edges.push(DependencyEdge::new(
160                    dependent,
161                    referenced,
162                    DependencyKind::ViewDependency { view_generation: 0 },
163                ));
164            }
165        }
166
167        for constraint in cache.constraints {
168            constraints.insert(
169                (constraint.table_id.clone(), constraint.name.clone()),
170                constraint,
171            );
172        }
173
174        for t in cache.triggers {
175            triggers.insert(
176                t.trigger_id.clone(),
177                TriggerOverlay::Present(crate::model::trigger::TriggerState {
178                    id: t.trigger_id.clone(),
179                    table_id: t.table_id.clone(),
180                    enabled_mode: t.enabled_mode,
181                    generation: 0,
182                }),
183            );
184            graph.edges.push(DependencyEdge::new(
185                t.trigger_id.clone(),
186                t.table_id,
187                DependencyKind::TriggerOnTable {
188                    trigger_id: t.trigger_id,
189                    function_id: t.function_id,
190                },
191            ));
192        }
193
194        let mut functions: HashMap<ObjectId, crate::model::function::FunctionOverlay> =
195            HashMap::new();
196        for (id, func_state) in &cache.functions {
197            functions.insert(
198                id.clone(),
199                crate::model::function::FunctionOverlay::Present(func_state.clone()),
200            );
201        }
202
203        Self {
204            pg_version_num: cache.pg_version_num,
205            baseline_relations,
206            baseline_indexes,
207            baseline_foreign_keys,
208            baseline_fk_dependencies,
209            local: LocalState {
210                relations,
211                types,
212                functions,
213                sequences: HashMap::new(),
214                publications: HashMap::new(),
215                subscriptions: HashMap::new(),
216                roles: HashMap::new(),
217                triggers,
218                constraints,
219                graph,
220                search_path: default_search_path.clone(),
221                default_search_path,
222                current_role: "postgres".to_string(),
223                confidence: Confidence::Exact,
224                transactions: Vec::new(),
225                pending_validation: HashSet::new(),
226                generation_counter: 0,
227            },
228        }
229    }
230
231    pub fn get_relation(&self, id: &ObjectId) -> Option<&RelationOverlay> {
232        self.local.relations.get(id)
233    }
234
235    pub fn resolve_function_schema(
236        &self,
237        name: &crate::ast::identifiers::QualifiedName,
238        sig_str: &str,
239    ) -> String {
240        if let Some(schema) = &name.schema {
241            return schema.resolve();
242        }
243        for schema in &self.local.search_path {
244            let candidate = ObjectId::new(schema.clone(), sig_str.to_string());
245            if self.local.functions.contains_key(&candidate) {
246                return schema.clone();
247            }
248        }
249        self.local
250            .search_path
251            .first()
252            .cloned()
253            .unwrap_or_else(|| "public".to_string())
254    }
255
256    pub fn resolve_relation_id(&self, name: &crate::ast::identifiers::QualifiedName) -> ObjectId {
257        if let Some(schema) = &name.schema {
258            return ObjectId::new(schema.resolve(), name.name.resolve());
259        }
260        let resolved_name = name.name.resolve();
261        for schema in &self.local.search_path {
262            let mut candidate = ObjectId::new(schema.clone(), resolved_name.clone());
263            if self.local.relations.contains_key(&candidate) {
264                candidate.inferred_schema = true;
265                return candidate;
266            }
267        }
268        let schema = self
269            .local
270            .search_path
271            .first()
272            .cloned()
273            .unwrap_or_else(|| "public".to_string());
274        let mut id = ObjectId::new(schema, resolved_name);
275        id.inferred_schema = true;
276        id
277    }
278
279    pub fn relation_is_present(&self, id: &ObjectId) -> bool {
280        matches!(
281            self.local.relations.get(id),
282            Some(RelationOverlay::Present(_))
283        )
284    }
285
286    pub fn column_was_added_in_transaction(&self, table_id: &ObjectId, column: &str) -> bool {
287        if self.local.transactions.is_empty() {
288            return false;
289        }
290
291        // Search from the oldest transaction frame to the newest
292        for frame in &self.local.transactions {
293            for change in &frame.undo_log {
294                if let StateChange::RelationSnapshot { id, previous } = change
295                    && id == table_id
296                {
297                    match previous.as_ref() {
298                        None | Some(RelationOverlay::Dropped) => {
299                            return true;
300                        }
301                        Some(RelationOverlay::Present(r)) => {
302                            let col_existed = r.columns.iter().any(|c| c.name == column);
303                            return !col_existed;
304                        }
305                    }
306                }
307            }
308        }
309        false
310    }
311
312    pub fn capture_pre_state(&self) -> PreState {
313        let mut relations = HashMap::new();
314        for (id, overlay) in &self.local.relations {
315            if let RelationOverlay::Present(s) = overlay {
316                relations.insert(id.clone(), s.clone());
317            }
318        }
319
320        let mut functions = HashMap::new();
321        for (id, overlay) in &self.local.functions {
322            if let crate::model::function::FunctionOverlay::Present(s) = overlay {
323                functions.insert(id.clone(), s.clone());
324            }
325        }
326
327        let mut roles = HashMap::new();
328        for (name, overlay) in &self.local.roles {
329            if let crate::model::role::RoleOverlay::Present(s) = overlay {
330                roles.insert(name.clone(), s.clone());
331            }
332        }
333
334        let mut publications = HashMap::new();
335        for (name, overlay) in &self.local.publications {
336            if let crate::model::replication::PublicationOverlay::Present(s) = overlay {
337                publications.insert(name.clone(), s.clone());
338            }
339        }
340
341        let mut subscriptions = HashMap::new();
342        for (name, overlay) in &self.local.subscriptions {
343            if let crate::model::replication::SubscriptionOverlay::Present(s) = overlay {
344                subscriptions.insert(name.clone(), s.clone());
345            }
346        }
347
348        let mut sequences = HashMap::new();
349        for (id, overlay) in &self.local.sequences {
350            if let SequenceOverlay::Present(s) = overlay {
351                sequences.insert(id.clone(), s.clone());
352            }
353        }
354
355        let mut types = HashMap::new();
356        for (id, overlay) in &self.local.types {
357            if let TypeOverlay::Present(s) = overlay {
358                types.insert(id.clone(), s.clone());
359            }
360        }
361
362        let indexes = self
363            .local
364            .graph
365            .edges
366            .iter()
367            .filter(|e| matches!(e.kind, DependencyKind::IndexOnRelation { .. }))
368            .cloned()
369            .collect();
370
371        PreState {
372            relations,
373            functions,
374            roles,
375            publications,
376            subscriptions,
377            sequences,
378            types,
379            indexes,
380        }
381    }
382
383    pub fn get_cascade_closure(&self, target_oid: &ObjectId) -> CascadeResult {
384        let mut result = CascadeResult::default();
385        let mut visited = HashSet::new();
386        self.walk_cascade(target_oid, &mut visited, &mut result);
387        result
388    }
389
390    fn walk_cascade(
391        &self,
392        current: &ObjectId,
393        visited: &mut HashSet<ObjectId>,
394        result: &mut CascadeResult,
395    ) {
396        let resolved_current = self.local.graph.resolve_rename(current).clone();
397
398        if !visited.insert(resolved_current.clone()) {
399            return;
400        }
401
402        result.dropped_relations.insert(resolved_current.clone());
403
404        for edge in &self.local.graph.edges {
405            match &edge.kind {
406                DependencyKind::ViewDependency { .. } => {
407                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current {
408                        let resolved_view_id =
409                            self.local.graph.resolve_rename(&edge.dependent).clone();
410                        if !visited.contains(&resolved_view_id) {
411                            self.walk_cascade(&resolved_view_id, visited, result);
412                        }
413                    }
414                }
415                DependencyKind::IndexOnRelation { .. } => {
416                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current {
417                        result
418                            .dropped_indexes
419                            .insert(self.local.graph.resolve_rename(&edge.dependent).clone());
420                    }
421                }
422                DependencyKind::ForeignKey {
423                    constraint_name, ..
424                } => {
425                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current
426                        && let Some(cname) = constraint_name
427                    {
428                        result.dropped_constraints.insert((
429                            self.local.graph.resolve_rename(&edge.dependent).clone(),
430                            cname.clone(),
431                        ));
432                    }
433                }
434                DependencyKind::PartitionOf
435                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current =>
436                {
437                    let resolved_child = self.local.graph.resolve_rename(&edge.dependent).clone();
438                    if !visited.contains(&resolved_child) {
439                        self.walk_cascade(&resolved_child, visited, result);
440                    }
441                }
442                _ => {}
443            }
444        }
445    }
446
447    fn resolve_grant_privileges(
448        spec: &crate::analysis::facts::PrivilegeSpec,
449    ) -> HashSet<Privilege> {
450        match spec {
451            crate::analysis::facts::PrivilegeSpec::All => vec![
452                Privilege::Select,
453                Privilege::Insert,
454                Privilege::Update,
455                Privilege::Delete,
456                Privilege::Truncate,
457                Privilege::References,
458                Privilege::Trigger,
459            ]
460            .into_iter()
461            .collect(),
462            crate::analysis::facts::PrivilegeSpec::List(list) => list
463                .iter()
464                .filter_map(|p| match p {
465                    crate::analysis::facts::PrivilegeFact::Select => Some(Privilege::Select),
466                    crate::analysis::facts::PrivilegeFact::Insert => Some(Privilege::Insert),
467                    crate::analysis::facts::PrivilegeFact::Update => Some(Privilege::Update),
468                    crate::analysis::facts::PrivilegeFact::Delete => Some(Privilege::Delete),
469                    crate::analysis::facts::PrivilegeFact::Truncate => Some(Privilege::Truncate),
470                    crate::analysis::facts::PrivilegeFact::References => {
471                        Some(Privilege::References)
472                    }
473                    crate::analysis::facts::PrivilegeFact::Trigger => Some(Privilege::Trigger),
474                    _ => None,
475                })
476                .collect(),
477        }
478    }
479
480    fn resolve_role_name(
481        role: &crate::analysis::facts::RoleFact,
482        current_role: &str,
483    ) -> Option<ObjectId> {
484        let name = match role {
485            crate::analysis::facts::RoleFact::Named { name, .. } => Some(name.clone()),
486            crate::analysis::facts::RoleFact::CurrentUser
487            | crate::analysis::facts::RoleFact::CurrentRole => Some(current_role.to_string()),
488            crate::analysis::facts::RoleFact::SessionUser => Some("postgres".to_string()),
489            crate::analysis::facts::RoleFact::Unknown => None,
490        }?;
491        Some(ObjectId::new("", name))
492    }
493
494    fn apply_grant_to_relation(
495        &mut self,
496        id: &ObjectId,
497        privileges: &HashSet<Privilege>,
498        grantees: &[crate::analysis::facts::RoleFact],
499    ) {
500        self.snapshot_relation(id);
501        if let Some(RelationOverlay::Present(rel)) = self.local.relations.get_mut(id) {
502            for grantee in grantees {
503                if let Some(role_id) = Self::resolve_role_name(grantee, &self.local.current_role) {
504                    rel.privileges.grant(role_id, privileges.clone());
505                }
506            }
507        }
508    }
509
510    fn apply_revoke_to_relation(
511        &mut self,
512        id: &ObjectId,
513        privileges: &HashSet<Privilege>,
514        revokees: &[crate::analysis::facts::RoleFact],
515    ) {
516        self.snapshot_relation(id);
517        if let Some(RelationOverlay::Present(rel)) = self.local.relations.get_mut(id) {
518            for revokee in revokees {
519                if let Some(role_id) = Self::resolve_role_name(revokee, &self.local.current_role) {
520                    rel.privileges.revoke(&role_id, privileges);
521                }
522            }
523        }
524    }
525
526    pub fn apply(
527        &mut self,
528        mutation: &Mutation,
529        precomputed_cascade: Option<&CascadeResult>,
530    ) -> MutationResult {
531        match mutation {
532            Mutation::CreateSchema(_) => MutationResult::Applied,
533            Mutation::DropSchema(drop_schema) => {
534                if drop_schema.cascade {
535                    let mut relations_to_drop = Vec::new();
536                    for id in self.local.relations.keys() {
537                        if drop_schema.names.contains(&id.schema) {
538                            relations_to_drop.push(id.clone());
539                        }
540                    }
541                    for id in relations_to_drop {
542                        self.snapshot_relation(&id);
543                        self.local.relations.insert(id, RelationOverlay::Dropped);
544                    }
545
546                    let constraints_to_drop: Vec<(ObjectId, String)> = self
547                        .local
548                        .constraints
549                        .keys()
550                        .filter(|(table_id, _)| drop_schema.names.contains(&table_id.schema))
551                        .cloned()
552                        .collect();
553                    for (table_id, name) in constraints_to_drop {
554                        self.snapshot_constraint(&table_id, &name);
555                        self.local.constraints.remove(&(table_id, name));
556                    }
557
558                    let mut types_to_drop = Vec::new();
559                    for id in self.local.types.keys() {
560                        if drop_schema.names.contains(&id.schema) {
561                            types_to_drop.push(id.clone());
562                        }
563                    }
564                    for id in types_to_drop {
565                        self.snapshot_type(&id);
566                        self.local.types.insert(id, TypeOverlay::Dropped);
567                    }
568
569                    let mut seqs_to_drop = Vec::new();
570                    for id in self.local.sequences.keys() {
571                        if drop_schema.names.contains(&id.schema) {
572                            seqs_to_drop.push(id.clone());
573                        }
574                    }
575                    for id in seqs_to_drop {
576                        self.snapshot_sequence(&id);
577                        self.local.sequences.insert(id, SequenceOverlay::Dropped);
578                    }
579
580                    self.snapshot_graph_full();
581
582                    let g = &mut self.local.graph;
583                    g.edges.retain(|e| {
584                        !drop_schema.names.contains(&e.dependent.schema)
585                            && !drop_schema.names.contains(&e.referenced.schema)
586                            && match &e.kind {
587                                DependencyKind::TriggerOnTable { function_id, .. } => {
588                                    !drop_schema.names.contains(&function_id.schema)
589                                }
590                                _ => true,
591                            }
592                    });
593                } else {
594                    // Non-cascade: fail if any objects in the schema still exist
595                    let has_relation = self.local.relations.iter().any(|(id, ov)| {
596                        drop_schema.names.contains(&id.schema)
597                            && !matches!(ov, RelationOverlay::Dropped)
598                    });
599                    let has_type = self.local.types.iter().any(|(id, ov)| {
600                        drop_schema.names.contains(&id.schema)
601                            && !matches!(ov, TypeOverlay::Dropped)
602                    });
603                    let has_sequence = self.local.sequences.iter().any(|(id, ov)| {
604                        drop_schema.names.contains(&id.schema)
605                            && !matches!(ov, SequenceOverlay::Dropped)
606                    });
607                    let has_function = self.local.functions.iter().any(|(id, ov)| {
608                        drop_schema.names.contains(&id.schema)
609                            && !matches!(ov, crate::model::function::FunctionOverlay::Dropped)
610                    });
611                    let has_trigger = self.local.triggers.iter().any(|(id, ov)| {
612                        drop_schema.names.contains(&id.schema)
613                            && !matches!(ov, TriggerOverlay::Dropped)
614                    });
615                    if has_relation || has_type || has_sequence || has_function || has_trigger {
616                        return MutationResult::Conflict {
617                            reason: format!(
618                                "schema(s) {:?} still contain objects; use CASCADE to drop them",
619                                drop_schema.names
620                            ),
621                        };
622                    }
623                }
624                MutationResult::Applied
625            }
626            Mutation::DropTable(drop_table) => {
627                if !self.relation_is_present(&drop_table.id) {
628                    if drop_table.if_exists {
629                        return MutationResult::Skipped;
630                    } else {
631                        self.local.confidence = Confidence::Tainted;
632                        return MutationResult::Skipped;
633                    }
634                }
635
636                let constraints_to_drop: Vec<String> = self
637                    .local
638                    .constraints
639                    .keys()
640                    .filter(|(table_id, _)| table_id == &drop_table.id)
641                    .map(|(_, name)| name.clone())
642                    .collect();
643                for name in constraints_to_drop {
644                    self.snapshot_constraint(&drop_table.id, &name);
645                    self.local
646                        .constraints
647                        .remove(&(drop_table.id.clone(), name));
648                }
649
650                let triggers_to_drop: Vec<ObjectId> = self
651                    .local
652                    .triggers
653                    .iter()
654                    .filter_map(|(id, overlay)| {
655                        let TriggerOverlay::Present(trigger) = overlay else {
656                            return None;
657                        };
658                        let graph_matches = self.local.graph.edges.iter().any(|edge| {
659                            matches!(edge.kind, DependencyKind::TriggerOnTable { .. })
660                                && edge.dependent == *id
661                                && edge.referenced == drop_table.id
662                        });
663                        (trigger.table_id == drop_table.id || graph_matches).then(|| id.clone())
664                    })
665                    .collect();
666                for trigger_id in triggers_to_drop {
667                    self.snapshot_trigger(&trigger_id);
668                    self.local
669                        .triggers
670                        .insert(trigger_id, TriggerOverlay::Dropped);
671                }
672
673                // PostgreSQL always drops triggers owned by a dropped table.
674                self.snapshot_graph_full();
675                self.local.graph.edges.retain(|e| {
676                    !(matches!(e.kind, DependencyKind::TriggerOnTable { .. })
677                        && e.referenced == drop_table.id)
678                });
679
680                let renames: Vec<DependencyEdge> = self
681                    .local
682                    .graph
683                    .edges
684                    .iter()
685                    .filter(|e| matches!(e.kind, DependencyKind::RenameTo))
686                    .cloned()
687                    .collect();
688                let resolve = |id: &ObjectId| -> ObjectId {
689                    let mut current = id;
690                    loop {
691                        match renames.iter().find(|r| &r.dependent == current) {
692                            Some(edge) => current = &edge.referenced,
693                            None => return current.clone(),
694                        }
695                    }
696                };
697
698                let resolved_drop = resolve(&drop_table.id);
699
700                if drop_table.cascade {
701                    let local_closure;
702                    let closure = match precomputed_cascade {
703                        Some(c) => c,
704                        None => {
705                            local_closure = self.get_cascade_closure(&drop_table.id);
706                            &local_closure
707                        }
708                    };
709
710                    for dropped_rel_id in &closure.dropped_relations {
711                        self.snapshot_relation(dropped_rel_id);
712                        self.local
713                            .relations
714                            .insert(dropped_rel_id.clone(), RelationOverlay::Dropped);
715                    }
716
717                    self.snapshot_graph_full();
718                    self.local.graph.edges.retain(|e| match &e.kind {
719                        DependencyKind::IndexOnRelation { .. } => {
720                            !closure.dropped_indexes.contains(&resolve(&e.dependent))
721                        }
722                        DependencyKind::ForeignKey {
723                            constraint_name, ..
724                        } => {
725                            let from_dropped =
726                                closure.dropped_relations.contains(&resolve(&e.dependent));
727                            let to_dropped =
728                                closure.dropped_relations.contains(&resolve(&e.referenced));
729                            let constraint_explicitly_dropped = if let Some(cname) = constraint_name
730                            {
731                                closure
732                                    .dropped_constraints
733                                    .contains(&(resolve(&e.dependent), cname.clone()))
734                            } else {
735                                false
736                            };
737                            !(from_dropped || to_dropped || constraint_explicitly_dropped)
738                        }
739                        DependencyKind::ViewDependency { .. } => {
740                            !closure.dropped_relations.contains(&resolve(&e.dependent))
741                        }
742                        DependencyKind::SequenceOwnedBy { .. } => {
743                            !closure.dropped_relations.contains(&resolve(&e.referenced))
744                        }
745                        _ => true,
746                    });
747                } else {
748                    let has_view_deps = self.local.graph.edges.iter().any(|e| {
749                        matches!(e.kind, DependencyKind::ViewDependency { .. })
750                            && resolve(&e.referenced) == resolved_drop
751                    });
752                    let has_fk_deps = self.local.graph.edges.iter().any(|e| {
753                        matches!(e.kind, DependencyKind::ForeignKey { .. })
754                            && resolve(&e.referenced) == resolved_drop
755                            && resolve(&e.dependent) != resolved_drop
756                    });
757                    let has_partition_deps = self.local.graph.edges.iter().any(|e| {
758                        matches!(e.kind, DependencyKind::PartitionOf)
759                            && resolve(&e.referenced) == resolved_drop
760                    });
761
762                    if has_view_deps || has_fk_deps || has_partition_deps {
763                        self.local.confidence = Confidence::Tainted;
764                        return MutationResult::Skipped;
765                    }
766
767                    self.snapshot_relation(&drop_table.id);
768                    self.local
769                        .relations
770                        .insert(drop_table.id.clone(), RelationOverlay::Dropped);
771
772                    self.snapshot_graph_full();
773                    self.local.graph.edges.retain(|e| {
774                        !(matches!(e.kind, DependencyKind::SequenceOwnedBy { .. })
775                            && resolve(&e.referenced) == resolved_drop)
776                    });
777                }
778
779                self.snapshot_graph_full();
780                self.local.graph.edges.retain(|e| {
781                    if let DependencyKind::PartitionOf = e.kind {
782                        resolve(&e.referenced) != resolved_drop
783                            && resolve(&e.dependent) != resolved_drop
784                    } else {
785                        true
786                    }
787                });
788
789                MutationResult::Applied
790            }
791            Mutation::CreateTable(create) => {
792                if create.if_not_exists && self.relation_is_present(&create.id) {
793                    return MutationResult::Skipped;
794                }
795                if !create.if_not_exists && self.relation_is_present(&create.id) {
796                    return MutationResult::Conflict {
797                        reason: format!("relation '{}' already exists", create.id),
798                    };
799                }
800
801                self.snapshot_relation(&create.id);
802
803                self.snapshot_generation_counter();
804                self.local.generation_counter += 1;
805                let generation = self.local.generation_counter;
806
807                let resolved_persistence = match create.persistence {
808                    PersistenceMutation::Permanent => {
809                        crate::model::relation::Persistence::Permanent
810                    }
811                    PersistenceMutation::Temporary => {
812                        crate::model::relation::Persistence::Temporary
813                    }
814                    PersistenceMutation::Unlogged => crate::model::relation::Persistence::Unlogged,
815                };
816
817                let mut rel_state = RelationState::new(
818                    create.id.clone(),
819                    ObjectId::new("public", &self.local.current_role),
820                    generation,
821                    if create.as_select { None } else { Some(0) },
822                    RelationKind::Table,
823                    resolved_persistence,
824                    self.local.transactions.len(),
825                );
826
827                // Store partition strategy information
828                rel_state.partition_type = create
829                    .partition_by
830                    .as_ref()
831                    .and_then(|pb| pb.split_whitespace().nth(2).map(|s| s.to_uppercase()))
832                    .or_else(|| {
833                        create.partition_of.as_ref().and_then(|parent_id| {
834                            self.local.relations.get(parent_id).and_then(|r| {
835                                if let RelationOverlay::Present(rel) = r {
836                                    rel.partition_type.clone()
837                                } else {
838                                    None
839                                }
840                            })
841                        })
842                    });
843                rel_state.partition_by = create.partition_by.clone();
844
845                let pk_columns: HashSet<&str> = create
846                    .table_constraints
847                    .iter()
848                    .filter_map(|tc| {
849                        if let TableConstraintFact::PrimaryKey { columns } = tc {
850                            Some(columns.iter().map(|s| s.as_str()))
851                        } else {
852                            None
853                        }
854                    })
855                    .flatten()
856                    .collect();
857
858                for col in &create.columns {
859                    let is_pk = col.is_primary_key || pk_columns.contains(col.name.as_str());
860                    rel_state.apply_column_action(&ColumnAction::Add {
861                        name: col.name.clone(),
862                        data_type: col.ty.clone(),
863                        not_null: col.not_null || is_pk,
864                        default: col.default.clone(),
865                    });
866                }
867
868                self.local
869                    .relations
870                    .insert(create.id.clone(), RelationOverlay::Present(rel_state));
871
872                if let Some(parent_id) = &create.partition_of {
873                    self.snapshot_graph();
874                    self.local.graph.edges.push(DependencyEdge::new(
875                        create.id.clone(),
876                        parent_id.clone(),
877                        DependencyKind::PartitionOf,
878                    ));
879                }
880
881                if !create.foreign_keys.is_empty() {
882                    self.snapshot_graph();
883                }
884
885                for fk in &create.foreign_keys {
886                    self.local.graph.edges.push(DependencyEdge::new(
887                        create.id.clone(),
888                        fk.to_table.clone(),
889                        DependencyKind::ForeignKey {
890                            constraint_name: fk.constraint_name.clone(),
891                            from_columns: fk.from_columns.clone(),
892                            to_columns: fk.to_columns.clone(),
893                            from_generation: generation,
894                        },
895                    ));
896                }
897                MutationResult::Applied
898            }
899            Mutation::CreateView(create_view) => {
900                if !create_view.or_replace && self.relation_is_present(&create_view.id) {
901                    return MutationResult::Conflict {
902                        reason: format!("relation '{}' already exists", create_view.id),
903                    };
904                }
905                self.snapshot_relation(&create_view.id);
906                self.snapshot_generation_counter();
907                self.local.generation_counter += 1;
908                let generation = self.local.generation_counter;
909
910                self.local.relations.insert(
911                    create_view.id.clone(),
912                    RelationOverlay::Present(RelationState::new(
913                        create_view.id.clone(),
914                        ObjectId::new("public", &self.local.current_role),
915                        generation,
916                        None,
917                        RelationKind::View,
918                        Persistence::Permanent,
919                        self.local.transactions.len(),
920                    )),
921                );
922
923                self.snapshot_graph();
924                for dep in &create_view.depends_on {
925                    self.local.graph.edges.push(DependencyEdge::new(
926                        create_view.id.clone(),
927                        dep.clone(),
928                        DependencyKind::ViewDependency {
929                            view_generation: generation,
930                        },
931                    ));
932                }
933                MutationResult::Applied
934            }
935            Mutation::CreateMaterializedView(create_mv) => {
936                if self.relation_is_present(&create_mv.id) {
937                    return MutationResult::Conflict {
938                        reason: format!("relation '{}' already exists", create_mv.id),
939                    };
940                }
941                self.snapshot_relation(&create_mv.id);
942                self.snapshot_generation_counter();
943                self.local.generation_counter += 1;
944                let generation = self.local.generation_counter;
945
946                self.local.relations.insert(
947                    create_mv.id.clone(),
948                    RelationOverlay::Present(RelationState::new(
949                        create_mv.id.clone(),
950                        ObjectId::new("public", &self.local.current_role),
951                        generation,
952                        None,
953                        RelationKind::MaterializedView,
954                        Persistence::Permanent,
955                        self.local.transactions.len(),
956                    )),
957                );
958
959                self.snapshot_graph();
960                for dep in &create_mv.depends_on {
961                    self.local.graph.edges.push(DependencyEdge::new(
962                        create_mv.id.clone(),
963                        dep.clone(),
964                        DependencyKind::ViewDependency {
965                            view_generation: generation,
966                        },
967                    ));
968                }
969                MutationResult::Applied
970            }
971            Mutation::RefreshMaterializedView(_) => MutationResult::Applied,
972            Mutation::CreateIndex(create_idx) => {
973                let exists = self.local.graph.edges.iter().any(|e| {
974                    matches!(e.kind, DependencyKind::IndexOnRelation { .. })
975                        && e.dependent == create_idx.id
976                });
977                if create_idx.if_not_exists && exists {
978                    return MutationResult::Skipped;
979                }
980                if !create_idx.if_not_exists && exists {
981                    return MutationResult::Conflict {
982                        reason: format!("relation '{}' already exists", create_idx.id),
983                    };
984                }
985                self.snapshot_graph();
986                self.local.graph.edges.push(DependencyEdge::new(
987                    create_idx.id.clone(),
988                    create_idx.table.clone(),
989                    DependencyKind::IndexOnRelation {
990                        using_method: create_idx.using_method.clone(),
991                        has_predicate: create_idx.has_predicate,
992                        is_concurrent: create_idx.concurrently,
993                        is_unique: create_idx.unique,
994                    },
995                ));
996                MutationResult::Applied
997            }
998            Mutation::CreatePolicy(create_policy) => {
999                self.snapshot_relation(&create_policy.table);
1000                if let Some(RelationOverlay::Present(rel)) =
1001                    self.local.relations.get_mut(&create_policy.table)
1002                {
1003                    rel.policies.insert(create_policy.name.clone());
1004                }
1005                MutationResult::Applied
1006            }
1007            Mutation::DropPolicy(drop_policy) => {
1008                self.snapshot_relation(&drop_policy.table);
1009                if let Some(RelationOverlay::Present(rel)) =
1010                    self.local.relations.get_mut(&drop_policy.table)
1011                {
1012                    rel.policies.remove(&drop_policy.name);
1013                }
1014                MutationResult::Applied
1015            }
1016            Mutation::CreateTrigger(create_trigger) => {
1017                let trigger_id = ObjectId::new(
1018                    create_trigger.table.schema.clone(),
1019                    create_trigger.name.clone(),
1020                );
1021                self.snapshot_trigger(&trigger_id);
1022                self.local.triggers.insert(
1023                    trigger_id.clone(),
1024                    TriggerOverlay::Present(crate::model::trigger::TriggerState {
1025                        id: trigger_id.clone(),
1026                        table_id: create_trigger.table.clone(),
1027                        enabled_mode: crate::model::trigger::TriggerEnableMode::Origin,
1028                        generation: self.local.generation_counter,
1029                    }),
1030                );
1031
1032                self.snapshot_relation(&create_trigger.table);
1033                if let Some(RelationOverlay::Present(rel)) =
1034                    self.local.relations.get_mut(&create_trigger.table)
1035                {
1036                    rel.triggers.insert(create_trigger.name.clone());
1037                }
1038
1039                self.snapshot_graph_full();
1040                self.local.graph.edges.push(DependencyEdge::new(
1041                    trigger_id.clone(),
1042                    create_trigger.table.clone(),
1043                    DependencyKind::TriggerOnTable {
1044                        trigger_id: trigger_id.clone(),
1045                        function_id: create_trigger.function_id.clone(),
1046                    },
1047                ));
1048
1049                MutationResult::Applied
1050            }
1051            Mutation::DropTrigger(drop_trigger) => {
1052                let trigger_id =
1053                    ObjectId::new(drop_trigger.table.schema.clone(), drop_trigger.name.clone());
1054                self.snapshot_trigger(&trigger_id);
1055                self.local
1056                    .triggers
1057                    .insert(trigger_id.clone(), TriggerOverlay::Dropped);
1058
1059                self.snapshot_relation(&drop_trigger.table);
1060                if let Some(RelationOverlay::Present(rel)) =
1061                    self.local.relations.get_mut(&drop_trigger.table)
1062                {
1063                    rel.triggers.remove(&drop_trigger.name);
1064                }
1065
1066                self.snapshot_graph_full();
1067                self.local.graph.edges.retain(|e| {
1068                    !(matches!(e.kind, DependencyKind::TriggerOnTable { .. })
1069                        && e.dependent == trigger_id)
1070                });
1071
1072                MutationResult::Applied
1073            }
1074            Mutation::AlterTable(alter) => {
1075                let trigger_mode = match &alter.action {
1076                    AlterTableActionMutation::DisableTrigger { trigger_name } => Some((
1077                        trigger_name.as_deref(),
1078                        crate::model::trigger::TriggerEnableMode::Disabled,
1079                    )),
1080                    AlterTableActionMutation::EnableTrigger { trigger_name } => Some((
1081                        trigger_name.as_deref(),
1082                        crate::model::trigger::TriggerEnableMode::Origin,
1083                    )),
1084                    _ => None,
1085                };
1086                if let Some((trigger_name, enabled_mode)) = trigger_mode {
1087                    let all = trigger_name.is_none_or(|name| name.eq_ignore_ascii_case("all"));
1088                    let trigger_ids: Vec<ObjectId> = self
1089                        .local
1090                        .triggers
1091                        .iter()
1092                        .filter_map(|(id, overlay)| {
1093                            let TriggerOverlay::Present(trigger) = overlay else {
1094                                return None;
1095                            };
1096                            (trigger.table_id == alter.id
1097                                && (all || trigger_name == Some(id.name.as_str())))
1098                            .then(|| id.clone())
1099                        })
1100                        .collect();
1101                    for trigger_id in trigger_ids {
1102                        self.snapshot_trigger(&trigger_id);
1103                        if let Some(TriggerOverlay::Present(trigger)) =
1104                            self.local.triggers.get_mut(&trigger_id)
1105                        {
1106                            trigger.enabled_mode = enabled_mode;
1107                        }
1108                    }
1109                    return MutationResult::Applied;
1110                }
1111
1112                self.snapshot_relation(&alter.id);
1113                let rel_overlay = self.local.relations.get_mut(&alter.id);
1114                if let Some(RelationOverlay::Present(rel)) = rel_overlay {
1115                    let generation = rel.generation;
1116                    match &alter.action {
1117                        AlterTableActionMutation::AddColumn {
1118                            name,
1119                            ty,
1120                            if_not_exists,
1121                            not_null,
1122                            default,
1123                            depends_on,
1124                        } => {
1125                            if !(*if_not_exists && rel.has_column(name)) {
1126                                if let Some(existing_col) =
1127                                    rel.columns.iter().find(|c| c.name == *name)
1128                                    && existing_col.data_type.as_deref() != ty.as_deref()
1129                                {
1130                                    return MutationResult::Conflict {
1131                                        reason: format!(
1132                                            "column '{}' already added with type {} (likely an earlier file in this chain), this file adds it again with type {}",
1133                                            name,
1134                                            existing_col.data_type.as_deref().unwrap_or("unknown"),
1135                                            ty.as_deref().unwrap_or("unknown")
1136                                        ),
1137                                    };
1138                                }
1139                                rel.apply_column_action(&ColumnAction::Add {
1140                                    name: name.clone(),
1141                                    data_type: ty.clone(),
1142                                    not_null: *not_null,
1143                                    default: default.clone(),
1144                                });
1145
1146                                if let Some((source_table, source_col)) = depends_on {
1147                                    self.snapshot_graph();
1148                                    self.local.graph.edges.push(DependencyEdge::new(
1149                                        alter.id.clone(),
1150                                        source_table.clone(),
1151                                        DependencyKind::ColumnGeneratedFrom {
1152                                            column: name.clone(),
1153                                            depends_on_column: source_col.clone(),
1154                                        },
1155                                    ));
1156                                }
1157                            }
1158                        }
1159                        AlterTableActionMutation::DropColumn { name, if_exists } => {
1160                            if !rel.has_column(name) {
1161                                if *if_exists {
1162                                    // Column doesn't exist and IF EXISTS was specified: no-op
1163                                    return MutationResult::Skipped;
1164                                }
1165                                // Column doesn't exist and IF EXISTS not specified: PG runtime error
1166                                self.local.confidence = Confidence::Tainted;
1167                                return MutationResult::Skipped;
1168                            }
1169                            rel.apply_column_action(&ColumnAction::Drop { name: name.clone() });
1170                        }
1171                        AlterTableActionMutation::RenameColumn { from, to } => {
1172                            rel.apply_column_action(&ColumnAction::Rename {
1173                                from: from.clone(),
1174                                to: to.clone(),
1175                            });
1176                        }
1177                        AlterTableActionMutation::SetNotNull { column } => {
1178                            rel.apply_column_action(&ColumnAction::SetNotNull {
1179                                name: column.clone(),
1180                            });
1181                        }
1182                        AlterTableActionMutation::DropNotNull { column } => {
1183                            rel.apply_column_action(&ColumnAction::DropNotNull {
1184                                name: column.clone(),
1185                            });
1186                        }
1187                        AlterTableActionMutation::SetType { column, ty, .. } => {
1188                            if !rel.has_column(column) {
1189                                self.local.confidence = Confidence::Tainted;
1190                            }
1191                            rel.apply_column_action(&ColumnAction::SetType {
1192                                name: column.clone(),
1193                                data_type: ty.clone(),
1194                            });
1195                        }
1196                        AlterTableActionMutation::SetDefault { column, default } => {
1197                            if !rel.has_column(column) {
1198                                self.local.confidence = Confidence::Tainted;
1199                            }
1200                            rel.apply_column_action(&ColumnAction::SetDefault {
1201                                name: column.clone(),
1202                                default: default.clone(),
1203                            });
1204                        }
1205                        AlterTableActionMutation::AddForeignKey {
1206                            constraint_name,
1207                            to_table,
1208                            from_columns,
1209                            to_columns,
1210                            ..
1211                        } => {
1212                            let constraint_name = constraint_name.clone().unwrap_or_else(|| {
1213                                format!("{}_{}_fkey", alter.id.name, from_columns.join("_"))
1214                            });
1215                            self.snapshot_constraint(&alter.id, &constraint_name);
1216                            self.local.constraints.insert(
1217                                (alter.id.clone(), constraint_name.clone()),
1218                                ConstraintState {
1219                                    table_id: alter.id.clone(),
1220                                    name: constraint_name.clone(),
1221                                    kind: ConstraintKind::ForeignKey,
1222                                    validated: true,
1223                                },
1224                            );
1225                            self.snapshot_graph();
1226                            self.local.graph.edges.push(DependencyEdge::new(
1227                                alter.id.clone(),
1228                                to_table.clone(),
1229                                DependencyKind::ForeignKey {
1230                                    constraint_name: Some(constraint_name),
1231                                    from_columns: from_columns.clone(),
1232                                    to_columns: to_columns.clone(),
1233                                    from_generation: generation,
1234                                },
1235                            ));
1236                        }
1237                        AlterTableActionMutation::DropConstraint { name } => {
1238                            self.snapshot_constraint(&alter.id, name);
1239                            self.local
1240                                .constraints
1241                                .remove(&(alter.id.clone(), name.clone()));
1242                            self.snapshot_graph();
1243                            self.local.graph.edges.retain(|e| {
1244                                if let DependencyKind::ForeignKey {
1245                                    constraint_name, ..
1246                                } = &e.kind
1247                                {
1248                                    !(e.dependent == alter.id
1249                                        && constraint_name.as_ref() == Some(name))
1250                                } else {
1251                                    true
1252                                }
1253                            });
1254                        }
1255                        AlterTableActionMutation::RenameConstraint { old_name, new_name } => {
1256                            self.snapshot_constraint(&alter.id, old_name);
1257                            self.snapshot_constraint(&alter.id, new_name);
1258                            if let Some(mut constraint) = self
1259                                .local
1260                                .constraints
1261                                .remove(&(alter.id.clone(), old_name.clone()))
1262                            {
1263                                constraint.name = new_name.clone();
1264                                self.local
1265                                    .constraints
1266                                    .insert((alter.id.clone(), new_name.clone()), constraint);
1267                            }
1268                            self.snapshot_graph_full();
1269                            for edge in &mut self.local.graph.edges {
1270                                if edge.dependent == alter.id
1271                                    && let DependencyKind::ForeignKey {
1272                                        constraint_name, ..
1273                                    } = &mut edge.kind
1274                                    && constraint_name.as_deref() == Some(old_name)
1275                                {
1276                                    *constraint_name = Some(new_name.clone());
1277                                }
1278                            }
1279                        }
1280                        AlterTableActionMutation::AddCheckConstraint {
1281                            constraint_name,
1282                            not_valid,
1283                        } => {
1284                            let constraint_name = constraint_name
1285                                .clone()
1286                                .unwrap_or_else(|| format!("{}_check", alter.id.name));
1287                            self.snapshot_constraint(&alter.id, &constraint_name);
1288                            self.local.constraints.insert(
1289                                (alter.id.clone(), constraint_name.clone()),
1290                                ConstraintState {
1291                                    table_id: alter.id.clone(),
1292                                    name: constraint_name,
1293                                    kind: ConstraintKind::Check,
1294                                    validated: !not_valid,
1295                                },
1296                            );
1297                        }
1298                        AlterTableActionMutation::AddUniqueConstraint { constraint_name } => {
1299                            let constraint_name = constraint_name
1300                                .clone()
1301                                .unwrap_or_else(|| format!("{}_key", alter.id.name));
1302                            self.snapshot_constraint(&alter.id, &constraint_name);
1303                            self.local.constraints.insert(
1304                                (alter.id.clone(), constraint_name.clone()),
1305                                ConstraintState {
1306                                    table_id: alter.id.clone(),
1307                                    name: constraint_name,
1308                                    kind: ConstraintKind::Unique,
1309                                    validated: true,
1310                                },
1311                            );
1312                        }
1313                        AlterTableActionMutation::ValidateConstraint { constraint_name } => {
1314                            self.snapshot_constraint(&alter.id, constraint_name);
1315                            if let Some(constraint) = self
1316                                .local
1317                                .constraints
1318                                .get_mut(&(alter.id.clone(), constraint_name.clone()))
1319                            {
1320                                constraint.validated = true;
1321                            }
1322                        }
1323                        AlterTableActionMutation::AttachPartition { child } => {
1324                            // BUG-012: Reject cycle topologies before inserting the edge.
1325                            if self.local.graph.check_partition_cycle(&alter.id, child) {
1326                                self.snapshot_confidence();
1327                                self.local.confidence = Confidence::Tainted;
1328                            } else {
1329                                self.snapshot_graph();
1330                                self.local.graph.edges.push(DependencyEdge::new(
1331                                    child.clone(),
1332                                    alter.id.clone(),
1333                                    DependencyKind::PartitionOf,
1334                                ));
1335                            }
1336                        }
1337                        AlterTableActionMutation::DetachPartition { child } => {
1338                            self.snapshot_graph();
1339                            self.local.graph.edges.retain(|e| {
1340                                !(matches!(e.kind, DependencyKind::PartitionOf)
1341                                    && e.dependent == *child
1342                                    && e.referenced == alter.id)
1343                            });
1344                        }
1345                        _ => {}
1346                    }
1347                }
1348                MutationResult::Applied
1349            }
1350            Mutation::CreateType(create_type) => {
1351                if matches!(
1352                    self.local.types.get(&create_type.id),
1353                    Some(TypeOverlay::Present(_))
1354                ) {
1355                    return MutationResult::Conflict {
1356                        reason: format!("type '{}' already exists", create_type.id),
1357                    };
1358                }
1359                self.snapshot_type(&create_type.id);
1360                self.snapshot_generation_counter();
1361                self.local.generation_counter += 1;
1362                let generation = self.local.generation_counter;
1363
1364                self.local.types.insert(
1365                    create_type.id.clone(),
1366                    TypeOverlay::Present(TypeState {
1367                        id: create_type.id.clone(),
1368                        generation,
1369                        kind: create_type.kind.clone(),
1370                    }),
1371                );
1372                MutationResult::Applied
1373            }
1374            Mutation::AlterType(alter_type) => {
1375                self.snapshot_type(&alter_type.id);
1376                if let Some(TypeOverlay::Present(t)) = self.local.types.get_mut(&alter_type.id) {
1377                    match &alter_type.action {
1378                        AlterTypeActionMutation::AddValue {
1379                            new_value,
1380                            neighbor,
1381                            before,
1382                        } => {
1383                            if let TypeKind::Enum { variants } = &mut t.kind {
1384                                if variants.contains(new_value) {
1385                                    return MutationResult::Skipped;
1386                                }
1387                                let insertion_index = neighbor
1388                                    .as_ref()
1389                                    .and_then(|neighbor| {
1390                                        variants.iter().position(|value| value == neighbor)
1391                                    })
1392                                    .map(|index| if *before { index } else { index + 1 })
1393                                    .unwrap_or(variants.len());
1394                                variants.insert(insertion_index, new_value.clone());
1395                            }
1396                        }
1397                    }
1398                }
1399                MutationResult::Applied
1400            }
1401            Mutation::CreateDomain(create_domain) => {
1402                if matches!(
1403                    self.local.types.get(&create_domain.id),
1404                    Some(TypeOverlay::Present(_))
1405                ) {
1406                    return MutationResult::Conflict {
1407                        reason: format!("type '{}' already exists", create_domain.id),
1408                    };
1409                }
1410                self.snapshot_type(&create_domain.id);
1411                self.snapshot_generation_counter();
1412                self.local.generation_counter += 1;
1413                let generation = self.local.generation_counter;
1414
1415                self.local.types.insert(
1416                    create_domain.id.clone(),
1417                    TypeOverlay::Present(TypeState {
1418                        id: create_domain.id.clone(),
1419                        generation,
1420                        kind: TypeKind::Domain {
1421                            base_type: create_domain.base_type.clone(),
1422                        },
1423                    }),
1424                );
1425                MutationResult::Applied
1426            }
1427            Mutation::AlterDomain(_) => MutationResult::Applied,
1428            Mutation::DropDomain(drop_domain) => {
1429                for id in &drop_domain.ids {
1430                    self.snapshot_type(id);
1431                    self.local.types.insert(id.clone(), TypeOverlay::Dropped);
1432                }
1433                MutationResult::Applied
1434            }
1435            Mutation::DropType(drop_type) => {
1436                for id in &drop_type.ids {
1437                    self.snapshot_type(id);
1438                    self.local.types.insert(id.clone(), TypeOverlay::Dropped);
1439                }
1440                MutationResult::Applied
1441            }
1442            Mutation::CreateSequence(create_seq) => {
1443                if create_seq.if_not_exists && self.local.sequences.contains_key(&create_seq.id) {
1444                    return MutationResult::Skipped;
1445                }
1446                if !create_seq.if_not_exists && self.local.sequences.contains_key(&create_seq.id) {
1447                    return MutationResult::Conflict {
1448                        reason: format!("relation '{}' already exists", create_seq.id),
1449                    };
1450                }
1451                self.snapshot_sequence(&create_seq.id);
1452                self.snapshot_generation_counter();
1453                self.local.generation_counter += 1;
1454                let generation = self.local.generation_counter;
1455
1456                self.local.sequences.insert(
1457                    create_seq.id.clone(),
1458                    SequenceOverlay::Present(SequenceState {
1459                        id: create_seq.id.clone(),
1460                        generation,
1461                    }),
1462                );
1463
1464                if let Some((table_id, col)) = &create_seq.owned_by {
1465                    self.snapshot_graph();
1466                    self.local.graph.edges.push(DependencyEdge::new(
1467                        create_seq.id.clone(),
1468                        table_id.clone(),
1469                        DependencyKind::SequenceOwnedBy {
1470                            column: col.clone(),
1471                        },
1472                    ));
1473                }
1474                MutationResult::Applied
1475            }
1476            Mutation::AlterSequence(alter_seq) => {
1477                self.snapshot_sequence(&alter_seq.id);
1478                self.snapshot_graph();
1479                self.local.graph.edges.retain(|e| {
1480                    !(matches!(e.kind, DependencyKind::SequenceOwnedBy { .. })
1481                        && e.dependent == alter_seq.id)
1482                });
1483                if let Some((table_id, col)) = &alter_seq.owned_by {
1484                    self.local.graph.edges.push(DependencyEdge::new(
1485                        alter_seq.id.clone(),
1486                        table_id.clone(),
1487                        DependencyKind::SequenceOwnedBy {
1488                            column: col.clone(),
1489                        },
1490                    ));
1491                }
1492                MutationResult::Applied
1493            }
1494            Mutation::DropSequence(drop_seq) => {
1495                for id in &drop_seq.ids {
1496                    self.snapshot_sequence(id);
1497                    self.local
1498                        .sequences
1499                        .insert(id.clone(), SequenceOverlay::Dropped);
1500                }
1501                self.snapshot_graph_full();
1502                self.local.graph.edges.retain(|e| {
1503                    !(matches!(e.kind, DependencyKind::SequenceOwnedBy { .. })
1504                        && drop_seq.ids.contains(&e.dependent))
1505                });
1506                MutationResult::Applied
1507            }
1508            Mutation::Rename(rename) => {
1509                self.snapshot_relation(&rename.old_id);
1510                self.snapshot_relation(&rename.new_id);
1511                if let Some(RelationOverlay::Present(mut state)) =
1512                    self.local.relations.remove(&rename.old_id)
1513                {
1514                    state.id = rename.new_id.clone();
1515                    self.local
1516                        .relations
1517                        .insert(rename.new_id.clone(), RelationOverlay::Present(state));
1518                }
1519                let constraints_to_move: Vec<(String, ConstraintState)> = self
1520                    .local
1521                    .constraints
1522                    .iter()
1523                    .filter(|((table_id, _), _)| table_id == &rename.old_id)
1524                    .map(|((_, name), constraint)| (name.clone(), constraint.clone()))
1525                    .collect();
1526                for (name, mut constraint) in constraints_to_move {
1527                    self.snapshot_constraint(&rename.old_id, &name);
1528                    self.snapshot_constraint(&rename.new_id, &name);
1529                    self.local
1530                        .constraints
1531                        .remove(&(rename.old_id.clone(), name.clone()));
1532                    constraint.table_id = rename.new_id.clone();
1533                    self.local
1534                        .constraints
1535                        .insert((rename.new_id.clone(), name), constraint);
1536                }
1537                self.snapshot_graph();
1538                self.local.graph.edges.push(DependencyEdge::new(
1539                    rename.old_id.clone(),
1540                    rename.new_id.clone(),
1541                    DependencyKind::RenameTo,
1542                ));
1543
1544                // Snapshot all 8 affected graph edge lists before calling propagate_rename
1545                self.snapshot_graph_full();
1546
1547                self.local
1548                    .graph
1549                    .propagate_rename(&rename.old_id, &rename.new_id);
1550
1551                MutationResult::Applied
1552            }
1553            Mutation::DropView(drop_view) => {
1554                for id in &drop_view.ids {
1555                    self.snapshot_relation(id);
1556                    self.local
1557                        .relations
1558                        .insert(id.clone(), RelationOverlay::Dropped);
1559                }
1560                self.snapshot_graph_full();
1561                self.local.graph.edges.retain(|e| {
1562                    !(matches!(e.kind, DependencyKind::ViewDependency { .. })
1563                        && drop_view.ids.contains(&e.dependent))
1564                });
1565                MutationResult::Applied
1566            }
1567            Mutation::DropMaterializedView(drop_mv) => {
1568                for id in &drop_mv.ids {
1569                    self.snapshot_relation(id);
1570                    self.local
1571                        .relations
1572                        .insert(id.clone(), RelationOverlay::Dropped);
1573                }
1574                self.snapshot_graph_full();
1575                self.local.graph.edges.retain(|e| {
1576                    !((matches!(e.kind, DependencyKind::ViewDependency { .. })
1577                        && drop_mv.ids.contains(&e.dependent))
1578                        || (matches!(e.kind, DependencyKind::IndexOnRelation { .. })
1579                            && drop_mv.ids.contains(&e.referenced)))
1580                });
1581                MutationResult::Applied
1582            }
1583            Mutation::DropIndex(drop_idx) => {
1584                self.snapshot_graph();
1585                self.local.graph.edges.retain(|e| {
1586                    !(matches!(e.kind, DependencyKind::IndexOnRelation { .. })
1587                        && e.dependent == drop_idx.id)
1588                });
1589                MutationResult::Applied
1590            }
1591            Mutation::SearchPath(sp) => {
1592                self.snapshot_search_path();
1593                match &sp.target {
1594                    SearchPathTarget::Default => {
1595                        self.local.search_path = self.local.default_search_path.clone();
1596                    }
1597                    SearchPathTarget::Schemas(schemas) => {
1598                        self.local.search_path = schemas.clone();
1599                    }
1600                }
1601                MutationResult::Applied
1602            }
1603            Mutation::BeginTransaction => {
1604                self.local
1605                    .transactions
1606                    .push(TransactionFrame::new("transaction"));
1607                MutationResult::Applied
1608            }
1609            Mutation::CommitTransaction => {
1610                while self.local.transactions.pop().is_some() {}
1611                MutationResult::Applied
1612            }
1613            Mutation::RollbackTransaction => {
1614                while let Some(frame) = self.local.transactions.pop() {
1615                    self.rollback_frame(frame);
1616                }
1617                MutationResult::Applied
1618            }
1619            Mutation::RollbackToSavepoint(rts) => {
1620                let mut rolled_back = Vec::new();
1621                while let Some(frame) = self.local.transactions.last() {
1622                    if frame.name == rts.name {
1623                        break;
1624                    }
1625                    rolled_back.push(self.local.transactions.pop().unwrap());
1626                }
1627                if let Some(frame) = self.local.transactions.last_mut() {
1628                    let mut temp_frame = TransactionFrame::new(&frame.name);
1629                    while let Some(change) = frame.undo_log.pop() {
1630                        temp_frame.undo_log.push(change);
1631                    }
1632                    self.rollback_frame(temp_frame);
1633                }
1634                for frame in rolled_back.into_iter().rev() {
1635                    self.rollback_frame(frame);
1636                }
1637                MutationResult::Applied
1638            }
1639            Mutation::Savepoint(sp) => {
1640                self.local
1641                    .transactions
1642                    .push(TransactionFrame::new(sp.name.clone()));
1643                MutationResult::Applied
1644            }
1645            Mutation::ReleaseSavepoint(rsp) => {
1646                let mut rolled_back = Vec::new();
1647                while let Some(frame) = self.local.transactions.last() {
1648                    if frame.name == rsp.name {
1649                        break;
1650                    }
1651                    rolled_back.push(self.local.transactions.pop().unwrap());
1652                }
1653                if let Some(frame) = self.local.transactions.pop()
1654                    && let Some(outer) = self.local.transactions.last_mut()
1655                {
1656                    outer.undo_log.extend(frame.undo_log);
1657                }
1658                for frame in rolled_back.into_iter().rev() {
1659                    self.local.transactions.push(frame);
1660                }
1661                MutationResult::Applied
1662            }
1663            Mutation::Opaque(_) => {
1664                self.snapshot_confidence();
1665                self.local.confidence = Confidence::Tainted;
1666                MutationResult::Applied
1667            }
1668            Mutation::CreateFunction(f) => {
1669                self.snapshot_function(&f.id);
1670                self.snapshot_generation_counter();
1671                self.local.generation_counter += 1;
1672                let _generation = self.local.generation_counter;
1673
1674                let volatility = f
1675                    .options
1676                    .iter()
1677                    .find_map(|opt| {
1678                        if let crate::analysis::facts::FuncOptionFact::Volatility(v) = opt {
1679                            Some(match v {
1680                                crate::analysis::facts::VolatilityKind::Volatile => {
1681                                    crate::model::function::Volatility::Volatile
1682                                }
1683                                crate::analysis::facts::VolatilityKind::Stable => {
1684                                    crate::model::function::Volatility::Stable
1685                                }
1686                                crate::analysis::facts::VolatilityKind::Immutable => {
1687                                    crate::model::function::Volatility::Immutable
1688                                }
1689                            })
1690                        } else {
1691                            None
1692                        }
1693                    })
1694                    .unwrap_or(crate::model::function::Volatility::Volatile);
1695
1696                let security = f
1697                    .options
1698                    .iter()
1699                    .find_map(|opt| {
1700                        if let crate::analysis::facts::FuncOptionFact::Security(s) = opt {
1701                            Some(match s {
1702                                crate::analysis::facts::SecurityKind::Invoker => {
1703                                    crate::model::function::SecurityMode::Invoker
1704                                }
1705                                crate::analysis::facts::SecurityKind::Definer => {
1706                                    crate::model::function::SecurityMode::Definer
1707                                }
1708                            })
1709                        } else {
1710                            None
1711                        }
1712                    })
1713                    .unwrap_or(crate::model::function::SecurityMode::Invoker);
1714
1715                let language = f
1716                    .options
1717                    .iter()
1718                    .find_map(|opt| {
1719                        if let crate::analysis::facts::FuncOptionFact::Language(l) = opt {
1720                            Some(l.clone())
1721                        } else {
1722                            None
1723                        }
1724                    })
1725                    .unwrap_or_else(|| "sql".to_string());
1726
1727                self.local.functions.insert(
1728                    f.id.clone(),
1729                    crate::model::function::FunctionOverlay::Present(
1730                        crate::model::function::FunctionState {
1731                            id: f.id.clone(),
1732                            arg_types: f.params.iter().map(|p| p.ty.clone()).collect(),
1733                            return_type: f
1734                                .return_type
1735                                .as_ref()
1736                                .map(|rt| format!("{:?}", rt))
1737                                .unwrap_or_default(),
1738                            volatility,
1739                            language,
1740                            security,
1741                        },
1742                    ),
1743                );
1744                MutationResult::Applied
1745            }
1746            Mutation::AlterFunction(f) => {
1747                use crate::analysis::facts::{AlterFunctionAction, FuncOptionFact};
1748                use crate::model::function::{FunctionOverlay, SecurityMode, Volatility};
1749
1750                match &f.action {
1751                    AlterFunctionAction::OptionsChange(options) => {
1752                        self.snapshot_function(&f.id);
1753                        if let Some(FunctionOverlay::Present(function)) =
1754                            self.local.functions.get_mut(&f.id)
1755                        {
1756                            for option in options {
1757                                match option {
1758                                    FuncOptionFact::Volatility(volatility) => {
1759                                        function.volatility = match volatility {
1760                                            crate::analysis::facts::VolatilityKind::Volatile => {
1761                                                Volatility::Volatile
1762                                            }
1763                                            crate::analysis::facts::VolatilityKind::Stable => {
1764                                                Volatility::Stable
1765                                            }
1766                                            crate::analysis::facts::VolatilityKind::Immutable => {
1767                                                Volatility::Immutable
1768                                            }
1769                                        };
1770                                    }
1771                                    FuncOptionFact::Security(security) => {
1772                                        function.security = match security {
1773                                            crate::analysis::facts::SecurityKind::Invoker => {
1774                                                SecurityMode::Invoker
1775                                            }
1776                                            crate::analysis::facts::SecurityKind::Definer => {
1777                                                SecurityMode::Definer
1778                                            }
1779                                        };
1780                                    }
1781                                    FuncOptionFact::Language(language) => {
1782                                        function.language = language.clone();
1783                                    }
1784                                    _ => {}
1785                                }
1786                            }
1787                        }
1788                    }
1789                    AlterFunctionAction::Rename { to, .. } => {
1790                        let signature =
1791                            f.id.name
1792                                .find('(')
1793                                .map(|index| &f.id.name[index..])
1794                                .unwrap_or("");
1795                        let new_id = ObjectId::new(f.id.schema.clone(), format!("{to}{signature}"));
1796                        self.move_function(&f.id, &new_id);
1797                    }
1798                    AlterFunctionAction::SchemaChange { new_schema } => {
1799                        let new_id = ObjectId::new(new_schema.clone(), f.id.name.clone());
1800                        self.move_function(&f.id, &new_id);
1801                    }
1802                    AlterFunctionAction::OwnerChange(_)
1803                    | AlterFunctionAction::DependsOnExtension { .. }
1804                    | AlterFunctionAction::NoDependsOnExtension { .. } => {
1805                        self.snapshot_function(&f.id);
1806                    }
1807                }
1808                MutationResult::Applied
1809            }
1810            Mutation::DropFunction(f) => {
1811                let mut any_applied = false;
1812                for sig in &f.signatures {
1813                    let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
1814                    let schema = self.resolve_function_schema(&sig.name, &sig_str);
1815                    let id = ObjectId::new(schema, sig_str);
1816                    if !matches!(
1817                        self.local.functions.get(&id),
1818                        Some(crate::model::function::FunctionOverlay::Present(_))
1819                    ) {
1820                        if !f.if_exists {
1821                            self.local.confidence = Confidence::Tainted;
1822                            return MutationResult::Skipped;
1823                        }
1824                    } else {
1825                        let dependent_triggers: Vec<(ObjectId, ObjectId)> = self
1826                            .local
1827                            .graph
1828                            .edges
1829                            .iter()
1830                            .filter_map(|edge| {
1831                                let DependencyKind::TriggerOnTable { function_id, .. } = &edge.kind
1832                                else {
1833                                    return None;
1834                                };
1835                                (function_id == &id)
1836                                    .then(|| (edge.dependent.clone(), edge.referenced.clone()))
1837                            })
1838                            .collect();
1839                        if !dependent_triggers.is_empty() && !f.cascade {
1840                            return MutationResult::Conflict {
1841                                reason: format!(
1842                                    "function '{}' still has dependent triggers; use CASCADE",
1843                                    id
1844                                ),
1845                            };
1846                        }
1847
1848                        any_applied = true;
1849                        self.snapshot_function(&id);
1850                        self.local
1851                            .functions
1852                            .insert(id.clone(), crate::model::function::FunctionOverlay::Dropped);
1853
1854                        if f.cascade {
1855                            for (trigger_id, table_id) in &dependent_triggers {
1856                                self.snapshot_trigger(trigger_id);
1857                                self.local
1858                                    .triggers
1859                                    .insert(trigger_id.clone(), TriggerOverlay::Dropped);
1860                                self.snapshot_relation(table_id);
1861                                if let Some(RelationOverlay::Present(relation)) =
1862                                    self.local.relations.get_mut(table_id)
1863                                {
1864                                    relation.triggers.remove(&trigger_id.name);
1865                                }
1866                            }
1867                            if !dependent_triggers.is_empty() {
1868                                self.snapshot_graph_full();
1869                                self.local.graph.edges.retain(|edge| {
1870                                    !dependent_triggers
1871                                        .iter()
1872                                        .any(|(trigger_id, _)| edge.dependent == *trigger_id)
1873                                });
1874                            }
1875                        }
1876                    }
1877                }
1878                if any_applied {
1879                    MutationResult::Applied
1880                } else {
1881                    MutationResult::Skipped
1882                }
1883            }
1884            Mutation::CreateProcedure(p) => {
1885                self.snapshot_function(&p.id);
1886                self.snapshot_generation_counter();
1887                self.local.generation_counter += 1;
1888                let _generation = self.local.generation_counter;
1889
1890                self.local.functions.insert(
1891                    p.id.clone(),
1892                    crate::model::function::FunctionOverlay::Present(
1893                        crate::model::function::FunctionState {
1894                            id: p.id.clone(),
1895                            arg_types: p.params.iter().map(|p| p.ty.clone()).collect(),
1896                            return_type: "void".to_string(),
1897                            volatility: crate::model::function::Volatility::Volatile,
1898                            language: "sql".to_string(),
1899                            security: crate::model::function::SecurityMode::Invoker,
1900                        },
1901                    ),
1902                );
1903                MutationResult::Applied
1904            }
1905            Mutation::AlterProcedure(p) => {
1906                self.snapshot_function(&p.id);
1907                // No generation tracking in FunctionState
1908                MutationResult::Applied
1909            }
1910            Mutation::DropProcedure(p) => {
1911                let mut any_applied = false;
1912                for sig in &p.signatures {
1913                    let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
1914                    let schema = self.resolve_function_schema(&sig.name, &sig_str);
1915                    let id = ObjectId::new(schema, sig_str);
1916                    if !matches!(
1917                        self.local.functions.get(&id),
1918                        Some(crate::model::function::FunctionOverlay::Present(_))
1919                    ) {
1920                        if !p.if_exists {
1921                            self.local.confidence = Confidence::Tainted;
1922                            return MutationResult::Skipped;
1923                        }
1924                    } else {
1925                        any_applied = true;
1926                        self.snapshot_function(&id);
1927                        self.local
1928                            .functions
1929                            .insert(id, crate::model::function::FunctionOverlay::Dropped);
1930                    }
1931                }
1932                if any_applied {
1933                    MutationResult::Applied
1934                } else {
1935                    MutationResult::Skipped
1936                }
1937            }
1938            Mutation::CreatePublication(p) => {
1939                self.snapshot_publication(&p.name);
1940                self.snapshot_generation_counter();
1941                self.local.generation_counter += 1;
1942                let generation = self.local.generation_counter;
1943
1944                self.local.publications.insert(
1945                    p.name.clone(),
1946                    crate::model::replication::PublicationOverlay::Present(
1947                        crate::model::replication::PublicationState {
1948                            name: p.name.clone(),
1949                            scope: p.scope.clone(),
1950                            params: p.params.clone(),
1951                            generation,
1952                        },
1953                    ),
1954                );
1955
1956                if let crate::analysis::facts::PublicationScope::Explicit(objects) = &p.scope {
1957                    self.snapshot_graph_full();
1958                    for obj in objects {
1959                        if let crate::analysis::facts::PublicationObjectFact::Table {
1960                            name, ..
1961                        } = obj
1962                        {
1963                            let table_id = self.resolve_relation_id(name);
1964                            self.local.graph.edges.push(DependencyEdge::new(
1965                                table_id,
1966                                ObjectId::new("public", &p.name),
1967                                DependencyKind::PublicationIncludes {
1968                                    publication_name: p.name.clone(),
1969                                },
1970                            ));
1971                        }
1972                    }
1973                }
1974                MutationResult::Applied
1975            }
1976            Mutation::AlterPublication(p) => {
1977                self.snapshot_publication(&p.name);
1978                if !self.local.publications.contains_key(&p.name) {
1979                    self.local.confidence = Confidence::Tainted;
1980                    return MutationResult::Skipped;
1981                }
1982                self.snapshot_generation_counter();
1983                self.local.generation_counter += 1;
1984                let new_gen = self.local.generation_counter;
1985
1986                if let Some(crate::model::replication::PublicationOverlay::Present(publ)) =
1987                    self.local.publications.get_mut(&p.name)
1988                {
1989                    publ.generation = new_gen;
1990                }
1991                MutationResult::Applied
1992            }
1993            Mutation::DropPublication(p) => {
1994                for name in &p.names {
1995                    self.snapshot_publication(name);
1996                    if !p.if_exists && !self.local.publications.contains_key(name) {
1997                        self.local.confidence = Confidence::Tainted;
1998                        return MutationResult::Skipped;
1999                    }
2000                    self.local.publications.insert(
2001                        name.clone(),
2002                        crate::model::replication::PublicationOverlay::Dropped,
2003                    );
2004                }
2005                self.snapshot_graph_full();
2006                self.local.graph.edges.retain(|e| {
2007                    !(matches!(e.kind, DependencyKind::PublicationIncludes { .. })
2008                        && p.names.contains(&e.referenced.name))
2009                });
2010                MutationResult::Applied
2011            }
2012            Mutation::CreateSubscription(s) => {
2013                let name = s.name.clone().unwrap_or_else(|| "unnamed_sub".into());
2014                self.snapshot_subscription(&name);
2015                self.snapshot_generation_counter();
2016                self.local.generation_counter += 1;
2017                let generation = self.local.generation_counter;
2018
2019                self.local.subscriptions.insert(
2020                    name.clone(),
2021                    crate::model::replication::SubscriptionOverlay::Present(
2022                        crate::model::replication::SubscriptionState {
2023                            name,
2024                            connection: s.connection.clone(),
2025                            publications: s.publications.clone(),
2026                            params: s.params.clone(),
2027                            generation,
2028                        },
2029                    ),
2030                );
2031                MutationResult::Applied
2032            }
2033            Mutation::AlterSubscription(s) => {
2034                self.snapshot_subscription(&s.name);
2035                if !self.local.subscriptions.contains_key(&s.name) {
2036                    self.local.confidence = Confidence::Tainted;
2037                    return MutationResult::Skipped;
2038                }
2039                self.snapshot_generation_counter();
2040                self.local.generation_counter += 1;
2041                let new_gen = self.local.generation_counter;
2042
2043                if let Some(crate::model::replication::SubscriptionOverlay::Present(sub)) =
2044                    self.local.subscriptions.get_mut(&s.name)
2045                {
2046                    sub.generation = new_gen;
2047                }
2048                MutationResult::Applied
2049            }
2050            Mutation::DropSubscription(s) => {
2051                self.snapshot_subscription(&s.name);
2052                if !s.if_exists && !self.local.subscriptions.contains_key(&s.name) {
2053                    self.local.confidence = Confidence::Tainted;
2054                    return MutationResult::Skipped;
2055                }
2056                self.local.subscriptions.insert(
2057                    s.name.clone(),
2058                    crate::model::replication::SubscriptionOverlay::Dropped,
2059                );
2060                MutationResult::Applied
2061            }
2062            Mutation::CreateRole(r) => {
2063                let role_id = ObjectId::new("", &r.name);
2064                self.snapshot_role(&role_id);
2065                self.snapshot_generation_counter();
2066                self.local.generation_counter += 1;
2067                let _generation = self.local.generation_counter;
2068
2069                self.local.roles.insert(
2070                    role_id.clone(),
2071                    crate::model::role::RoleOverlay::Present(crate::model::role::RoleState {
2072                        id: role_id,
2073                        can_login: true,
2074                        is_superuser: false,
2075                        member_of: Vec::new(),
2076                        granted_privileges: Vec::new(),
2077                    }),
2078                );
2079                MutationResult::Applied
2080            }
2081            Mutation::AlterRole(r) => {
2082                if let Some(role_id) = Self::resolve_role_name(&r.name, &self.local.current_role) {
2083                    self.snapshot_role(&role_id);
2084                    if !self.local.roles.contains_key(&role_id) {
2085                        self.local.confidence = Confidence::Tainted;
2086                        return MutationResult::Skipped;
2087                    }
2088                    self.snapshot_generation_counter();
2089                    self.local.generation_counter += 1;
2090                    let _new_gen = self.local.generation_counter;
2091
2092                    if let Some(crate::model::role::RoleOverlay::Present(_role)) =
2093                        self.local.roles.get_mut(&role_id)
2094                    {
2095                        // No further action as fields have been simplified
2096                    }
2097                    MutationResult::Applied
2098                } else {
2099                    MutationResult::Skipped
2100                }
2101            }
2102            Mutation::DropRole(r) => {
2103                for name in &r.names {
2104                    if let Some(role_id) = Self::resolve_role_name(
2105                        &crate::analysis::facts::RoleFact::Named {
2106                            name: name.clone(),
2107                            via_legacy_group_syntax: false,
2108                        },
2109                        &self.local.current_role,
2110                    ) {
2111                        self.snapshot_role(&role_id);
2112                        if !r.if_exists && !self.local.roles.contains_key(&role_id) {
2113                            self.local.confidence = Confidence::Tainted;
2114
2115                            return MutationResult::Skipped;
2116                        }
2117                        self.local
2118                            .roles
2119                            .insert(role_id, crate::model::role::RoleOverlay::Dropped);
2120                    }
2121                }
2122                MutationResult::Applied
2123            }
2124            Mutation::Grant(grant) => {
2125                let privileges = Self::resolve_grant_privileges(&grant.privileges);
2126                let grantees = &grant.grantees;
2127                match &grant.target {
2128                    crate::analysis::mutations::ResolvedGrantTarget::Tables(ids) => {
2129                        for id in ids {
2130                            self.apply_grant_to_relation(id, &privileges, grantees);
2131                        }
2132                    }
2133                    crate::analysis::mutations::ResolvedGrantTarget::AllTablesInSchema(schemas) => {
2134                        let target_ids: Vec<ObjectId> = self
2135                            .local
2136                            .relations
2137                            .keys()
2138                            .filter(|id| schemas.contains(&id.schema))
2139                            .cloned()
2140                            .collect();
2141                        for id in &target_ids {
2142                            self.apply_grant_to_relation(id, &privileges, grantees);
2143                        }
2144                    }
2145                }
2146                MutationResult::Applied
2147            }
2148            Mutation::Revoke(revoke) => {
2149                let privileges = Self::resolve_grant_privileges(&revoke.privileges);
2150                let revokees = &revoke.revokees;
2151                match &revoke.target {
2152                    crate::analysis::mutations::ResolvedGrantTarget::Tables(ids) => {
2153                        for id in ids {
2154                            self.apply_revoke_to_relation(id, &privileges, revokees);
2155                        }
2156                    }
2157                    crate::analysis::mutations::ResolvedGrantTarget::AllTablesInSchema(schemas) => {
2158                        let target_ids: Vec<ObjectId> = self
2159                            .local
2160                            .relations
2161                            .keys()
2162                            .filter(|id| schemas.contains(&id.schema))
2163                            .cloned()
2164                            .collect();
2165                        for id in &target_ids {
2166                            self.apply_revoke_to_relation(id, &privileges, revokees);
2167                        }
2168                    }
2169                }
2170                MutationResult::Applied
2171            }
2172            Mutation::CreateDatabase(_) => MutationResult::Applied,
2173            Mutation::AlterDatabase(_) => MutationResult::Applied,
2174            Mutation::DropDatabase(_) => MutationResult::Applied,
2175            Mutation::Vacuum { .. } => MutationResult::Applied,
2176        }
2177    }
2178
2179    fn snapshot_relation(&mut self, id: &ObjectId) {
2180        if let Some(frame) = self.local.transactions.last_mut() {
2181            let previous = self.local.relations.get(id).cloned();
2182            frame.undo_log.push(StateChange::RelationSnapshot {
2183                id: id.clone(),
2184                previous: Box::new(previous),
2185            });
2186        }
2187    }
2188
2189    fn snapshot_type(&mut self, id: &ObjectId) {
2190        if let Some(frame) = self.local.transactions.last_mut() {
2191            let previous = self.local.types.get(id).cloned();
2192            frame.undo_log.push(StateChange::TypeSnapshot {
2193                id: id.clone(),
2194                previous,
2195            });
2196        }
2197    }
2198
2199    fn snapshot_sequence(&mut self, id: &ObjectId) {
2200        if let Some(frame) = self.local.transactions.last_mut() {
2201            let previous = self.local.sequences.get(id).cloned();
2202            frame.undo_log.push(StateChange::SequenceSnapshot {
2203                id: id.clone(),
2204                previous,
2205            });
2206        }
2207    }
2208
2209    fn move_function(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
2210        self.snapshot_function(old_id);
2211        self.snapshot_function(new_id);
2212        if let Some(crate::model::function::FunctionOverlay::Present(mut function)) =
2213            self.local.functions.remove(old_id)
2214        {
2215            function.id = new_id.clone();
2216            self.local.functions.insert(
2217                new_id.clone(),
2218                crate::model::function::FunctionOverlay::Present(function),
2219            );
2220        }
2221
2222        self.snapshot_graph_full();
2223        self.local.graph.propagate_rename(old_id, new_id);
2224        self.local.graph.edges.push(DependencyEdge::new(
2225            old_id.clone(),
2226            new_id.clone(),
2227            DependencyKind::RenameTo,
2228        ));
2229    }
2230
2231    fn snapshot_function(&mut self, id: &ObjectId) {
2232        if let Some(frame) = self.local.transactions.last_mut() {
2233            let previous = self.local.functions.get(id).cloned();
2234            frame.undo_log.push(StateChange::FunctionSnapshot {
2235                id: id.clone(),
2236                previous,
2237            });
2238        }
2239    }
2240
2241    fn snapshot_publication(&mut self, name: &str) {
2242        if let Some(frame) = self.local.transactions.last_mut() {
2243            let previous = self.local.publications.get(name).cloned();
2244            frame.undo_log.push(StateChange::PublicationSnapshot {
2245                id: ObjectId::new("", name),
2246                previous,
2247            });
2248        }
2249    }
2250
2251    fn snapshot_subscription(&mut self, name: &str) {
2252        if let Some(frame) = self.local.transactions.last_mut() {
2253            let previous = self.local.subscriptions.get(name).cloned();
2254            frame.undo_log.push(StateChange::SubscriptionSnapshot {
2255                id: ObjectId::new("", name),
2256                previous,
2257            });
2258        }
2259    }
2260
2261    fn snapshot_role(&mut self, id: &ObjectId) {
2262        if let Some(frame) = self.local.transactions.last_mut() {
2263            let previous = self.local.roles.get(id).cloned();
2264            frame.undo_log.push(StateChange::RoleSnapshot {
2265                id: id.clone(),
2266                previous,
2267            });
2268        }
2269    }
2270
2271    fn snapshot_trigger(&mut self, id: &ObjectId) {
2272        if let Some(frame) = self.local.transactions.last_mut() {
2273            let previous = self.local.triggers.get(id).cloned();
2274            frame.undo_log.push(StateChange::TriggerSnapshot {
2275                id: id.clone(),
2276                previous,
2277            });
2278        }
2279    }
2280
2281    fn snapshot_constraint(&mut self, table_id: &ObjectId, name: &str) {
2282        if let Some(frame) = self.local.transactions.last_mut() {
2283            let key = (table_id.clone(), name.to_string());
2284            let previous = self.local.constraints.get(&key).cloned();
2285            frame.undo_log.push(StateChange::ConstraintSnapshot {
2286                table_id: table_id.clone(),
2287                name: name.to_string(),
2288                previous,
2289            });
2290        }
2291    }
2292
2293    #[allow(dead_code)]
2294    fn snapshot_current_role(&mut self) {
2295        if let Some(frame) = self.local.transactions.last_mut() {
2296            frame.undo_log.push(StateChange::CurrentRoleSnapshot {
2297                previous: self.local.current_role.clone(),
2298            });
2299        }
2300    }
2301
2302    fn snapshot_search_path(&mut self) {
2303        if let Some(frame) = self.local.transactions.last_mut() {
2304            frame.undo_log.push(StateChange::SearchPathSnapshot {
2305                previous: self.local.search_path.clone(),
2306            });
2307        }
2308    }
2309
2310    fn snapshot_generation_counter(&mut self) {
2311        if let Some(frame) = self.local.transactions.last_mut() {
2312            frame.undo_log.push(StateChange::GenerationCounterSnapshot {
2313                previous: self.local.generation_counter,
2314            });
2315        }
2316    }
2317
2318    #[allow(dead_code)]
2319    fn snapshot_pending_validation(&mut self) {
2320        if let Some(frame) = self.local.transactions.last_mut() {
2321            frame.undo_log.push(StateChange::PendingValidationSnapshot {
2322                previous: self.local.pending_validation.clone(),
2323            });
2324        }
2325    }
2326
2327    fn snapshot_confidence(&mut self) {
2328        if let Some(frame) = self.local.transactions.last_mut() {
2329            frame.undo_log.push(StateChange::ConfidenceSnapshot {
2330                previous: self.local.confidence.clone(),
2331            });
2332        }
2333    }
2334
2335    fn snapshot_graph(&mut self) {
2336        if let Some(frame) = self.local.transactions.last_mut() {
2337            frame.undo_log.push(StateChange::GraphLengthMarker {
2338                len: self.local.graph.edges.len(),
2339            });
2340        }
2341    }
2342
2343    fn snapshot_graph_full(&mut self) {
2344        if let Some(frame) = self.local.transactions.last_mut() {
2345            frame.undo_log.push(StateChange::GraphSnapshot {
2346                previous: self.local.graph.edges.clone(),
2347            });
2348        }
2349    }
2350
2351    fn rollback_frame(&mut self, mut frame: TransactionFrame) {
2352        while let Some(change) = frame.undo_log.pop() {
2353            match change {
2354                StateChange::RelationSnapshot { id, previous } => {
2355                    if let Some(prev) = *previous {
2356                        self.local.relations.insert(id, prev);
2357                    } else {
2358                        self.local.relations.remove(&id);
2359                    }
2360                }
2361                StateChange::TypeSnapshot { id, previous } => {
2362                    if let Some(prev) = previous {
2363                        self.local.types.insert(id, prev);
2364                    } else {
2365                        self.local.types.remove(&id);
2366                    }
2367                }
2368                StateChange::SequenceSnapshot { id, previous } => {
2369                    if let Some(prev) = previous {
2370                        self.local.sequences.insert(id, prev);
2371                    } else {
2372                        self.local.sequences.remove(&id);
2373                    }
2374                }
2375                StateChange::FunctionSnapshot { id, previous } => {
2376                    if let Some(prev) = previous {
2377                        self.local.functions.insert(id, prev);
2378                    } else {
2379                        self.local.functions.remove(&id);
2380                    }
2381                }
2382                StateChange::PublicationSnapshot { id, previous } => {
2383                    if let Some(prev) = previous {
2384                        self.local.publications.insert(id.name, prev);
2385                    } else {
2386                        self.local.publications.remove(&id.name);
2387                    }
2388                }
2389                StateChange::SubscriptionSnapshot { id, previous } => {
2390                    if let Some(prev) = previous {
2391                        self.local.subscriptions.insert(id.name, prev);
2392                    } else {
2393                        self.local.subscriptions.remove(&id.name);
2394                    }
2395                }
2396                StateChange::RoleSnapshot { id, previous } => {
2397                    if let Some(prev) = previous {
2398                        self.local.roles.insert(id, prev);
2399                    } else {
2400                        self.local.roles.remove(&id);
2401                    }
2402                }
2403                StateChange::TriggerSnapshot { id, previous } => {
2404                    if let Some(prev) = previous {
2405                        self.local.triggers.insert(id, prev);
2406                    } else {
2407                        self.local.triggers.remove(&id);
2408                    }
2409                }
2410                StateChange::ConstraintSnapshot {
2411                    table_id,
2412                    name,
2413                    previous,
2414                } => {
2415                    let key = (table_id, name);
2416                    if let Some(previous) = previous {
2417                        self.local.constraints.insert(key, previous);
2418                    } else {
2419                        self.local.constraints.remove(&key);
2420                    }
2421                }
2422                StateChange::GraphLengthMarker { len } => {
2423                    self.local.graph.edges.truncate(len);
2424                }
2425                StateChange::GraphSnapshot { previous } => {
2426                    self.local.graph.edges = previous;
2427                }
2428                StateChange::CurrentRoleSnapshot { previous } => {
2429                    self.local.current_role = previous;
2430                }
2431                StateChange::SearchPathSnapshot { previous } => {
2432                    self.local.search_path = previous;
2433                }
2434                StateChange::GenerationCounterSnapshot { previous } => {
2435                    self.local.generation_counter = previous;
2436                }
2437                StateChange::PendingValidationSnapshot { previous } => {
2438                    self.local.pending_validation = previous;
2439                }
2440                StateChange::ConfidenceSnapshot { previous } => {
2441                    self.local.confidence = previous;
2442                }
2443            }
2444        }
2445    }
2446}