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