Skip to main content

safe_migrate/analysis/
state.rs

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