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::{NamespaceSnapshot, 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::schema::SchemaOverlay;
14use crate::model::sequence::{SequenceKind, SequenceOverlay, SequenceState};
15use crate::model::trigger::TriggerOverlay;
16use crate::model::types::{TypeKind, TypeOverlay, TypeState};
17use std::collections::{HashMap, HashSet};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum Confidence {
21    Exact,
22    Tainted,
23}
24
25#[derive(Debug, PartialEq, Eq)]
26pub enum MutationResult {
27    Applied,
28    Skipped,
29    /// PostgreSQL did not execute this statement because an earlier statement
30    /// aborted the active transaction.
31    NotExecuted,
32    Conflict {
33        reason: String,
34    },
35}
36
37#[derive(Debug, Default, Clone)]
38pub struct CascadeResult {
39    pub dropped_relations: HashSet<ObjectId>,
40    pub dropped_indexes: HashSet<ObjectId>,
41    pub dropped_constraints: HashSet<(ObjectId, String)>,
42}
43
44#[derive(Clone)]
45pub struct LocalState {
46    pub schemas: HashMap<String, SchemaOverlay>,
47    pub relations: HashMap<ObjectId, RelationOverlay>,
48    pub types: HashMap<ObjectId, TypeOverlay>,
49    pub functions: HashMap<ObjectId, crate::model::function::FunctionOverlay>,
50    pub sequences: HashMap<ObjectId, SequenceOverlay>,
51    pub publications: HashMap<String, crate::model::replication::PublicationOverlay>,
52    pub subscriptions: HashMap<String, crate::model::replication::SubscriptionOverlay>,
53    pub roles: HashMap<ObjectId, crate::model::role::RoleOverlay>,
54    pub triggers: HashMap<ObjectId, TriggerOverlay>,
55    pub constraints: HashMap<(ObjectId, String), ConstraintState>,
56    pub graph: DependencyGraph,
57    pub search_path: Vec<String>,
58    pub default_search_path: Vec<String>,
59    pub search_path_template: Vec<String>,
60    pub default_search_path_template: Vec<String>,
61    /// Role currently active for this session context (updated by SET ROLE /
62    /// SET SESSION AUTHORIZATION). Begins equal to `session_role`.
63    pub current_role: String,
64    /// Whether `current_role` is statically known. False when no V5 cache was
65    /// loaded and no SET ROLE statement has been processed yet.
66    pub current_role_known: bool,
67    /// Effective role setting that survives transaction commit. A LOCAL role
68    /// change updates `current_role` without changing this value.
69    pub persistent_current_role: String,
70    pub persistent_current_role_known: bool,
71    /// The session-level role as captured from the cache's `source_role`. This
72    /// is the value `SET ROLE NONE` / `SET SESSION AUTHORIZATION DEFAULT`
73    /// reverts to.
74    pub session_role: String,
75    /// Whether `session_role` is statically known (mirrors `current_role_known`
76    /// at baseline; a SET SESSION AUTHORIZATION updates this too).
77    pub session_role_known: bool,
78    /// Session authorization that survives transaction commit.
79    pub persistent_session_role: String,
80    pub persistent_session_role_known: bool,
81    /// Login identity restored by `SET SESSION AUTHORIZATION DEFAULT`.
82    pub authenticated_role: String,
83    pub authenticated_role_known: bool,
84    /// Whether the cache contains a complete PostgreSQL role catalog.
85    pub roles_known: bool,
86    pub confidence: Confidence,
87    pub transactions: Vec<TransactionFrame>,
88    pub transaction_aborted: bool,
89    pub pending_validation: HashSet<(ObjectId, String)>,
90    pub generation_counter: u64,
91}
92
93#[derive(Clone, Debug)]
94pub struct PreState {
95    pub relations: HashMap<ObjectId, crate::model::relation::RelationState>,
96    pub functions: HashMap<ObjectId, crate::model::function::FunctionState>,
97    pub roles: HashMap<ObjectId, crate::model::role::RoleState>,
98    pub publications: HashMap<String, crate::model::replication::PublicationState>,
99    pub subscriptions: HashMap<String, crate::model::replication::SubscriptionState>,
100    pub sequences: HashMap<ObjectId, crate::model::sequence::SequenceState>,
101    pub types: HashMap<ObjectId, crate::model::types::TypeState>,
102    pub indexes: Vec<crate::analysis::graph::DependencyEdge>,
103}
104
105#[derive(Clone)]
106pub struct AnalysisState {
107    pub pg_version_num: Option<u32>,
108    /// Whether the initial cache was loaded from a real cache file. An empty
109    /// cache can be a valid baseline for an empty database, so availability
110    /// must not be inferred from the number of modeled objects.
111    pub baseline_available: bool,
112    /// `None` means the cache covered all non-system schemas. A populated set
113    /// records an explicitly scoped sync, for which objects outside the set
114    /// are unknown rather than known absent.
115    pub baseline_schemas: Option<HashSet<String>>,
116    pub baseline_relations: HashSet<ObjectId>,
117    pub baseline_indexes: HashSet<ObjectId>,
118    pub baseline_foreign_keys: HashSet<(ObjectId, String)>,
119    pub baseline_fk_dependencies: HashSet<ObjectId>,
120    pub baseline_sequences: HashSet<ObjectId>,
121    pub local: LocalState,
122}
123
124impl AnalysisState {
125    fn trigger_key(table_id: &ObjectId, name: &str) -> ObjectId {
126        // PostgreSQL identifiers cannot contain NUL, so this is an unambiguous
127        // internal composite key while keeping the public cache representation
128        // as the trigger's actual name.
129        ObjectId::new(&table_id.schema, format!("{}\0{name}", table_id.name))
130    }
131
132    pub fn new(cache: DbCache) -> Self {
133        Self::with_baseline(cache, true)
134    }
135
136    pub fn with_baseline(cache: DbCache, baseline_available: bool) -> Self {
137        let default_search_path = cache.search_path.clone();
138        let default_search_path_template = if cache.metadata.schemas.is_none() {
139            cache
140                .metadata
141                .source_search_path
142                .clone()
143                .unwrap_or_else(|| default_search_path.clone())
144        } else {
145            default_search_path.clone()
146        };
147        let current_role_known = cache.metadata.source_role.is_some();
148        let current_role = cache
149            .metadata
150            .source_role
151            .clone()
152            .unwrap_or_else(|| "postgres".to_string());
153        let session_role_known = cache.metadata.source_session_role.is_some();
154        let session_role = cache
155            .metadata
156            .source_session_role
157            .clone()
158            .unwrap_or_else(|| current_role.clone());
159        let authenticated_role = session_role.clone();
160        let authenticated_role_known = session_role_known;
161        let persistent_current_role = current_role.clone();
162        let persistent_current_role_known = current_role_known;
163        let persistent_session_role = session_role.clone();
164        let persistent_session_role_known = session_role_known;
165        let roles_known = cache.metadata.source_session_role.is_some();
166        let baseline_schemas = cache
167            .metadata
168            .schemas
169            .as_ref()
170            .map(|schemas| schemas.iter().cloned().collect());
171        let mut relations: HashMap<ObjectId, RelationOverlay> = HashMap::new();
172        let mut baseline_relations = HashSet::new();
173        let mut baseline_indexes = HashSet::new();
174        let mut baseline_foreign_keys = HashSet::new();
175        let mut baseline_fk_dependencies = HashSet::new();
176        let mut triggers = HashMap::new();
177        let mut constraints = HashMap::new();
178        let mut types = HashMap::new();
179        let mut graph = DependencyGraph::new();
180
181        let mut schemas: HashMap<String, SchemaOverlay> = cache
182            .schemas
183            .iter()
184            .map(|(name, schema)| (name.clone(), SchemaOverlay::Present(schema.clone())))
185            .collect();
186        // Effective cached search-path entries and modeled objects are direct
187        // evidence that their namespaces existed at synchronization time.
188        // This also keeps programmatically assembled V5 caches internally
189        // consistent without treating unrelated out-of-scope schemas as
190        // authoritative catalogs.
191        let inferred_schema_owner = ObjectId::new(
192            "",
193            cache.metadata.source_role.as_deref().unwrap_or("postgres"),
194        );
195        for name in cache
196            .relations
197            .keys()
198            .map(|id| &id.schema)
199            .chain(cache.types.keys().map(|id| &id.schema))
200            .chain(cache.functions.keys().map(|id| &id.schema))
201            .chain(cache.sequences.keys().map(|id| &id.schema))
202        {
203            schemas.entry(name.clone()).or_insert_with(|| {
204                SchemaOverlay::Present(crate::model::schema::SchemaState {
205                    name: name.clone(),
206                    owner: inferred_schema_owner.clone(),
207                    generation: 0,
208                })
209            });
210        }
211        if cache.schemas.is_empty() && cache.metadata.schemas.is_none() {
212            for name in &cache.search_path {
213                schemas.entry(name.clone()).or_insert_with(|| {
214                    SchemaOverlay::Present(crate::model::schema::SchemaState {
215                        name: name.clone(),
216                        owner: inferred_schema_owner.clone(),
217                        generation: 0,
218                    })
219                });
220            }
221        }
222
223        let sequences = cache
224            .sequences
225            .iter()
226            .map(|(id, sequence)| (id.clone(), SequenceOverlay::Present(sequence.clone())))
227            .collect();
228        let baseline_sequences = cache.sequences.keys().cloned().collect();
229        for sequence in cache.sequences.values() {
230            if let Some((table, column)) = &sequence.owned_by {
231                graph.edges.push(DependencyEdge::new(
232                    sequence.id.clone(),
233                    table.clone(),
234                    DependencyKind::SequenceOwnedBy {
235                        column: column.clone(),
236                    },
237                ));
238            }
239        }
240
241        for (id, rel_state) in cache.baseline_relations() {
242            if rel_state.is_fk_dependency {
243                baseline_fk_dependencies.insert(id.clone());
244            }
245            relations.insert(id.clone(), RelationOverlay::Present(rel_state.clone()));
246            baseline_relations.insert(id.clone());
247        }
248
249        for (id, type_state) in &cache.types {
250            types.insert(id.clone(), TypeOverlay::Present(type_state.clone()));
251        }
252
253        for fk in cache.foreign_keys {
254            baseline_foreign_keys.insert((fk.from_table.clone(), fk.constraint_name.clone()));
255            graph.edges.push(DependencyEdge::new(
256                fk.from_table,
257                fk.to_table,
258                DependencyKind::ForeignKey {
259                    constraint_name: Some(fk.constraint_name),
260                    from_columns: Vec::new(),
261                    to_columns: Vec::new(),
262                    from_generation: 0,
263                },
264            ));
265        }
266
267        for idx in cache.indexes {
268            // BUG-008: index ObjectIds go into baseline_indexes, not baseline_relations
269            baseline_indexes.insert(idx.index_id.clone());
270            graph.edges.push(DependencyEdge::new(
271                idx.index_id,
272                idx.table_id,
273                DependencyKind::IndexOnRelation {
274                    using_method: None,
275                    has_predicate: false,
276                    is_concurrent: false,
277                    is_unique: false,
278                    eligibility_known: false,
279                },
280            ));
281        }
282
283        for dependency in cache.dependencies {
284            if dependency.deptype != "view" {
285                continue;
286            }
287            let (Some(obj_schema), Some(obj_name), Some(ref_schema), Some(ref_name)) = (
288                dependency.obj_schema,
289                dependency.obj_name,
290                dependency.ref_schema,
291                dependency.ref_name,
292            ) else {
293                continue;
294            };
295            let dependent = ObjectId::new(obj_schema, obj_name);
296            let referenced = ObjectId::new(ref_schema, ref_name);
297            // Older caches created on PostgreSQL 14/15 can contain an
298            // internal pg_rewrite self-edge for a view. Ignore it while
299            // loading so upgrading safe-migrate does not require a re-sync to
300            // restore a meaningful dependency graph.
301            if dependent == referenced {
302                continue;
303            }
304            let is_view = relations.get(&dependent).is_some_and(|relation| {
305                matches!(
306                    relation,
307                    RelationOverlay::Present(state)
308                        if matches!(
309                            state.kind,
310                            crate::model::relation::RelationKind::View
311                                | crate::model::relation::RelationKind::MaterializedView
312                        )
313                )
314            });
315            if is_view && relations.contains_key(&referenced) {
316                graph.edges.push(DependencyEdge::new(
317                    dependent,
318                    referenced,
319                    DependencyKind::ViewDependency { view_generation: 0 },
320                ));
321            }
322        }
323
324        for constraint in cache.constraints {
325            constraints.insert(
326                (constraint.table_id.clone(), constraint.name.clone()),
327                constraint,
328            );
329        }
330
331        for t in cache.triggers {
332            let trigger_key = Self::trigger_key(&t.table_id, &t.trigger_id.name);
333            triggers.insert(
334                trigger_key.clone(),
335                TriggerOverlay::Present(crate::model::trigger::TriggerState {
336                    name: t.trigger_id.name.clone(),
337                    id: trigger_key.clone(),
338                    table_id: t.table_id.clone(),
339                    enabled_mode: t.enabled_mode,
340                    generation: 0,
341                }),
342            );
343            graph.edges.push(DependencyEdge::new(
344                trigger_key.clone(),
345                t.table_id,
346                DependencyKind::TriggerOnTable {
347                    trigger_id: trigger_key,
348                    function_id: t.function_id,
349                },
350            ));
351        }
352
353        let mut functions: HashMap<ObjectId, crate::model::function::FunctionOverlay> =
354            HashMap::new();
355        for (id, func_state) in &cache.functions {
356            functions.insert(
357                id.clone(),
358                crate::model::function::FunctionOverlay::Present(func_state.clone()),
359            );
360        }
361
362        let mut state = Self {
363            pg_version_num: cache.pg_version_num,
364            baseline_available,
365            baseline_schemas,
366            baseline_relations,
367            baseline_indexes,
368            baseline_foreign_keys,
369            baseline_fk_dependencies,
370            baseline_sequences,
371            local: LocalState {
372                schemas,
373                relations,
374                types,
375                functions,
376                sequences,
377                publications: HashMap::new(),
378                subscriptions: HashMap::new(),
379                roles: cache
380                    .roles
381                    .into_iter()
382                    .map(|(id, role)| (id, crate::model::role::RoleOverlay::Present(role)))
383                    .collect(),
384                triggers,
385                constraints,
386                graph,
387                search_path: default_search_path.clone(),
388                default_search_path,
389                search_path_template: default_search_path_template.clone(),
390                default_search_path_template,
391                current_role,
392                current_role_known,
393                persistent_current_role,
394                persistent_current_role_known,
395                session_role,
396                session_role_known,
397                persistent_session_role,
398                persistent_session_role_known,
399                authenticated_role,
400                authenticated_role_known,
401                roles_known,
402                confidence: Confidence::Exact,
403                transactions: Vec::new(),
404                transaction_aborted: false,
405                pending_validation: HashSet::new(),
406                generation_counter: 0,
407            },
408        };
409        state.refresh_role_sensitive_search_path();
410        state.local.default_search_path = state.local.search_path.clone();
411        state
412    }
413
414    pub fn get_relation(&self, id: &ObjectId) -> Option<&RelationOverlay> {
415        self.local.relations.get(id)
416    }
417
418    pub fn resolve_function_schema(
419        &self,
420        name: &crate::ast::identifiers::QualifiedName,
421        sig_str: &str,
422    ) -> String {
423        if let Some(schema) = &name.schema {
424            return schema.resolve();
425        }
426        for schema in &self.local.search_path {
427            let candidate = ObjectId::new(schema.clone(), sig_str.to_string());
428            if self.local.functions.contains_key(&candidate) {
429                return schema.clone();
430            }
431        }
432        self.local
433            .search_path
434            .first()
435            .cloned()
436            .unwrap_or_else(|| "public".to_string())
437    }
438
439    pub fn resolve_relation_id(&self, name: &crate::ast::identifiers::QualifiedName) -> ObjectId {
440        if let Some(schema) = &name.schema {
441            return ObjectId::new(schema.resolve(), name.name.resolve());
442        }
443        let resolved_name = name.name.resolve();
444        for schema in &self.local.search_path {
445            let mut candidate = ObjectId::new(schema.clone(), resolved_name.clone());
446            if self.local.relations.contains_key(&candidate) {
447                candidate.inferred_schema = true;
448                return candidate;
449            }
450        }
451        let schema = self
452            .local
453            .search_path
454            .first()
455            .cloned()
456            .unwrap_or_else(|| "public".to_string());
457        let mut id = ObjectId::new(schema, resolved_name);
458        id.inferred_schema = true;
459        id
460    }
461
462    pub fn relation_is_present(&self, id: &ObjectId) -> bool {
463        matches!(
464            self.local.relations.get(id),
465            Some(RelationOverlay::Present(_))
466        )
467    }
468
469    /// Returns whether a cache-backed absence is authoritative for an object.
470    /// A scoped cache only establishes absence in the schemas it actually
471    /// synchronized.
472    pub fn baseline_covers_object(&self, id: &ObjectId) -> bool {
473        self.baseline_schemas
474            .as_ref()
475            .is_none_or(|schemas| schemas.contains(&id.schema))
476    }
477
478    pub fn baseline_scope_omits_displayed_object<'a>(
479        &self,
480        object_name: &'a str,
481    ) -> Option<&'a str> {
482        let schemas = self.baseline_schemas.as_ref()?;
483        let (schema, _) = object_name.split_once('.')?;
484        (!schemas.contains(schema)).then_some(schema)
485    }
486
487    fn sequence_is_present(&self, id: &ObjectId) -> bool {
488        matches!(
489            self.local.sequences.get(id),
490            Some(SequenceOverlay::Present(_))
491        )
492    }
493
494    fn type_is_present(&self, id: &ObjectId) -> bool {
495        matches!(self.local.types.get(id), Some(TypeOverlay::Present(_)))
496    }
497
498    fn index_is_present(&self, id: &ObjectId) -> bool {
499        self.local.graph.edges.iter().any(|edge| {
500            matches!(edge.kind, DependencyKind::IndexOnRelation { .. }) && edge.dependent == *id
501        })
502    }
503
504    fn next_generated_constraint_name(
505        &self,
506        table: &ObjectId,
507        name1: &str,
508        name2: Option<&str>,
509        label: &str,
510    ) -> String {
511        (0..)
512            .map(|suffix| {
513                let label = if suffix == 0 {
514                    label.to_string()
515                } else {
516                    format!("{label}{suffix}")
517                };
518                Self::postgres_object_name(name1, name2, &label)
519            })
520            .find(|candidate| {
521                !self
522                    .local
523                    .constraints
524                    .contains_key(&(table.clone(), candidate.clone()))
525            })
526            .expect("constraint suffix space is unbounded")
527    }
528
529    fn postgres_object_name(name1: &str, name2: Option<&str>, label: &str) -> String {
530        const MAX_IDENTIFIER_BYTES: usize = 63;
531
532        fn truncate(value: &str, max_bytes: usize) -> &str {
533            let mut end = max_bytes.min(value.len());
534            while !value.is_char_boundary(end) {
535                end -= 1;
536            }
537            &value[..end]
538        }
539
540        let separators = usize::from(name2.is_some()) + 1;
541        let available = MAX_IDENTIFIER_BYTES.saturating_sub(label.len() + separators);
542        let mut name1_bytes = name1.len();
543        let mut name2_bytes = name2.map_or(0, str::len);
544        while name1_bytes + name2_bytes > available {
545            if name1_bytes > name2_bytes {
546                name1_bytes -= 1;
547            } else {
548                name2_bytes -= 1;
549            }
550        }
551
552        let name1 = truncate(name1, name1_bytes);
553        match name2 {
554            Some(name2) => format!("{name1}_{}_{}", truncate(name2, name2_bytes), label),
555            None => format!("{name1}_{label}"),
556        }
557    }
558
559    fn relation_namespace_is_taken(&self, id: &ObjectId) -> bool {
560        self.relation_is_present(id)
561            || self.sequence_is_present(id)
562            || self.index_is_present(id)
563            || self.type_is_present(id)
564    }
565
566    fn next_implicit_sequence_id(
567        &self,
568        table: &ObjectId,
569        column: &str,
570        reserved: &HashSet<ObjectId>,
571    ) -> ObjectId {
572        (0..)
573            .map(|suffix| {
574                let label = if suffix == 0 {
575                    "seq".to_string()
576                } else {
577                    format!("seq{suffix}")
578                };
579                ObjectId::new(
580                    &table.schema,
581                    Self::postgres_object_name(&table.name, Some(column), &label),
582                )
583            })
584            .find(|candidate| {
585                !reserved.contains(candidate) && !self.relation_namespace_is_taken(candidate)
586            })
587            .expect("implicit sequence suffix space is unbounded")
588    }
589
590    fn sequence_nextval_default(id: &ObjectId) -> crate::analysis::expr_ir::ExprIr {
591        crate::analysis::expr_ir::ExprIr::FunctionCall {
592            name: "nextval".to_string(),
593            args: vec![crate::analysis::expr_ir::ExprIr::Literal(format!(
594                "{}.{}",
595                id.schema, id.name
596            ))],
597        }
598    }
599
600    pub fn column_was_added_in_transaction(&self, table_id: &ObjectId, column: &str) -> bool {
601        if self.local.transactions.is_empty() {
602            return false;
603        }
604
605        // Search from the oldest transaction frame to the newest
606        for frame in &self.local.transactions {
607            for change in &frame.undo_log {
608                if let StateChange::RelationSnapshot { id, previous } = change
609                    && id == table_id
610                {
611                    match previous.as_ref() {
612                        None | Some(RelationOverlay::Dropped) => {
613                            return true;
614                        }
615                        Some(RelationOverlay::Present(r)) => {
616                            let col_existed = r.columns.iter().any(|c| c.name == column);
617                            return !col_existed;
618                        }
619                    }
620                }
621            }
622        }
623        false
624    }
625
626    pub fn capture_pre_state(&self) -> PreState {
627        let mut relations = HashMap::new();
628        for (id, overlay) in &self.local.relations {
629            if let RelationOverlay::Present(s) = overlay {
630                relations.insert(id.clone(), s.clone());
631            }
632        }
633
634        let mut functions = HashMap::new();
635        for (id, overlay) in &self.local.functions {
636            if let crate::model::function::FunctionOverlay::Present(s) = overlay {
637                functions.insert(id.clone(), s.clone());
638            }
639        }
640
641        let mut roles = HashMap::new();
642        for (name, overlay) in &self.local.roles {
643            if let crate::model::role::RoleOverlay::Present(s) = overlay {
644                roles.insert(name.clone(), s.clone());
645            }
646        }
647
648        let mut publications = HashMap::new();
649        for (name, overlay) in &self.local.publications {
650            if let crate::model::replication::PublicationOverlay::Present(s) = overlay {
651                publications.insert(name.clone(), s.clone());
652            }
653        }
654
655        let mut subscriptions = HashMap::new();
656        for (name, overlay) in &self.local.subscriptions {
657            if let crate::model::replication::SubscriptionOverlay::Present(s) = overlay {
658                subscriptions.insert(name.clone(), s.clone());
659            }
660        }
661
662        let mut sequences = HashMap::new();
663        for (id, overlay) in &self.local.sequences {
664            if let SequenceOverlay::Present(s) = overlay {
665                sequences.insert(id.clone(), s.clone());
666            }
667        }
668
669        let mut types = HashMap::new();
670        for (id, overlay) in &self.local.types {
671            if let TypeOverlay::Present(s) = overlay {
672                types.insert(id.clone(), s.clone());
673            }
674        }
675
676        let indexes = self
677            .local
678            .graph
679            .edges
680            .iter()
681            .filter(|e| matches!(e.kind, DependencyKind::IndexOnRelation { .. }))
682            .cloned()
683            .collect();
684
685        PreState {
686            relations,
687            functions,
688            roles,
689            publications,
690            subscriptions,
691            sequences,
692            types,
693            indexes,
694        }
695    }
696
697    pub fn get_cascade_closure(&self, target_oid: &ObjectId) -> CascadeResult {
698        let mut result = CascadeResult::default();
699        let mut visited = HashSet::new();
700        self.walk_cascade(target_oid, &mut visited, &mut result);
701        result
702    }
703
704    fn walk_cascade(
705        &self,
706        current: &ObjectId,
707        visited: &mut HashSet<ObjectId>,
708        result: &mut CascadeResult,
709    ) {
710        let resolved_current = self.local.graph.resolve_rename(current).clone();
711
712        if !visited.insert(resolved_current.clone()) {
713            return;
714        }
715
716        result.dropped_relations.insert(resolved_current.clone());
717
718        for edge in &self.local.graph.edges {
719            match &edge.kind {
720                DependencyKind::ViewDependency { .. } => {
721                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current {
722                        let resolved_view_id =
723                            self.local.graph.resolve_rename(&edge.dependent).clone();
724                        if !visited.contains(&resolved_view_id) {
725                            self.walk_cascade(&resolved_view_id, visited, result);
726                        }
727                    }
728                }
729                DependencyKind::IndexOnRelation { .. } => {
730                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current {
731                        result
732                            .dropped_indexes
733                            .insert(self.local.graph.resolve_rename(&edge.dependent).clone());
734                    }
735                }
736                DependencyKind::ForeignKey {
737                    constraint_name, ..
738                } => {
739                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current
740                        && let Some(cname) = constraint_name
741                    {
742                        result.dropped_constraints.insert((
743                            self.local.graph.resolve_rename(&edge.dependent).clone(),
744                            cname.clone(),
745                        ));
746                    }
747                }
748                DependencyKind::PartitionOf
749                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current =>
750                {
751                    let resolved_child = self.local.graph.resolve_rename(&edge.dependent).clone();
752                    if !visited.contains(&resolved_child) {
753                        self.walk_cascade(&resolved_child, visited, result);
754                    }
755                }
756                _ => {}
757            }
758        }
759    }
760
761    fn resolve_grant_privileges(
762        spec: &crate::analysis::facts::PrivilegeSpec,
763    ) -> HashSet<Privilege> {
764        match spec {
765            crate::analysis::facts::PrivilegeSpec::All => vec![
766                Privilege::Select,
767                Privilege::Insert,
768                Privilege::Update,
769                Privilege::Delete,
770                Privilege::Truncate,
771                Privilege::References,
772                Privilege::Trigger,
773            ]
774            .into_iter()
775            .collect(),
776            crate::analysis::facts::PrivilegeSpec::List(list) => list
777                .iter()
778                .filter_map(|p| match p {
779                    crate::analysis::facts::PrivilegeFact::Select => Some(Privilege::Select),
780                    crate::analysis::facts::PrivilegeFact::Insert => Some(Privilege::Insert),
781                    crate::analysis::facts::PrivilegeFact::Update => Some(Privilege::Update),
782                    crate::analysis::facts::PrivilegeFact::Delete => Some(Privilege::Delete),
783                    crate::analysis::facts::PrivilegeFact::Truncate => Some(Privilege::Truncate),
784                    crate::analysis::facts::PrivilegeFact::References => {
785                        Some(Privilege::References)
786                    }
787                    crate::analysis::facts::PrivilegeFact::Trigger => Some(Privilege::Trigger),
788                    _ => None,
789                })
790                .collect(),
791        }
792    }
793
794    fn resolve_role_name(
795        role: &crate::analysis::facts::RoleFact,
796        current_role: &str,
797        session_role: &str,
798    ) -> Option<ObjectId> {
799        let name = match role {
800            crate::analysis::facts::RoleFact::Named { name, .. } => Some(name.clone()),
801            crate::analysis::facts::RoleFact::CurrentUser
802            | crate::analysis::facts::RoleFact::CurrentRole => Some(current_role.to_string()),
803            crate::analysis::facts::RoleFact::SessionUser => Some(session_role.to_string()),
804            crate::analysis::facts::RoleFact::Unknown => None,
805        }?;
806        Some(ObjectId::new("", name))
807    }
808
809    fn role_fact_identity(
810        &self,
811        role: &crate::analysis::facts::RoleFact,
812    ) -> Option<(String, bool)> {
813        match role {
814            crate::analysis::facts::RoleFact::Named { name, .. } => Some((name.clone(), true)),
815            crate::analysis::facts::RoleFact::CurrentUser
816            | crate::analysis::facts::RoleFact::CurrentRole => Some((
817                self.local.current_role.clone(),
818                self.local.current_role_known,
819            )),
820            crate::analysis::facts::RoleFact::SessionUser => Some((
821                self.local.session_role.clone(),
822                self.local.session_role_known,
823            )),
824            crate::analysis::facts::RoleFact::Unknown => None,
825        }
826    }
827
828    fn present_role(&self, name: &str) -> Option<&crate::model::role::RoleState> {
829        match self.local.roles.get(&ObjectId::new("", name)) {
830            Some(crate::model::role::RoleOverlay::Present(role)) => Some(role),
831            _ => None,
832        }
833    }
834
835    fn can_set_role_to(&self, target: &str) -> Option<bool> {
836        if !self.local.roles_known || !self.local.session_role_known {
837            return None;
838        }
839        if self.present_role(target).is_none() {
840            return Some(false);
841        }
842        if self.local.session_role == target {
843            return Some(true);
844        }
845        let session = self.present_role(&self.local.session_role)?;
846        if session.is_superuser {
847            return Some(true);
848        }
849
850        let mut pending = session.can_set_role_to.clone();
851        let mut visited = HashSet::new();
852        while let Some(role_id) = pending.pop() {
853            if !visited.insert(role_id.clone()) {
854                continue;
855            }
856            if role_id.name == target {
857                return Some(true);
858            }
859            if let Some(role) = self.present_role(&role_id.name) {
860                pending.extend(role.can_set_role_to.iter().cloned());
861            }
862        }
863        Some(false)
864    }
865
866    fn can_set_session_authorization_to(&self, target: &str) -> Option<bool> {
867        if !self.local.roles_known || !self.local.authenticated_role_known {
868            return None;
869        }
870        if self.present_role(target).is_none() {
871            return Some(false);
872        }
873        if self.local.authenticated_role == target {
874            return Some(true);
875        }
876        Some(
877            self.present_role(&self.local.authenticated_role)
878                .is_some_and(|role| role.is_superuser),
879        )
880    }
881
882    fn schema_is_present(&self, name: &str) -> bool {
883        matches!(
884            self.local.schemas.get(name),
885            Some(SchemaOverlay::Present(_))
886        )
887    }
888
889    fn schema_absence_is_authoritative(&self, name: &str) -> bool {
890        if matches!(self.local.schemas.get(name), Some(SchemaOverlay::Dropped)) {
891            return true;
892        }
893        self.baseline_available
894            && self
895                .baseline_schemas
896                .as_ref()
897                .is_none_or(|schemas| schemas.contains(name))
898    }
899
900    fn refresh_role_sensitive_search_path(&mut self) {
901        let template = self.local.search_path_template.clone();
902        let mut effective = Vec::new();
903        for entry in template {
904            let schema = if entry == "$user" {
905                if self.local.current_role_known {
906                    self.local.current_role.clone()
907                } else {
908                    self.local.confidence = Confidence::Tainted;
909                    continue;
910                }
911            } else {
912                entry
913            };
914            if self.schema_is_present(&schema) {
915                if !effective.contains(&schema) {
916                    effective.push(schema);
917                }
918            } else if !self.schema_absence_is_authoritative(&schema) {
919                self.local.confidence = Confidence::Tainted;
920                if !effective.contains(&schema) {
921                    effective.push(schema);
922                }
923            }
924        }
925        self.local.search_path = effective;
926    }
927
928    fn remap_schema_id(id: &mut ObjectId, old_name: &str, new_name: &str) {
929        if id.schema == old_name {
930            id.schema = new_name.to_string();
931        }
932    }
933
934    fn rename_schema_namespace(&mut self, old_name: &str, new_name: &str) {
935        self.snapshot_namespace();
936
937        let mut aliases = Vec::new();
938        let mut relations = HashMap::new();
939        for (mut id, mut overlay) in std::mem::take(&mut self.local.relations) {
940            let old_id = id.clone();
941            Self::remap_schema_id(&mut id, old_name, new_name);
942            if let RelationOverlay::Present(state) = &mut overlay {
943                Self::remap_schema_id(&mut state.id, old_name, new_name);
944            }
945            if id != old_id {
946                aliases.push((old_id, id.clone()));
947            }
948            relations.insert(id, overlay);
949        }
950        self.local.relations = relations;
951
952        let mut types = HashMap::new();
953        for (mut id, mut overlay) in std::mem::take(&mut self.local.types) {
954            let old_id = id.clone();
955            Self::remap_schema_id(&mut id, old_name, new_name);
956            if let TypeOverlay::Present(state) = &mut overlay {
957                Self::remap_schema_id(&mut state.id, old_name, new_name);
958            }
959            if id != old_id {
960                aliases.push((old_id, id.clone()));
961            }
962            types.insert(id, overlay);
963        }
964        self.local.types = types;
965
966        let mut functions = HashMap::new();
967        for (mut id, mut overlay) in std::mem::take(&mut self.local.functions) {
968            let old_id = id.clone();
969            Self::remap_schema_id(&mut id, old_name, new_name);
970            if let crate::model::function::FunctionOverlay::Present(state) = &mut overlay {
971                Self::remap_schema_id(&mut state.id, old_name, new_name);
972            }
973            if id != old_id {
974                aliases.push((old_id, id.clone()));
975            }
976            functions.insert(id, overlay);
977        }
978        self.local.functions = functions;
979
980        let mut sequences = HashMap::new();
981        for (mut id, mut overlay) in std::mem::take(&mut self.local.sequences) {
982            let old_id = id.clone();
983            Self::remap_schema_id(&mut id, old_name, new_name);
984            if let SequenceOverlay::Present(state) = &mut overlay {
985                Self::remap_schema_id(&mut state.id, old_name, new_name);
986                if let Some((table, _)) = &mut state.owned_by {
987                    Self::remap_schema_id(table, old_name, new_name);
988                }
989            }
990            if id != old_id {
991                aliases.push((old_id, id.clone()));
992            }
993            sequences.insert(id, overlay);
994        }
995        self.local.sequences = sequences;
996
997        let mut triggers = HashMap::new();
998        for (mut id, mut overlay) in std::mem::take(&mut self.local.triggers) {
999            let old_id = id.clone();
1000            Self::remap_schema_id(&mut id, old_name, new_name);
1001            if let TriggerOverlay::Present(state) = &mut overlay {
1002                Self::remap_schema_id(&mut state.id, old_name, new_name);
1003                Self::remap_schema_id(&mut state.table_id, old_name, new_name);
1004            }
1005            if id != old_id {
1006                aliases.push((old_id, id.clone()));
1007            }
1008            triggers.insert(id, overlay);
1009        }
1010        self.local.triggers = triggers;
1011
1012        for overlay in self.local.publications.values_mut() {
1013            let crate::model::replication::PublicationOverlay::Present(publication) = overlay
1014            else {
1015                continue;
1016            };
1017            let crate::analysis::facts::PublicationScope::Explicit(objects) =
1018                &mut publication.scope
1019            else {
1020                continue;
1021            };
1022            for object in objects {
1023                match object {
1024                    crate::analysis::facts::PublicationObjectFact::Table { name, .. } => {
1025                        if name
1026                            .schema
1027                            .as_ref()
1028                            .is_some_and(|schema| schema.resolve() == old_name)
1029                        {
1030                            name.schema = Some(crate::ast::identifiers::Ident::new(new_name, true));
1031                        }
1032                    }
1033                    crate::analysis::facts::PublicationObjectFact::SchemaTables {
1034                        schema, ..
1035                    } if schema == old_name => *schema = new_name.to_string(),
1036                    _ => {}
1037                }
1038            }
1039        }
1040
1041        self.local.constraints = std::mem::take(&mut self.local.constraints)
1042            .into_iter()
1043            .map(|((mut table, name), mut constraint)| {
1044                Self::remap_schema_id(&mut table, old_name, new_name);
1045                Self::remap_schema_id(&mut constraint.table_id, old_name, new_name);
1046                ((table, name), constraint)
1047            })
1048            .collect();
1049        self.local.pending_validation = std::mem::take(&mut self.local.pending_validation)
1050            .into_iter()
1051            .map(|(mut table, name)| {
1052                Self::remap_schema_id(&mut table, old_name, new_name);
1053                (table, name)
1054            })
1055            .collect();
1056
1057        for edge in &mut self.local.graph.edges {
1058            Self::remap_schema_id(&mut edge.dependent, old_name, new_name);
1059            Self::remap_schema_id(&mut edge.referenced, old_name, new_name);
1060            if let DependencyKind::TriggerOnTable {
1061                trigger_id,
1062                function_id,
1063            } = &mut edge.kind
1064            {
1065                Self::remap_schema_id(trigger_id, old_name, new_name);
1066                Self::remap_schema_id(function_id, old_name, new_name);
1067            }
1068        }
1069        for (old_id, new_id) in aliases {
1070            self.local.graph.edges.push(DependencyEdge::new(
1071                old_id,
1072                new_id,
1073                DependencyKind::RenameTo,
1074            ));
1075        }
1076
1077        let remap_set = |set: &mut HashSet<ObjectId>| {
1078            *set = std::mem::take(set)
1079                .into_iter()
1080                .map(|mut id| {
1081                    Self::remap_schema_id(&mut id, old_name, new_name);
1082                    id
1083                })
1084                .collect();
1085        };
1086        remap_set(&mut self.baseline_relations);
1087        remap_set(&mut self.baseline_indexes);
1088        remap_set(&mut self.baseline_fk_dependencies);
1089        remap_set(&mut self.baseline_sequences);
1090        self.baseline_foreign_keys = std::mem::take(&mut self.baseline_foreign_keys)
1091            .into_iter()
1092            .map(|(mut table, name)| {
1093                Self::remap_schema_id(&mut table, old_name, new_name);
1094                (table, name)
1095            })
1096            .collect();
1097
1098        if let Some(SchemaOverlay::Present(mut schema)) = self.local.schemas.remove(old_name) {
1099            schema.name = new_name.to_string();
1100            self.local
1101                .schemas
1102                .insert(new_name.to_string(), SchemaOverlay::Present(schema));
1103        }
1104        self.refresh_role_sensitive_search_path();
1105    }
1106
1107    fn restore_persistent_role_context(&mut self) {
1108        self.local.current_role = self.local.persistent_current_role.clone();
1109        self.local.current_role_known = self.local.persistent_current_role_known;
1110        self.local.session_role = self.local.persistent_session_role.clone();
1111        self.local.session_role_known = self.local.persistent_session_role_known;
1112        self.refresh_role_sensitive_search_path();
1113    }
1114
1115    fn apply_grant_to_relation(
1116        &mut self,
1117        id: &ObjectId,
1118        privileges: &HashSet<Privilege>,
1119        grantees: &[crate::analysis::facts::RoleFact],
1120    ) {
1121        self.snapshot_relation(id);
1122        if let Some(RelationOverlay::Present(rel)) = self.local.relations.get_mut(id) {
1123            for grantee in grantees {
1124                if let Some(role_id) = Self::resolve_role_name(
1125                    grantee,
1126                    &self.local.current_role,
1127                    &self.local.session_role,
1128                ) {
1129                    rel.privileges.grant(role_id, privileges.clone());
1130                }
1131            }
1132        }
1133    }
1134
1135    fn apply_revoke_to_relation(
1136        &mut self,
1137        id: &ObjectId,
1138        privileges: &HashSet<Privilege>,
1139        revokees: &[crate::analysis::facts::RoleFact],
1140    ) {
1141        self.snapshot_relation(id);
1142        if let Some(RelationOverlay::Present(rel)) = self.local.relations.get_mut(id) {
1143            for revokee in revokees {
1144                if let Some(role_id) = Self::resolve_role_name(
1145                    revokee,
1146                    &self.local.current_role,
1147                    &self.local.session_role,
1148                ) {
1149                    rel.privileges.revoke(&role_id, privileges);
1150                }
1151            }
1152        }
1153    }
1154
1155    pub fn apply(
1156        &mut self,
1157        mutation: &Mutation,
1158        precomputed_cascade: Option<&CascadeResult>,
1159    ) -> MutationResult {
1160        if self.local.transaction_aborted
1161            && !matches!(
1162                mutation,
1163                Mutation::CommitTransaction
1164                    | Mutation::CommitAndChain
1165                    | Mutation::RollbackTransaction
1166                    | Mutation::RollbackAndChain
1167                    | Mutation::RollbackToSavepoint(_)
1168            )
1169        {
1170            return MutationResult::NotExecuted;
1171        }
1172
1173        let result = self.apply_inner(mutation, precomputed_cascade);
1174        if matches!(result, MutationResult::Conflict { .. }) && !self.local.transactions.is_empty()
1175        {
1176            self.local.transaction_aborted = true;
1177        }
1178        result
1179    }
1180
1181    fn apply_inner(
1182        &mut self,
1183        mutation: &Mutation,
1184        precomputed_cascade: Option<&CascadeResult>,
1185    ) -> MutationResult {
1186        match mutation {
1187            Mutation::CreateSchema(create_schema) => {
1188                if self.schema_is_present(&create_schema.name) {
1189                    return if create_schema.if_not_exists {
1190                        MutationResult::Skipped
1191                    } else {
1192                        MutationResult::Conflict {
1193                            reason: format!("schema '{}' already exists", create_schema.name),
1194                        }
1195                    };
1196                }
1197                let (owner_name, owner_known) = match &create_schema.authorization {
1198                    Some(role) => match self.role_fact_identity(role) {
1199                        Some(identity) => identity,
1200                        None => {
1201                            self.snapshot_confidence();
1202                            self.local.confidence = Confidence::Tainted;
1203                            (self.local.current_role.clone(), false)
1204                        }
1205                    },
1206                    None => (
1207                        self.local.current_role.clone(),
1208                        self.local.current_role_known,
1209                    ),
1210                };
1211                if owner_known && self.local.roles_known && self.present_role(&owner_name).is_none()
1212                {
1213                    return MutationResult::Conflict {
1214                        reason: format!("role '{}' does not exist", owner_name),
1215                    };
1216                }
1217                if !owner_known || !self.local.roles_known {
1218                    self.snapshot_confidence();
1219                    self.local.confidence = Confidence::Tainted;
1220                }
1221                self.snapshot_generation_counter();
1222                self.local.generation_counter += 1;
1223                let generation = self.local.generation_counter;
1224                self.snapshot_schema(&create_schema.name);
1225                self.local.schemas.insert(
1226                    create_schema.name.clone(),
1227                    SchemaOverlay::Present(crate::model::schema::SchemaState {
1228                        name: create_schema.name.clone(),
1229                        owner: ObjectId::new("", owner_name),
1230                        generation,
1231                    }),
1232                );
1233                self.snapshot_search_path();
1234                self.refresh_role_sensitive_search_path();
1235                MutationResult::Applied
1236            }
1237            Mutation::AlterSchema(alter_schema) => match alter_schema {
1238                crate::analysis::mutations::AlterSchemaMutation::OwnerTo { name, new_owner } => {
1239                    if !self.schema_is_present(name) {
1240                        if self.schema_absence_is_authoritative(name) {
1241                            return MutationResult::Conflict {
1242                                reason: format!("schema '{}' does not exist", name),
1243                            };
1244                        }
1245                        self.snapshot_confidence();
1246                        self.local.confidence = Confidence::Tainted;
1247                        return MutationResult::Skipped;
1248                    }
1249                    let Some((owner_name, owner_known)) = self.role_fact_identity(new_owner) else {
1250                        self.snapshot_confidence();
1251                        self.local.confidence = Confidence::Tainted;
1252                        return MutationResult::Skipped;
1253                    };
1254                    if owner_known
1255                        && self.local.roles_known
1256                        && self.present_role(&owner_name).is_none()
1257                    {
1258                        return MutationResult::Conflict {
1259                            reason: format!("role '{}' does not exist", owner_name),
1260                        };
1261                    }
1262                    if !owner_known || !self.local.roles_known {
1263                        self.snapshot_confidence();
1264                        self.local.confidence = Confidence::Tainted;
1265                    }
1266                    self.snapshot_schema(name);
1267                    if let Some(SchemaOverlay::Present(schema)) = self.local.schemas.get_mut(name) {
1268                        schema.owner = ObjectId::new("", owner_name);
1269                    }
1270                    MutationResult::Applied
1271                }
1272                crate::analysis::mutations::AlterSchemaMutation::Rename { old_name, new_name } => {
1273                    if !self.schema_is_present(old_name) {
1274                        if !self.schema_absence_is_authoritative(old_name) {
1275                            self.snapshot_confidence();
1276                            self.local.confidence = Confidence::Tainted;
1277                            return MutationResult::Skipped;
1278                        }
1279                        return MutationResult::Conflict {
1280                            reason: format!("schema '{}' does not exist", old_name),
1281                        };
1282                    }
1283                    if self.schema_is_present(new_name) {
1284                        return MutationResult::Conflict {
1285                            reason: format!("schema '{}' already exists", new_name),
1286                        };
1287                    }
1288                    if !self.schema_absence_is_authoritative(new_name) {
1289                        self.snapshot_confidence();
1290                        self.local.confidence = Confidence::Tainted;
1291                    }
1292                    self.snapshot_search_path();
1293                    self.rename_schema_namespace(old_name, new_name);
1294                    MutationResult::Applied
1295                }
1296            },
1297            Mutation::DropSchema(drop_schema) => {
1298                for name in &drop_schema.names {
1299                    if !self.schema_is_present(name) && self.schema_absence_is_authoritative(name) {
1300                        if !drop_schema.if_exists {
1301                            return MutationResult::Conflict {
1302                                reason: format!("schema '{}' does not exist", name),
1303                            };
1304                        }
1305                    } else if !self.schema_is_present(name) {
1306                        self.snapshot_confidence();
1307                        self.local.confidence = Confidence::Tainted;
1308                    }
1309                }
1310                let present_names: Vec<String> = drop_schema
1311                    .names
1312                    .iter()
1313                    .filter(|name| self.schema_is_present(name))
1314                    .cloned()
1315                    .collect();
1316                if present_names.is_empty() {
1317                    return MutationResult::Skipped;
1318                }
1319                if drop_schema.cascade {
1320                    self.snapshot_namespace();
1321                    let mut relations_to_drop = Vec::new();
1322                    for id in self.local.relations.keys() {
1323                        if drop_schema.names.contains(&id.schema) {
1324                            relations_to_drop.push(id.clone());
1325                        }
1326                    }
1327                    for id in relations_to_drop {
1328                        self.snapshot_relation(&id);
1329                        self.local.relations.insert(id, RelationOverlay::Dropped);
1330                    }
1331
1332                    let constraints_to_drop: Vec<(ObjectId, String)> = self
1333                        .local
1334                        .constraints
1335                        .keys()
1336                        .filter(|(table_id, _)| drop_schema.names.contains(&table_id.schema))
1337                        .cloned()
1338                        .collect();
1339                    for (table_id, name) in constraints_to_drop {
1340                        self.snapshot_constraint(&table_id, &name);
1341                        self.local.constraints.remove(&(table_id, name));
1342                    }
1343
1344                    let mut types_to_drop = Vec::new();
1345                    for id in self.local.types.keys() {
1346                        if drop_schema.names.contains(&id.schema) {
1347                            types_to_drop.push(id.clone());
1348                        }
1349                    }
1350                    for id in types_to_drop {
1351                        self.snapshot_type(&id);
1352                        self.local.types.insert(id, TypeOverlay::Dropped);
1353                    }
1354
1355                    let mut seqs_to_drop = Vec::new();
1356                    for id in self.local.sequences.keys() {
1357                        if drop_schema.names.contains(&id.schema) {
1358                            seqs_to_drop.push(id.clone());
1359                        }
1360                    }
1361                    for id in seqs_to_drop {
1362                        self.snapshot_sequence(&id);
1363                        self.local.sequences.insert(id, SequenceOverlay::Dropped);
1364                    }
1365
1366                    let functions_to_drop: Vec<ObjectId> = self
1367                        .local
1368                        .functions
1369                        .keys()
1370                        .filter(|id| drop_schema.names.contains(&id.schema))
1371                        .cloned()
1372                        .collect();
1373                    for id in functions_to_drop {
1374                        self.snapshot_function(&id);
1375                        self.local
1376                            .functions
1377                            .insert(id, crate::model::function::FunctionOverlay::Dropped);
1378                    }
1379
1380                    let triggers_to_drop: Vec<ObjectId> = self
1381                        .local
1382                        .triggers
1383                        .keys()
1384                        .filter(|id| drop_schema.names.contains(&id.schema))
1385                        .cloned()
1386                        .collect();
1387                    for id in triggers_to_drop {
1388                        self.snapshot_trigger(&id);
1389                        self.local.triggers.insert(id, TriggerOverlay::Dropped);
1390                    }
1391
1392                    self.local
1393                        .pending_validation
1394                        .retain(|(table, _)| !drop_schema.names.contains(&table.schema));
1395                    for overlay in self.local.publications.values_mut() {
1396                        let crate::model::replication::PublicationOverlay::Present(publication) =
1397                            overlay
1398                        else {
1399                            continue;
1400                        };
1401                        let crate::analysis::facts::PublicationScope::Explicit(objects) =
1402                            &mut publication.scope
1403                        else {
1404                            continue;
1405                        };
1406                        objects.retain(|object| match object {
1407                            crate::analysis::facts::PublicationObjectFact::Table {
1408                                name, ..
1409                            } => name.schema.as_ref().is_none_or(|schema| {
1410                                !drop_schema.names.contains(&schema.resolve())
1411                            }),
1412                            crate::analysis::facts::PublicationObjectFact::SchemaTables {
1413                                schema,
1414                                ..
1415                            } => !drop_schema.names.contains(schema),
1416                            _ => true,
1417                        });
1418                    }
1419
1420                    self.snapshot_graph_full();
1421
1422                    let g = &mut self.local.graph;
1423                    g.edges.retain(|e| {
1424                        !drop_schema.names.contains(&e.dependent.schema)
1425                            && !drop_schema.names.contains(&e.referenced.schema)
1426                            && match &e.kind {
1427                                DependencyKind::TriggerOnTable { function_id, .. } => {
1428                                    !drop_schema.names.contains(&function_id.schema)
1429                                }
1430                                _ => true,
1431                            }
1432                    });
1433                } else {
1434                    // Non-cascade: fail if any objects in the schema still exist
1435                    let has_relation = self.local.relations.iter().any(|(id, ov)| {
1436                        drop_schema.names.contains(&id.schema)
1437                            && !matches!(ov, RelationOverlay::Dropped)
1438                    });
1439                    let has_type = self.local.types.iter().any(|(id, ov)| {
1440                        drop_schema.names.contains(&id.schema)
1441                            && !matches!(ov, TypeOverlay::Dropped)
1442                    });
1443                    let has_sequence = self.local.sequences.iter().any(|(id, ov)| {
1444                        drop_schema.names.contains(&id.schema)
1445                            && !matches!(ov, SequenceOverlay::Dropped)
1446                    });
1447                    let has_function = self.local.functions.iter().any(|(id, ov)| {
1448                        drop_schema.names.contains(&id.schema)
1449                            && !matches!(ov, crate::model::function::FunctionOverlay::Dropped)
1450                    });
1451                    let has_trigger = self.local.triggers.iter().any(|(id, ov)| {
1452                        drop_schema.names.contains(&id.schema)
1453                            && !matches!(ov, TriggerOverlay::Dropped)
1454                    });
1455                    if has_relation || has_type || has_sequence || has_function || has_trigger {
1456                        return MutationResult::Conflict {
1457                            reason: format!(
1458                                "schema(s) {:?} still contain objects; use CASCADE to drop them",
1459                                drop_schema.names
1460                            ),
1461                        };
1462                    }
1463                }
1464                for name in present_names {
1465                    self.snapshot_schema(&name);
1466                    self.local.schemas.insert(name, SchemaOverlay::Dropped);
1467                }
1468                self.snapshot_search_path();
1469                self.refresh_role_sensitive_search_path();
1470                MutationResult::Applied
1471            }
1472            Mutation::DropTable(drop_table) => {
1473                if !self.relation_is_present(&drop_table.id) {
1474                    if drop_table.if_exists {
1475                        return MutationResult::Skipped;
1476                    } else {
1477                        self.local.confidence = Confidence::Tainted;
1478                        return MutationResult::Skipped;
1479                    }
1480                }
1481
1482                let renames: Vec<DependencyEdge> = self
1483                    .local
1484                    .graph
1485                    .edges
1486                    .iter()
1487                    .filter(|e| matches!(e.kind, DependencyKind::RenameTo))
1488                    .cloned()
1489                    .collect();
1490                let resolve = |id: &ObjectId| -> ObjectId {
1491                    let mut current = id;
1492                    let mut visited = HashSet::new();
1493                    loop {
1494                        if !visited.insert(current.clone()) {
1495                            return id.clone();
1496                        }
1497                        match renames.iter().find(|r| &r.dependent == current) {
1498                            Some(edge) => current = &edge.referenced,
1499                            None => return current.clone(),
1500                        }
1501                    }
1502                };
1503
1504                let resolved_drop = resolve(&drop_table.id);
1505                let mut dropped_relations = HashSet::from([resolved_drop.clone()]);
1506
1507                if drop_table.cascade {
1508                    let local_closure;
1509                    let closure = match precomputed_cascade {
1510                        Some(c) => c,
1511                        None => {
1512                            local_closure = self.get_cascade_closure(&drop_table.id);
1513                            &local_closure
1514                        }
1515                    };
1516                    dropped_relations = closure.dropped_relations.clone();
1517
1518                    for dropped_rel_id in &closure.dropped_relations {
1519                        self.snapshot_relation(dropped_rel_id);
1520                        self.local
1521                            .relations
1522                            .insert(dropped_rel_id.clone(), RelationOverlay::Dropped);
1523                    }
1524
1525                    self.snapshot_graph_full();
1526                    self.local.graph.edges.retain(|e| match &e.kind {
1527                        DependencyKind::IndexOnRelation { .. } => {
1528                            !closure.dropped_indexes.contains(&resolve(&e.dependent))
1529                        }
1530                        DependencyKind::ForeignKey {
1531                            constraint_name, ..
1532                        } => {
1533                            let from_dropped =
1534                                closure.dropped_relations.contains(&resolve(&e.dependent));
1535                            let to_dropped =
1536                                closure.dropped_relations.contains(&resolve(&e.referenced));
1537                            let constraint_explicitly_dropped = if let Some(cname) = constraint_name
1538                            {
1539                                closure
1540                                    .dropped_constraints
1541                                    .contains(&(resolve(&e.dependent), cname.clone()))
1542                            } else {
1543                                false
1544                            };
1545                            !(from_dropped || to_dropped || constraint_explicitly_dropped)
1546                        }
1547                        DependencyKind::ViewDependency { .. } => {
1548                            !closure.dropped_relations.contains(&resolve(&e.dependent))
1549                        }
1550                        DependencyKind::SequenceOwnedBy { .. } => {
1551                            !closure.dropped_relations.contains(&resolve(&e.referenced))
1552                        }
1553                        _ => true,
1554                    });
1555                } else {
1556                    let has_view_deps = self.local.graph.edges.iter().any(|e| {
1557                        matches!(e.kind, DependencyKind::ViewDependency { .. })
1558                            && resolve(&e.referenced) == resolved_drop
1559                    });
1560                    let has_fk_deps = self.local.graph.edges.iter().any(|e| {
1561                        matches!(e.kind, DependencyKind::ForeignKey { .. })
1562                            && resolve(&e.referenced) == resolved_drop
1563                            && resolve(&e.dependent) != resolved_drop
1564                    });
1565                    let has_partition_deps = self.local.graph.edges.iter().any(|e| {
1566                        matches!(e.kind, DependencyKind::PartitionOf)
1567                            && resolve(&e.referenced) == resolved_drop
1568                    });
1569
1570                    if has_view_deps || has_fk_deps || has_partition_deps {
1571                        return MutationResult::Conflict {
1572                            reason: format!(
1573                                "relation '{}' still has dependent objects; use CASCADE",
1574                                drop_table.id
1575                            ),
1576                        };
1577                    }
1578
1579                    self.snapshot_relation(&drop_table.id);
1580                    self.local
1581                        .relations
1582                        .insert(drop_table.id.clone(), RelationOverlay::Dropped);
1583
1584                    self.snapshot_graph_full();
1585                    self.local.graph.edges.retain(|e| {
1586                        !(matches!(e.kind, DependencyKind::SequenceOwnedBy { .. })
1587                            && resolve(&e.referenced) == resolved_drop)
1588                    });
1589                }
1590
1591                let owned_sequences_to_drop: Vec<ObjectId> = self
1592                    .local
1593                    .sequences
1594                    .iter()
1595                    .filter_map(|(id, overlay)| match overlay {
1596                        SequenceOverlay::Present(sequence)
1597                            if sequence.owned_by.as_ref().is_some_and(|(table, _)| {
1598                                dropped_relations.contains(&resolve(table))
1599                            }) =>
1600                        {
1601                            Some(id.clone())
1602                        }
1603                        _ => None,
1604                    })
1605                    .collect();
1606                for sequence_id in owned_sequences_to_drop {
1607                    self.snapshot_sequence(&sequence_id);
1608                    self.local
1609                        .sequences
1610                        .insert(sequence_id, SequenceOverlay::Dropped);
1611                }
1612
1613                let constraints_to_drop: Vec<(ObjectId, String)> = self
1614                    .local
1615                    .constraints
1616                    .keys()
1617                    .filter(|(table_id, _)| dropped_relations.contains(&resolve(table_id)))
1618                    .cloned()
1619                    .collect();
1620                for (table_id, name) in constraints_to_drop {
1621                    self.snapshot_constraint(&table_id, &name);
1622                    self.local.constraints.remove(&(table_id, name));
1623                }
1624
1625                let triggers_to_drop: Vec<ObjectId> = self
1626                    .local
1627                    .triggers
1628                    .iter()
1629                    .filter_map(|(id, overlay)| {
1630                        let TriggerOverlay::Present(trigger) = overlay else {
1631                            return None;
1632                        };
1633                        let graph_matches = self.local.graph.edges.iter().any(|edge| {
1634                            matches!(edge.kind, DependencyKind::TriggerOnTable { .. })
1635                                && edge.dependent == *id
1636                                && dropped_relations.contains(&resolve(&edge.referenced))
1637                        });
1638                        (dropped_relations.contains(&resolve(&trigger.table_id)) || graph_matches)
1639                            .then(|| id.clone())
1640                    })
1641                    .collect();
1642                for trigger_id in triggers_to_drop {
1643                    self.snapshot_trigger(&trigger_id);
1644                    self.local
1645                        .triggers
1646                        .insert(trigger_id, TriggerOverlay::Dropped);
1647                }
1648
1649                // PostgreSQL drops triggers only after the table drop succeeds.
1650                self.snapshot_graph_full();
1651                self.local.graph.edges.retain(|e| {
1652                    !(matches!(e.kind, DependencyKind::TriggerOnTable { .. })
1653                        && dropped_relations.contains(&resolve(&e.referenced)))
1654                });
1655
1656                self.snapshot_graph_full();
1657                self.local.graph.edges.retain(|e| {
1658                    if let DependencyKind::PartitionOf = e.kind {
1659                        resolve(&e.referenced) != resolved_drop
1660                            && resolve(&e.dependent) != resolved_drop
1661                    } else {
1662                        true
1663                    }
1664                });
1665
1666                MutationResult::Applied
1667            }
1668            Mutation::CreateTable(create) => {
1669                if create.if_not_exists && self.relation_namespace_is_taken(&create.id) {
1670                    return MutationResult::Skipped;
1671                }
1672                if self.relation_namespace_is_taken(&create.id) {
1673                    return MutationResult::Conflict {
1674                        reason: format!("relation '{}' already exists", create.id),
1675                    };
1676                }
1677
1678                // PostgreSQL chooses all implicit sequence names before the
1679                // table becomes visible. Reserve them up front so a collision
1680                // or malformed statement cannot leave partial local state.
1681                let mut reserved_sequences = HashSet::new();
1682                let mut implicit_sequences = Vec::new();
1683                for column in &create.columns {
1684                    let kind = match column.generation {
1685                        crate::analysis::facts::ColumnGeneration::Serial => {
1686                            Some(SequenceKind::SerialLike)
1687                        }
1688                        crate::analysis::facts::ColumnGeneration::Identity => {
1689                            Some(SequenceKind::Identity)
1690                        }
1691                        crate::analysis::facts::ColumnGeneration::Ordinary => None,
1692                    };
1693                    if let Some(kind) = kind {
1694                        let sequence_id = self.next_implicit_sequence_id(
1695                            &create.id,
1696                            &column.name,
1697                            &reserved_sequences,
1698                        );
1699                        reserved_sequences.insert(sequence_id.clone());
1700                        implicit_sequences.push((sequence_id, column.name.clone(), kind));
1701                    }
1702                }
1703
1704                self.snapshot_relation(&create.id);
1705
1706                self.snapshot_generation_counter();
1707                self.local.generation_counter += 1;
1708                let generation = self.local.generation_counter;
1709
1710                let resolved_persistence = match create.persistence {
1711                    PersistenceMutation::Permanent => {
1712                        crate::model::relation::Persistence::Permanent
1713                    }
1714                    PersistenceMutation::Temporary => {
1715                        crate::model::relation::Persistence::Temporary
1716                    }
1717                    PersistenceMutation::Unlogged => crate::model::relation::Persistence::Unlogged,
1718                };
1719
1720                let mut rel_state = RelationState::new(
1721                    create.id.clone(),
1722                    ObjectId::new("", &self.local.current_role),
1723                    generation,
1724                    if create.as_select { None } else { Some(0) },
1725                    RelationKind::Table,
1726                    resolved_persistence,
1727                    self.local.transactions.len(),
1728                );
1729
1730                // Store partition strategy information
1731                rel_state.partition_type = create
1732                    .partition_by
1733                    .as_ref()
1734                    .and_then(|pb| pb.split_whitespace().nth(2).map(|s| s.to_uppercase()))
1735                    .or_else(|| {
1736                        create.partition_of.as_ref().and_then(|parent_id| {
1737                            self.local.relations.get(parent_id).and_then(|r| {
1738                                if let RelationOverlay::Present(rel) = r {
1739                                    rel.partition_type.clone()
1740                                } else {
1741                                    None
1742                                }
1743                            })
1744                        })
1745                    });
1746                rel_state.partition_by = create.partition_by.clone();
1747
1748                let pk_columns: HashSet<&str> = create
1749                    .table_constraints
1750                    .iter()
1751                    .filter_map(|tc| {
1752                        if let TableConstraintFact::PrimaryKey { columns, .. } = tc {
1753                            Some(columns.iter().map(|s| s.as_str()))
1754                        } else {
1755                            None
1756                        }
1757                    })
1758                    .flatten()
1759                    .collect();
1760
1761                for col in &create.columns {
1762                    let is_pk = col.is_primary_key || pk_columns.contains(col.name.as_str());
1763                    rel_state.apply_column_action(&ColumnAction::Add {
1764                        name: col.name.clone(),
1765                        data_type: col.ty.clone(),
1766                        not_null: col.not_null || is_pk,
1767                        default: col.default.clone(),
1768                    });
1769                }
1770
1771                for (sequence_id, column_name, _) in &implicit_sequences {
1772                    if let Some(column) = rel_state
1773                        .columns
1774                        .iter_mut()
1775                        .find(|column| column.name == *column_name)
1776                    {
1777                        column.default = Some(Self::sequence_nextval_default(sequence_id));
1778                        column.default_expr_text = Some(format!(
1779                            "nextval('{}.{}'::regclass)",
1780                            sequence_id.schema, sequence_id.name
1781                        ));
1782                        column.is_nullable = false;
1783                    }
1784                }
1785
1786                self.local
1787                    .relations
1788                    .insert(create.id.clone(), RelationOverlay::Present(rel_state));
1789
1790                for (sequence_id, column_name, kind) in implicit_sequences {
1791                    self.snapshot_sequence(&sequence_id);
1792                    self.snapshot_generation_counter();
1793                    self.local.generation_counter += 1;
1794                    self.local.sequences.insert(
1795                        sequence_id.clone(),
1796                        SequenceOverlay::Present(SequenceState {
1797                            id: sequence_id.clone(),
1798                            owner: ObjectId::new("", &self.local.current_role),
1799                            owned_by: Some((create.id.clone(), column_name.clone())),
1800                            kind,
1801                            generation: self.local.generation_counter,
1802                        }),
1803                    );
1804                    self.snapshot_graph();
1805                    self.local.graph.edges.push(DependencyEdge::new(
1806                        sequence_id,
1807                        create.id.clone(),
1808                        DependencyKind::SequenceOwnedBy {
1809                            column: column_name,
1810                        },
1811                    ));
1812                }
1813
1814                let primary_key_name = create
1815                    .columns
1816                    .iter()
1817                    .find(|column| column.is_primary_key)
1818                    .map(|column| column.primary_key_constraint_name.clone())
1819                    .or_else(|| {
1820                        create.table_constraints.iter().find_map(|constraint| {
1821                            if let TableConstraintFact::PrimaryKey {
1822                                constraint_name, ..
1823                            } = constraint
1824                            {
1825                                Some(constraint_name.clone())
1826                            } else {
1827                                None
1828                            }
1829                        })
1830                    });
1831                if let Some(explicit_name) = primary_key_name {
1832                    let name = explicit_name.unwrap_or_else(|| {
1833                        self.next_generated_constraint_name(
1834                            &create.id,
1835                            &create.id.name,
1836                            None,
1837                            "pkey",
1838                        )
1839                    });
1840                    self.snapshot_constraint(&create.id, &name);
1841                    self.local.constraints.insert(
1842                        (create.id.clone(), name.clone()),
1843                        ConstraintState {
1844                            table_id: create.id.clone(),
1845                            name,
1846                            kind: ConstraintKind::PrimaryKey,
1847                            validated: true,
1848                        },
1849                    );
1850                }
1851
1852                let unique_constraints = create
1853                    .columns
1854                    .iter()
1855                    .filter(|column| column.is_unique)
1856                    .map(|column| {
1857                        (
1858                            column.unique_constraint_name.as_ref(),
1859                            vec![column.name.as_str()],
1860                        )
1861                    })
1862                    .chain(create.table_constraints.iter().filter_map(|constraint| {
1863                        if let TableConstraintFact::Unique {
1864                            constraint_name,
1865                            columns,
1866                        } = constraint
1867                        {
1868                            Some((
1869                                constraint_name.as_ref(),
1870                                columns.iter().map(String::as_str).collect(),
1871                            ))
1872                        } else {
1873                            None
1874                        }
1875                    }))
1876                    .collect::<Vec<_>>();
1877                for (explicit_name, columns) in unique_constraints {
1878                    let name = explicit_name.cloned().unwrap_or_else(|| {
1879                        self.next_generated_constraint_name(
1880                            &create.id,
1881                            &create.id.name,
1882                            Some(&columns.join("_")),
1883                            "key",
1884                        )
1885                    });
1886                    self.snapshot_constraint(&create.id, &name);
1887                    self.local.constraints.insert(
1888                        (create.id.clone(), name.clone()),
1889                        ConstraintState {
1890                            table_id: create.id.clone(),
1891                            name,
1892                            kind: ConstraintKind::Unique,
1893                            validated: true,
1894                        },
1895                    );
1896                }
1897
1898                if let Some(parent_id) = &create.partition_of {
1899                    self.snapshot_graph();
1900                    self.local.graph.edges.push(DependencyEdge::new(
1901                        create.id.clone(),
1902                        parent_id.clone(),
1903                        DependencyKind::PartitionOf,
1904                    ));
1905                }
1906
1907                if !create.foreign_keys.is_empty() {
1908                    self.snapshot_graph();
1909                }
1910
1911                for fk in &create.foreign_keys {
1912                    self.local.graph.edges.push(DependencyEdge::new(
1913                        create.id.clone(),
1914                        fk.to_table.clone(),
1915                        DependencyKind::ForeignKey {
1916                            constraint_name: fk.constraint_name.clone(),
1917                            from_columns: fk.from_columns.clone(),
1918                            to_columns: fk.to_columns.clone(),
1919                            from_generation: generation,
1920                        },
1921                    ));
1922                }
1923                MutationResult::Applied
1924            }
1925            Mutation::CreateView(create_view) => {
1926                if self.relation_namespace_is_taken(&create_view.id) {
1927                    let is_replaceable_view = matches!(
1928                        self.local.relations.get(&create_view.id),
1929                        Some(RelationOverlay::Present(relation))
1930                            if relation.kind == RelationKind::View
1931                    );
1932                    if !create_view.or_replace || !is_replaceable_view {
1933                        return MutationResult::Conflict {
1934                            reason: format!("relation '{}' already exists", create_view.id),
1935                        };
1936                    }
1937                }
1938                self.snapshot_relation(&create_view.id);
1939                self.snapshot_generation_counter();
1940                self.local.generation_counter += 1;
1941                let generation = self.local.generation_counter;
1942
1943                self.local.relations.insert(
1944                    create_view.id.clone(),
1945                    RelationOverlay::Present(RelationState::new(
1946                        create_view.id.clone(),
1947                        ObjectId::new("", &self.local.current_role),
1948                        generation,
1949                        None,
1950                        RelationKind::View,
1951                        Persistence::Permanent,
1952                        self.local.transactions.len(),
1953                    )),
1954                );
1955
1956                self.snapshot_graph();
1957                for dep in &create_view.depends_on {
1958                    self.local.graph.edges.push(DependencyEdge::new(
1959                        create_view.id.clone(),
1960                        dep.clone(),
1961                        DependencyKind::ViewDependency {
1962                            view_generation: generation,
1963                        },
1964                    ));
1965                }
1966                MutationResult::Applied
1967            }
1968            Mutation::CreateMaterializedView(create_mv) => {
1969                if self.relation_namespace_is_taken(&create_mv.id) {
1970                    return MutationResult::Conflict {
1971                        reason: format!("relation '{}' already exists", create_mv.id),
1972                    };
1973                }
1974                self.snapshot_relation(&create_mv.id);
1975                self.snapshot_generation_counter();
1976                self.local.generation_counter += 1;
1977                let generation = self.local.generation_counter;
1978
1979                self.local.relations.insert(
1980                    create_mv.id.clone(),
1981                    RelationOverlay::Present(RelationState::new(
1982                        create_mv.id.clone(),
1983                        ObjectId::new("", &self.local.current_role),
1984                        generation,
1985                        None,
1986                        RelationKind::MaterializedView,
1987                        Persistence::Permanent,
1988                        self.local.transactions.len(),
1989                    )),
1990                );
1991
1992                self.snapshot_graph();
1993                for dep in &create_mv.depends_on {
1994                    self.local.graph.edges.push(DependencyEdge::new(
1995                        create_mv.id.clone(),
1996                        dep.clone(),
1997                        DependencyKind::ViewDependency {
1998                            view_generation: generation,
1999                        },
2000                    ));
2001                }
2002                MutationResult::Applied
2003            }
2004            Mutation::RefreshMaterializedView(_) => MutationResult::Applied,
2005            Mutation::CreateIndex(create_idx) => {
2006                let exists = self.index_is_present(&create_idx.id);
2007                if create_idx.if_not_exists && exists {
2008                    return MutationResult::Skipped;
2009                }
2010                if self.relation_namespace_is_taken(&create_idx.id) {
2011                    return MutationResult::Conflict {
2012                        reason: format!("relation '{}' already exists", create_idx.id),
2013                    };
2014                }
2015                self.snapshot_graph();
2016                self.local.graph.edges.push(DependencyEdge::new(
2017                    create_idx.id.clone(),
2018                    create_idx.table.clone(),
2019                    DependencyKind::IndexOnRelation {
2020                        using_method: create_idx.using_method.clone(),
2021                        has_predicate: create_idx.has_predicate,
2022                        is_concurrent: create_idx.concurrently,
2023                        is_unique: create_idx.unique,
2024                        eligibility_known: true,
2025                    },
2026                ));
2027                MutationResult::Applied
2028            }
2029            Mutation::CreatePolicy(create_policy) => {
2030                self.snapshot_relation(&create_policy.table);
2031                if let Some(RelationOverlay::Present(rel)) =
2032                    self.local.relations.get_mut(&create_policy.table)
2033                {
2034                    if rel.policies.contains(&create_policy.name) {
2035                        return MutationResult::Conflict {
2036                            reason: format!(
2037                                "policy '{}' already exists on relation '{}'",
2038                                create_policy.name, create_policy.table
2039                            ),
2040                        };
2041                    }
2042                    rel.policies.insert(create_policy.name.clone());
2043                } else {
2044                    return MutationResult::Conflict {
2045                        reason: format!("relation '{}' does not exist", create_policy.table),
2046                    };
2047                }
2048                MutationResult::Applied
2049            }
2050            Mutation::DropPolicy(drop_policy) => {
2051                self.snapshot_relation(&drop_policy.table);
2052                if let Some(RelationOverlay::Present(rel)) =
2053                    self.local.relations.get_mut(&drop_policy.table)
2054                {
2055                    if !rel.policies.contains(&drop_policy.name) {
2056                        return if drop_policy.if_exists {
2057                            MutationResult::Skipped
2058                        } else {
2059                            MutationResult::Conflict {
2060                                reason: format!(
2061                                    "policy '{}' does not exist on relation '{}'",
2062                                    drop_policy.name, drop_policy.table
2063                                ),
2064                            }
2065                        };
2066                    }
2067                    rel.policies.remove(&drop_policy.name);
2068                } else {
2069                    return MutationResult::Conflict {
2070                        reason: format!("relation '{}' does not exist", drop_policy.table),
2071                    };
2072                }
2073                MutationResult::Applied
2074            }
2075            Mutation::CreateTrigger(create_trigger) => {
2076                let trigger_id = Self::trigger_key(&create_trigger.table, &create_trigger.name);
2077                if matches!(
2078                    self.local.triggers.get(&trigger_id),
2079                    Some(TriggerOverlay::Present(_))
2080                ) {
2081                    return MutationResult::Conflict {
2082                        reason: format!(
2083                            "trigger '{}' already exists on relation '{}'",
2084                            create_trigger.name, create_trigger.table
2085                        ),
2086                    };
2087                }
2088                self.snapshot_trigger(&trigger_id);
2089                self.local.triggers.insert(
2090                    trigger_id.clone(),
2091                    TriggerOverlay::Present(crate::model::trigger::TriggerState {
2092                        name: create_trigger.name.clone(),
2093                        id: trigger_id.clone(),
2094                        table_id: create_trigger.table.clone(),
2095                        enabled_mode: crate::model::trigger::TriggerEnableMode::Origin,
2096                        generation: self.local.generation_counter,
2097                    }),
2098                );
2099
2100                self.snapshot_relation(&create_trigger.table);
2101                if let Some(RelationOverlay::Present(rel)) =
2102                    self.local.relations.get_mut(&create_trigger.table)
2103                {
2104                    rel.triggers.insert(create_trigger.name.clone());
2105                }
2106
2107                self.snapshot_graph_full();
2108                self.local.graph.edges.push(DependencyEdge::new(
2109                    trigger_id.clone(),
2110                    create_trigger.table.clone(),
2111                    DependencyKind::TriggerOnTable {
2112                        trigger_id: trigger_id.clone(),
2113                        function_id: create_trigger.function_id.clone(),
2114                    },
2115                ));
2116
2117                MutationResult::Applied
2118            }
2119            Mutation::DropTrigger(drop_trigger) => {
2120                let trigger_id = Self::trigger_key(&drop_trigger.table, &drop_trigger.name);
2121                if !matches!(
2122                    self.local.triggers.get(&trigger_id),
2123                    Some(TriggerOverlay::Present(_))
2124                ) {
2125                    return if drop_trigger.if_exists {
2126                        MutationResult::Skipped
2127                    } else {
2128                        MutationResult::Conflict {
2129                            reason: format!(
2130                                "trigger '{}' does not exist on relation '{}'",
2131                                drop_trigger.name, drop_trigger.table
2132                            ),
2133                        }
2134                    };
2135                }
2136                self.snapshot_trigger(&trigger_id);
2137                self.local
2138                    .triggers
2139                    .insert(trigger_id.clone(), TriggerOverlay::Dropped);
2140
2141                self.snapshot_relation(&drop_trigger.table);
2142                if let Some(RelationOverlay::Present(rel)) =
2143                    self.local.relations.get_mut(&drop_trigger.table)
2144                {
2145                    rel.triggers.remove(&drop_trigger.name);
2146                }
2147
2148                self.snapshot_graph_full();
2149                self.local.graph.edges.retain(|e| {
2150                    !(matches!(e.kind, DependencyKind::TriggerOnTable { .. })
2151                        && e.dependent == trigger_id)
2152                });
2153
2154                MutationResult::Applied
2155            }
2156            Mutation::AlterTable(alter) => {
2157                if let AlterTableActionMutation::OwnerTo { new_owner } = &alter.action {
2158                    let Some((owner, known)) = self.role_fact_identity(new_owner) else {
2159                        self.snapshot_confidence();
2160                        self.local.confidence = Confidence::Tainted;
2161                        return MutationResult::Skipped;
2162                    };
2163                    if !known {
2164                        self.snapshot_confidence();
2165                        self.local.confidence = Confidence::Tainted;
2166                    }
2167                    self.snapshot_relation(&alter.id);
2168                    return match self.local.relations.get_mut(&alter.id) {
2169                        Some(RelationOverlay::Present(relation)) => {
2170                            relation.owner = ObjectId::new("", owner);
2171                            MutationResult::Applied
2172                        }
2173                        _ => MutationResult::Conflict {
2174                            reason: format!("relation '{}' does not exist", alter.id),
2175                        },
2176                    };
2177                }
2178
2179                let trigger_mode = match &alter.action {
2180                    AlterTableActionMutation::DisableTrigger { trigger_name } => Some((
2181                        trigger_name.as_deref(),
2182                        crate::model::trigger::TriggerEnableMode::Disabled,
2183                    )),
2184                    AlterTableActionMutation::EnableTrigger { trigger_name } => Some((
2185                        trigger_name.as_deref(),
2186                        crate::model::trigger::TriggerEnableMode::Origin,
2187                    )),
2188                    _ => None,
2189                };
2190                if let Some((trigger_name, enabled_mode)) = trigger_mode {
2191                    let all = trigger_name.is_none_or(|name| name.eq_ignore_ascii_case("all"));
2192                    let trigger_ids: Vec<ObjectId> = self
2193                        .local
2194                        .triggers
2195                        .iter()
2196                        .filter_map(|(id, overlay)| {
2197                            let TriggerOverlay::Present(trigger) = overlay else {
2198                                return None;
2199                            };
2200                            (trigger.table_id == alter.id
2201                                && (all || trigger_name == Some(trigger.name.as_str())))
2202                            .then(|| id.clone())
2203                        })
2204                        .collect();
2205                    for trigger_id in trigger_ids {
2206                        self.snapshot_trigger(&trigger_id);
2207                        if let Some(TriggerOverlay::Present(trigger)) =
2208                            self.local.triggers.get_mut(&trigger_id)
2209                        {
2210                            trigger.enabled_mode = enabled_mode;
2211                        }
2212                    }
2213                    return MutationResult::Applied;
2214                }
2215
2216                if let AlterTableActionMutation::AddForeignKey {
2217                    to_table,
2218                    from_columns,
2219                    to_columns,
2220                    ..
2221                } = &alter.action
2222                {
2223                    if let Some(RelationOverlay::Present(child)) =
2224                        self.local.relations.get(&alter.id)
2225                    {
2226                        if let Some(column) =
2227                            from_columns.iter().find(|column| !child.has_column(column))
2228                        {
2229                            return MutationResult::Conflict {
2230                                reason: format!(
2231                                    "foreign key column '{}' does not exist on relation '{}'",
2232                                    column, alter.id
2233                                ),
2234                            };
2235                        }
2236                    }
2237
2238                    let Some(RelationOverlay::Present(parent)) = self.local.relations.get(to_table)
2239                    else {
2240                        return MutationResult::Conflict {
2241                            reason: format!(
2242                                "foreign key references relation '{}' which does not exist",
2243                                to_table
2244                            ),
2245                        };
2246                    };
2247                    if let Some(column) =
2248                        to_columns.iter().find(|column| !parent.has_column(column))
2249                    {
2250                        return MutationResult::Conflict {
2251                            reason: format!(
2252                                "foreign key references column '{}.{}' which does not exist",
2253                                to_table, column
2254                            ),
2255                        };
2256                    }
2257                }
2258
2259                let implicit_add = match &alter.action {
2260                    AlterTableActionMutation::AddColumn {
2261                        name, generation, ..
2262                    } => match generation {
2263                        crate::analysis::facts::ColumnGeneration::Serial => Some((
2264                            self.next_implicit_sequence_id(&alter.id, name, &HashSet::new()),
2265                            name.clone(),
2266                            SequenceKind::SerialLike,
2267                        )),
2268                        crate::analysis::facts::ColumnGeneration::Identity => Some((
2269                            self.next_implicit_sequence_id(&alter.id, name, &HashSet::new()),
2270                            name.clone(),
2271                            SequenceKind::Identity,
2272                        )),
2273                        crate::analysis::facts::ColumnGeneration::Ordinary => None,
2274                    },
2275                    _ => None,
2276                };
2277                let owned_sequences_for_column: Vec<ObjectId> = match &alter.action {
2278                    AlterTableActionMutation::DropColumn { name, .. }
2279                    | AlterTableActionMutation::RenameColumn { from: name, .. } => self
2280                        .local
2281                        .sequences
2282                        .iter()
2283                        .filter_map(|(id, overlay)| match overlay {
2284                            SequenceOverlay::Present(sequence)
2285                                if sequence.owned_by.as_ref()
2286                                    == Some(&(alter.id.clone(), name.clone())) =>
2287                            {
2288                                Some(id.clone())
2289                            }
2290                            _ => None,
2291                        })
2292                        .collect(),
2293                    _ => Vec::new(),
2294                };
2295
2296                let using_index = match &alter.action {
2297                    AlterTableActionMutation::AddUniqueConstraint { using_index, .. }
2298                    | AlterTableActionMutation::AddPrimaryKeyConstraint { using_index, .. } => {
2299                        using_index.as_ref()
2300                    }
2301                    _ => None,
2302                };
2303                if let Some(index) = using_index {
2304                    let Some(edge) = self.local.graph.edges.iter().find(|edge| {
2305                        matches!(edge.kind, DependencyKind::IndexOnRelation { .. })
2306                            && edge.dependent == *index
2307                    }) else {
2308                        return MutationResult::Conflict {
2309                            reason: format!(
2310                                "constraint references index '{}' which does not exist",
2311                                index
2312                            ),
2313                        };
2314                    };
2315                    if edge.referenced != alter.id {
2316                        return MutationResult::Conflict {
2317                            reason: format!(
2318                                "constraint index '{}' belongs to relation '{}', not '{}'",
2319                                index, edge.referenced, alter.id
2320                            ),
2321                        };
2322                    }
2323                    if let DependencyKind::IndexOnRelation {
2324                        has_predicate,
2325                        is_unique,
2326                        eligibility_known,
2327                        ..
2328                    } = &edge.kind
2329                        && *eligibility_known
2330                        && (!is_unique || *has_predicate)
2331                    {
2332                        return MutationResult::Conflict {
2333                            reason: format!(
2334                                "constraint index '{}' must be unique and non-partial",
2335                                index
2336                            ),
2337                        };
2338                    }
2339                }
2340
2341                self.snapshot_relation(&alter.id);
2342                let rel_overlay = self.local.relations.get_mut(&alter.id);
2343                if let Some(RelationOverlay::Present(rel)) = rel_overlay {
2344                    let generation = rel.generation;
2345                    match &alter.action {
2346                        AlterTableActionMutation::AddColumn {
2347                            name,
2348                            ty,
2349                            if_not_exists,
2350                            not_null,
2351                            default,
2352                            depends_on,
2353                            generation: _,
2354                        } => {
2355                            if let Some(existing_col) = rel.columns.iter().find(|c| c.name == *name)
2356                            {
2357                                if *if_not_exists {
2358                                    return MutationResult::Skipped;
2359                                }
2360                                return MutationResult::Conflict {
2361                                    reason: format!(
2362                                        "column '{}' already exists with type {}; this statement adds it again with type {}",
2363                                        name,
2364                                        existing_col.data_type.as_deref().unwrap_or("unknown"),
2365                                        ty.as_deref().unwrap_or("unknown")
2366                                    ),
2367                                };
2368                            }
2369                            rel.apply_column_action(&ColumnAction::Add {
2370                                name: name.clone(),
2371                                data_type: ty.clone(),
2372                                not_null: *not_null,
2373                                default: default.clone(),
2374                            });
2375
2376                            if let Some((sequence_id, column_name, _)) = &implicit_add
2377                                && column_name == name
2378                                && let Some(column) =
2379                                    rel.columns.iter_mut().find(|column| column.name == *name)
2380                            {
2381                                column.default = Some(Self::sequence_nextval_default(sequence_id));
2382                                column.default_expr_text = Some(format!(
2383                                    "nextval('{}.{}'::regclass)",
2384                                    sequence_id.schema, sequence_id.name
2385                                ));
2386                                column.is_nullable = false;
2387                            }
2388
2389                            if let Some((source_table, source_col)) = depends_on {
2390                                self.snapshot_graph();
2391                                self.local.graph.edges.push(DependencyEdge::new(
2392                                    alter.id.clone(),
2393                                    source_table.clone(),
2394                                    DependencyKind::ColumnGeneratedFrom {
2395                                        column: name.clone(),
2396                                        depends_on_column: source_col.clone(),
2397                                    },
2398                                ));
2399                            }
2400                        }
2401                        AlterTableActionMutation::DropColumn { name, if_exists } => {
2402                            if !rel.has_column(name) {
2403                                if *if_exists {
2404                                    // Column doesn't exist and IF EXISTS was specified: no-op
2405                                    return MutationResult::Skipped;
2406                                }
2407                                return MutationResult::Conflict {
2408                                    reason: format!(
2409                                        "column '{}' does not exist on relation '{}'",
2410                                        name, alter.id
2411                                    ),
2412                                };
2413                            }
2414                            rel.apply_column_action(&ColumnAction::Drop { name: name.clone() });
2415                        }
2416                        AlterTableActionMutation::RenameColumn { from, to } => {
2417                            rel.apply_column_action(&ColumnAction::Rename {
2418                                from: from.clone(),
2419                                to: to.clone(),
2420                            });
2421                        }
2422                        AlterTableActionMutation::SetNotNull { column } => {
2423                            rel.apply_column_action(&ColumnAction::SetNotNull {
2424                                name: column.clone(),
2425                            });
2426                        }
2427                        AlterTableActionMutation::DropNotNull { column } => {
2428                            rel.apply_column_action(&ColumnAction::DropNotNull {
2429                                name: column.clone(),
2430                            });
2431                        }
2432                        AlterTableActionMutation::SetType { column, ty, .. } => {
2433                            if !rel.has_column(column) {
2434                                self.local.confidence = Confidence::Tainted;
2435                            }
2436                            rel.apply_column_action(&ColumnAction::SetType {
2437                                name: column.clone(),
2438                                data_type: ty.clone(),
2439                            });
2440                        }
2441                        AlterTableActionMutation::SetDefault { column, default } => {
2442                            if !rel.has_column(column) {
2443                                self.local.confidence = Confidence::Tainted;
2444                            }
2445                            rel.apply_column_action(&ColumnAction::SetDefault {
2446                                name: column.clone(),
2447                                default: default.clone(),
2448                            });
2449                        }
2450                        AlterTableActionMutation::AddForeignKey {
2451                            constraint_name,
2452                            to_table,
2453                            from_columns,
2454                            to_columns,
2455                            not_valid,
2456                        } => {
2457                            let constraint_name = constraint_name.clone().unwrap_or_else(|| {
2458                                format!("{}_{}_fkey", alter.id.name, from_columns.join("_"))
2459                            });
2460                            self.snapshot_constraint(&alter.id, &constraint_name);
2461                            self.local.constraints.insert(
2462                                (alter.id.clone(), constraint_name.clone()),
2463                                ConstraintState {
2464                                    table_id: alter.id.clone(),
2465                                    name: constraint_name.clone(),
2466                                    kind: ConstraintKind::ForeignKey,
2467                                    validated: !not_valid,
2468                                },
2469                            );
2470                            self.snapshot_graph();
2471                            self.local.graph.edges.push(DependencyEdge::new(
2472                                alter.id.clone(),
2473                                to_table.clone(),
2474                                DependencyKind::ForeignKey {
2475                                    constraint_name: Some(constraint_name),
2476                                    from_columns: from_columns.clone(),
2477                                    to_columns: to_columns.clone(),
2478                                    from_generation: generation,
2479                                },
2480                            ));
2481                        }
2482                        AlterTableActionMutation::DropConstraint { name } => {
2483                            self.snapshot_constraint(&alter.id, name);
2484                            self.local
2485                                .constraints
2486                                .remove(&(alter.id.clone(), name.clone()));
2487                            self.snapshot_graph();
2488                            self.local.graph.edges.retain(|e| {
2489                                if let DependencyKind::ForeignKey {
2490                                    constraint_name, ..
2491                                } = &e.kind
2492                                {
2493                                    !(e.dependent == alter.id
2494                                        && constraint_name.as_ref() == Some(name))
2495                                } else {
2496                                    true
2497                                }
2498                            });
2499                        }
2500                        AlterTableActionMutation::RenameConstraint { old_name, new_name } => {
2501                            self.snapshot_constraint(&alter.id, old_name);
2502                            self.snapshot_constraint(&alter.id, new_name);
2503                            if let Some(mut constraint) = self
2504                                .local
2505                                .constraints
2506                                .remove(&(alter.id.clone(), old_name.clone()))
2507                            {
2508                                constraint.name = new_name.clone();
2509                                self.local
2510                                    .constraints
2511                                    .insert((alter.id.clone(), new_name.clone()), constraint);
2512                            }
2513                            self.snapshot_graph_full();
2514                            for edge in &mut self.local.graph.edges {
2515                                if edge.dependent == alter.id
2516                                    && let DependencyKind::ForeignKey {
2517                                        constraint_name, ..
2518                                    } = &mut edge.kind
2519                                    && constraint_name.as_deref() == Some(old_name)
2520                                {
2521                                    *constraint_name = Some(new_name.clone());
2522                                }
2523                            }
2524                        }
2525                        AlterTableActionMutation::AddCheckConstraint {
2526                            constraint_name,
2527                            not_valid,
2528                        } => {
2529                            let constraint_name = constraint_name
2530                                .clone()
2531                                .unwrap_or_else(|| format!("{}_check", alter.id.name));
2532                            self.snapshot_constraint(&alter.id, &constraint_name);
2533                            self.local.constraints.insert(
2534                                (alter.id.clone(), constraint_name.clone()),
2535                                ConstraintState {
2536                                    table_id: alter.id.clone(),
2537                                    name: constraint_name,
2538                                    kind: ConstraintKind::Check,
2539                                    validated: !not_valid,
2540                                },
2541                            );
2542                        }
2543                        AlterTableActionMutation::AddUniqueConstraint {
2544                            constraint_name,
2545                            using_index,
2546                        } => {
2547                            let constraint_name = constraint_name
2548                                .clone()
2549                                .or_else(|| using_index.as_ref().map(|index| index.name.clone()))
2550                                .unwrap_or_else(|| format!("{}_key", alter.id.name));
2551                            self.snapshot_constraint(&alter.id, &constraint_name);
2552                            self.local.constraints.insert(
2553                                (alter.id.clone(), constraint_name.clone()),
2554                                ConstraintState {
2555                                    table_id: alter.id.clone(),
2556                                    name: constraint_name,
2557                                    kind: ConstraintKind::Unique,
2558                                    validated: true,
2559                                },
2560                            );
2561                        }
2562                        AlterTableActionMutation::AddPrimaryKeyConstraint {
2563                            constraint_name,
2564                            using_index,
2565                        } => {
2566                            let constraint_name = constraint_name
2567                                .clone()
2568                                .or_else(|| using_index.as_ref().map(|index| index.name.clone()))
2569                                .unwrap_or_else(|| format!("{}_pkey", alter.id.name));
2570                            self.snapshot_constraint(&alter.id, &constraint_name);
2571                            self.local.constraints.insert(
2572                                (alter.id.clone(), constraint_name.clone()),
2573                                ConstraintState {
2574                                    table_id: alter.id.clone(),
2575                                    name: constraint_name,
2576                                    kind: ConstraintKind::PrimaryKey,
2577                                    validated: true,
2578                                },
2579                            );
2580                        }
2581                        AlterTableActionMutation::AddExcludeConstraint { constraint_name } => {
2582                            let constraint_name = constraint_name
2583                                .clone()
2584                                .unwrap_or_else(|| format!("{}_excl", alter.id.name));
2585                            self.snapshot_constraint(&alter.id, &constraint_name);
2586                            self.local.constraints.insert(
2587                                (alter.id.clone(), constraint_name.clone()),
2588                                ConstraintState {
2589                                    table_id: alter.id.clone(),
2590                                    name: constraint_name,
2591                                    kind: ConstraintKind::Exclusion,
2592                                    validated: true,
2593                                },
2594                            );
2595                        }
2596                        AlterTableActionMutation::ValidateConstraint { constraint_name } => {
2597                            self.snapshot_constraint(&alter.id, constraint_name);
2598                            if let Some(constraint) = self
2599                                .local
2600                                .constraints
2601                                .get_mut(&(alter.id.clone(), constraint_name.clone()))
2602                            {
2603                                constraint.validated = true;
2604                            }
2605                        }
2606                        AlterTableActionMutation::AttachPartition { child, .. } => {
2607                            // BUG-012: Reject cycle topologies before inserting the edge.
2608                            if self.local.graph.check_partition_cycle(&alter.id, child) {
2609                                self.snapshot_confidence();
2610                                self.local.confidence = Confidence::Tainted;
2611                            } else {
2612                                self.snapshot_graph();
2613                                self.local.graph.edges.push(DependencyEdge::new(
2614                                    child.clone(),
2615                                    alter.id.clone(),
2616                                    DependencyKind::PartitionOf,
2617                                ));
2618                            }
2619                        }
2620                        AlterTableActionMutation::DetachPartition { child } => {
2621                            self.snapshot_graph();
2622                            self.local.graph.edges.retain(|e| {
2623                                !(matches!(e.kind, DependencyKind::PartitionOf)
2624                                    && e.dependent == *child
2625                                    && e.referenced == alter.id)
2626                            });
2627                        }
2628                        _ => {}
2629                    }
2630                }
2631                if let Some((sequence_id, column_name, kind)) = implicit_add {
2632                    self.snapshot_sequence(&sequence_id);
2633                    self.snapshot_generation_counter();
2634                    self.local.generation_counter += 1;
2635                    self.local.sequences.insert(
2636                        sequence_id.clone(),
2637                        SequenceOverlay::Present(SequenceState {
2638                            id: sequence_id.clone(),
2639                            owner: self
2640                                .local
2641                                .relations
2642                                .get(&alter.id)
2643                                .and_then(|overlay| match overlay {
2644                                    RelationOverlay::Present(table) => Some(table.owner.clone()),
2645                                    RelationOverlay::Dropped => None,
2646                                })
2647                                .unwrap_or_else(|| ObjectId::new("", &self.local.current_role)),
2648                            owned_by: Some((alter.id.clone(), column_name.clone())),
2649                            kind,
2650                            generation: self.local.generation_counter,
2651                        }),
2652                    );
2653                    self.snapshot_graph();
2654                    self.local.graph.edges.push(DependencyEdge::new(
2655                        sequence_id,
2656                        alter.id.clone(),
2657                        DependencyKind::SequenceOwnedBy {
2658                            column: column_name,
2659                        },
2660                    ));
2661                }
2662                match &alter.action {
2663                    AlterTableActionMutation::DropColumn { .. } => {
2664                        for sequence_id in owned_sequences_for_column {
2665                            self.snapshot_sequence(&sequence_id);
2666                            self.local
2667                                .sequences
2668                                .insert(sequence_id.clone(), SequenceOverlay::Dropped);
2669                            self.snapshot_graph_full();
2670                            self.local.graph.edges.retain(|edge| {
2671                                !(matches!(edge.kind, DependencyKind::SequenceOwnedBy { .. })
2672                                    && edge.dependent == sequence_id)
2673                            });
2674                        }
2675                    }
2676                    AlterTableActionMutation::RenameColumn { to, .. } => {
2677                        for sequence_id in owned_sequences_for_column {
2678                            self.snapshot_sequence(&sequence_id);
2679                            if let Some(SequenceOverlay::Present(sequence)) =
2680                                self.local.sequences.get_mut(&sequence_id)
2681                                && let Some((_, column)) = &mut sequence.owned_by
2682                            {
2683                                *column = to.clone();
2684                            }
2685                            self.snapshot_graph_full();
2686                            for edge in &mut self.local.graph.edges {
2687                                if edge.dependent == sequence_id
2688                                    && let DependencyKind::SequenceOwnedBy { column } =
2689                                        &mut edge.kind
2690                                {
2691                                    *column = to.clone();
2692                                }
2693                            }
2694                        }
2695                    }
2696                    _ => {}
2697                }
2698                MutationResult::Applied
2699            }
2700            Mutation::CreateType(create_type) => {
2701                if self.relation_namespace_is_taken(&create_type.id) {
2702                    return MutationResult::Conflict {
2703                        reason: format!("type '{}' already exists", create_type.id),
2704                    };
2705                }
2706                self.snapshot_type(&create_type.id);
2707                self.snapshot_generation_counter();
2708                self.local.generation_counter += 1;
2709                let generation = self.local.generation_counter;
2710
2711                self.local.types.insert(
2712                    create_type.id.clone(),
2713                    TypeOverlay::Present(TypeState {
2714                        id: create_type.id.clone(),
2715                        generation,
2716                        kind: create_type.kind.clone(),
2717                    }),
2718                );
2719                MutationResult::Applied
2720            }
2721            Mutation::AlterType(alter_type) => {
2722                self.snapshot_type(&alter_type.id);
2723                if let Some(TypeOverlay::Present(t)) = self.local.types.get_mut(&alter_type.id) {
2724                    match &alter_type.action {
2725                        AlterTypeActionMutation::AddValue {
2726                            new_value,
2727                            neighbor,
2728                            before,
2729                        } => {
2730                            if let TypeKind::Enum { variants } = &mut t.kind {
2731                                if variants.contains(new_value) {
2732                                    return MutationResult::Skipped;
2733                                }
2734                                let insertion_index = neighbor
2735                                    .as_ref()
2736                                    .and_then(|neighbor| {
2737                                        variants.iter().position(|value| value == neighbor)
2738                                    })
2739                                    .map(|index| if *before { index } else { index + 1 })
2740                                    .unwrap_or(variants.len());
2741                                variants.insert(insertion_index, new_value.clone());
2742                            }
2743                        }
2744                        AlterTypeActionMutation::RenameValue {
2745                            old_value,
2746                            new_value,
2747                        } => {
2748                            let TypeKind::Enum { variants } = &mut t.kind else {
2749                                return MutationResult::Conflict {
2750                                    reason: format!("type '{}' is not an enum", alter_type.id),
2751                                };
2752                            };
2753                            let Some(old_index) =
2754                                variants.iter().position(|value| value == old_value)
2755                            else {
2756                                return MutationResult::Conflict {
2757                                    reason: format!(
2758                                        "'{}' is not an existing label of enum '{}'",
2759                                        old_value, alter_type.id
2760                                    ),
2761                                };
2762                            };
2763                            if variants.iter().any(|value| value == new_value) {
2764                                return MutationResult::Conflict {
2765                                    reason: format!(
2766                                        "enum label '{}' already exists on type '{}'",
2767                                        new_value, alter_type.id
2768                                    ),
2769                                };
2770                            }
2771                            variants[old_index] = new_value.clone();
2772                        }
2773                    }
2774                } else if matches!(
2775                    alter_type.action,
2776                    AlterTypeActionMutation::RenameValue { .. }
2777                ) {
2778                    return MutationResult::Conflict {
2779                        reason: format!("type '{}' does not exist", alter_type.id),
2780                    };
2781                }
2782                MutationResult::Applied
2783            }
2784            Mutation::CreateDomain(create_domain) => {
2785                if self.relation_namespace_is_taken(&create_domain.id) {
2786                    return MutationResult::Conflict {
2787                        reason: format!("type '{}' already exists", create_domain.id),
2788                    };
2789                }
2790                self.snapshot_type(&create_domain.id);
2791                self.snapshot_generation_counter();
2792                self.local.generation_counter += 1;
2793                let generation = self.local.generation_counter;
2794
2795                self.local.types.insert(
2796                    create_domain.id.clone(),
2797                    TypeOverlay::Present(TypeState {
2798                        id: create_domain.id.clone(),
2799                        generation,
2800                        kind: TypeKind::Domain {
2801                            base_type: create_domain.base_type.clone(),
2802                        },
2803                    }),
2804                );
2805                MutationResult::Applied
2806            }
2807            Mutation::AlterDomain(_) => MutationResult::Applied,
2808            Mutation::DropDomain(drop_domain) => {
2809                for id in &drop_domain.ids {
2810                    self.snapshot_type(id);
2811                    self.local.types.insert(id.clone(), TypeOverlay::Dropped);
2812                }
2813                MutationResult::Applied
2814            }
2815            Mutation::DropType(drop_type) => {
2816                for id in &drop_type.ids {
2817                    self.snapshot_type(id);
2818                    self.local.types.insert(id.clone(), TypeOverlay::Dropped);
2819                }
2820                MutationResult::Applied
2821            }
2822            Mutation::CreateSequence(create_seq) => {
2823                if create_seq.if_not_exists && self.relation_namespace_is_taken(&create_seq.id) {
2824                    return MutationResult::Skipped;
2825                }
2826                if self.relation_namespace_is_taken(&create_seq.id) {
2827                    return MutationResult::Conflict {
2828                        reason: format!("relation '{}' already exists", create_seq.id),
2829                    };
2830                }
2831                if let Some((table_id, column)) = &create_seq.owned_by {
2832                    if table_id.schema != create_seq.id.schema {
2833                        return MutationResult::Conflict {
2834                            reason: "sequence must be in the same schema as its owning table"
2835                                .to_string(),
2836                        };
2837                    }
2838                    match self.local.relations.get(table_id) {
2839                        Some(RelationOverlay::Present(table)) => {
2840                            if !table.has_column(column) {
2841                                return MutationResult::Conflict {
2842                                    reason: format!(
2843                                        "column '{}.{}' does not exist",
2844                                        table_id, column
2845                                    ),
2846                                };
2847                            }
2848                            if self.local.current_role_known
2849                                && table.owner.name != self.local.current_role
2850                            {
2851                                return MutationResult::Conflict {
2852                                    reason: "sequence and table must have the same owner"
2853                                        .to_string(),
2854                                };
2855                            }
2856                        }
2857                        _ if self.baseline_covers_object(table_id) && self.baseline_available => {
2858                            return MutationResult::Conflict {
2859                                reason: format!("relation '{}' does not exist", table_id),
2860                            };
2861                        }
2862                        _ => {
2863                            self.snapshot_confidence();
2864                            self.local.confidence = Confidence::Tainted;
2865                        }
2866                    }
2867                }
2868                self.snapshot_sequence(&create_seq.id);
2869                self.snapshot_generation_counter();
2870                self.local.generation_counter += 1;
2871                let generation = self.local.generation_counter;
2872
2873                self.local.sequences.insert(
2874                    create_seq.id.clone(),
2875                    SequenceOverlay::Present(SequenceState {
2876                        id: create_seq.id.clone(),
2877                        owner: ObjectId::new("", self.local.current_role.clone()),
2878                        owned_by: create_seq.owned_by.clone(),
2879                        kind: if create_seq.owned_by.is_some() {
2880                            SequenceKind::Owned
2881                        } else {
2882                            SequenceKind::Standalone
2883                        },
2884                        generation,
2885                    }),
2886                );
2887
2888                if let Some((table_id, col)) = &create_seq.owned_by {
2889                    self.snapshot_graph();
2890                    self.local.graph.edges.push(DependencyEdge::new(
2891                        create_seq.id.clone(),
2892                        table_id.clone(),
2893                        DependencyKind::SequenceOwnedBy {
2894                            column: col.clone(),
2895                        },
2896                    ));
2897                }
2898                MutationResult::Applied
2899            }
2900            Mutation::AlterSequence(alter_seq) => {
2901                if !self.sequence_is_present(&alter_seq.id) {
2902                    if alter_seq.if_exists {
2903                        return MutationResult::Skipped;
2904                    }
2905                    if self.baseline_covers_object(&alter_seq.id) && self.baseline_available {
2906                        return MutationResult::Conflict {
2907                            reason: format!("sequence '{}' does not exist", alter_seq.id),
2908                        };
2909                    }
2910                    self.snapshot_confidence();
2911                    self.local.confidence = Confidence::Tainted;
2912                    return MutationResult::Skipped;
2913                }
2914                let current = match self.local.sequences.get(&alter_seq.id) {
2915                    Some(SequenceOverlay::Present(sequence)) => sequence.clone(),
2916                    _ => unreachable!("presence checked above"),
2917                };
2918                match &alter_seq.action {
2919                    crate::analysis::mutations::AlterSequenceActionMutation::OwnedBy(owned_by) => {
2920                        if current.kind == SequenceKind::Identity {
2921                            return MutationResult::Conflict {
2922                                reason: "cannot change ownership of an identity sequence"
2923                                    .to_string(),
2924                            };
2925                        }
2926                        if let Some((table_id, column)) = owned_by {
2927                            if table_id.schema != alter_seq.id.schema {
2928                                return MutationResult::Conflict {
2929                                    reason:
2930                                        "sequence must be in the same schema as its owning table"
2931                                            .to_string(),
2932                                };
2933                            }
2934                            let Some(RelationOverlay::Present(table)) =
2935                                self.local.relations.get(table_id)
2936                            else {
2937                                return MutationResult::Conflict {
2938                                    reason: format!("relation '{}' does not exist", table_id),
2939                                };
2940                            };
2941                            if !table.has_column(column) {
2942                                return MutationResult::Conflict {
2943                                    reason: format!(
2944                                        "column '{}.{}' does not exist",
2945                                        table_id, column
2946                                    ),
2947                                };
2948                            }
2949                            if table.owner != current.owner {
2950                                return MutationResult::Conflict {
2951                                    reason: "sequence and table must have the same owner"
2952                                        .to_string(),
2953                                };
2954                            }
2955                        }
2956                        self.snapshot_sequence(&alter_seq.id);
2957                        self.snapshot_graph();
2958                        self.local.graph.edges.retain(|edge| {
2959                            !(matches!(edge.kind, DependencyKind::SequenceOwnedBy { .. })
2960                                && edge.dependent == alter_seq.id)
2961                        });
2962                        if let Some(SequenceOverlay::Present(sequence)) =
2963                            self.local.sequences.get_mut(&alter_seq.id)
2964                        {
2965                            sequence.owned_by = owned_by.clone();
2966                            sequence.kind = if owned_by.is_some() {
2967                                SequenceKind::Owned
2968                            } else {
2969                                SequenceKind::Standalone
2970                            };
2971                        }
2972                        if let Some((table_id, column)) = owned_by {
2973                            self.local.graph.edges.push(DependencyEdge::new(
2974                                alter_seq.id.clone(),
2975                                table_id.clone(),
2976                                DependencyKind::SequenceOwnedBy {
2977                                    column: column.clone(),
2978                                },
2979                            ));
2980                        }
2981                        MutationResult::Applied
2982                    }
2983                    crate::analysis::mutations::AlterSequenceActionMutation::OwnerTo(owner) => {
2984                        if current.kind == SequenceKind::Identity {
2985                            return MutationResult::Conflict {
2986                                reason: "cannot alter an identity sequence independently"
2987                                    .to_string(),
2988                            };
2989                        }
2990                        let Some((owner_name, known)) = self.role_fact_identity(owner) else {
2991                            self.snapshot_confidence();
2992                            self.local.confidence = Confidence::Tainted;
2993                            return MutationResult::Skipped;
2994                        };
2995                        if known
2996                            && self.local.roles_known
2997                            && self.present_role(&owner_name).is_none()
2998                        {
2999                            return MutationResult::Conflict {
3000                                reason: format!("role '{}' does not exist", owner_name),
3001                            };
3002                        }
3003                        if let Some((table_id, _)) = &current.owned_by
3004                            && let Some(RelationOverlay::Present(table)) =
3005                                self.local.relations.get(table_id)
3006                            && table.owner.name != owner_name
3007                        {
3008                            return MutationResult::Conflict {
3009                                reason: "sequence and table must have the same owner".to_string(),
3010                            };
3011                        }
3012                        self.snapshot_sequence(&alter_seq.id);
3013                        if let Some(SequenceOverlay::Present(sequence)) =
3014                            self.local.sequences.get_mut(&alter_seq.id)
3015                        {
3016                            sequence.owner = ObjectId::new("", owner_name);
3017                        }
3018                        MutationResult::Applied
3019                    }
3020                    crate::analysis::mutations::AlterSequenceActionMutation::RenameTo(new_id)
3021                    | crate::analysis::mutations::AlterSequenceActionMutation::SetSchema(new_id) => {
3022                        if current.kind == SequenceKind::Identity {
3023                            return MutationResult::Conflict {
3024                                reason: "cannot alter an identity sequence independently"
3025                                    .to_string(),
3026                            };
3027                        }
3028                        if self.relation_namespace_is_taken(new_id) {
3029                            return MutationResult::Conflict {
3030                                reason: format!("relation '{}' already exists", new_id),
3031                            };
3032                        }
3033                        if let Some((table_id, _)) = &current.owned_by
3034                            && table_id.schema != new_id.schema
3035                        {
3036                            return MutationResult::Conflict {
3037                                reason: "sequence must be in the same schema as its owning table"
3038                                    .to_string(),
3039                            };
3040                        }
3041                        self.snapshot_namespace();
3042                        let mut moved = current;
3043                        moved.id = new_id.clone();
3044                        self.local.sequences.remove(&alter_seq.id);
3045                        self.local
3046                            .sequences
3047                            .insert(new_id.clone(), SequenceOverlay::Present(moved));
3048                        self.local.graph.propagate_rename(&alter_seq.id, new_id);
3049                        self.local.graph.edges.push(DependencyEdge::new(
3050                            alter_seq.id.clone(),
3051                            new_id.clone(),
3052                            DependencyKind::RenameTo,
3053                        ));
3054                        if self.baseline_sequences.remove(&alter_seq.id) {
3055                            self.baseline_sequences.insert(new_id.clone());
3056                        }
3057                        MutationResult::Applied
3058                    }
3059                    crate::analysis::mutations::AlterSequenceActionMutation::Other => {
3060                        MutationResult::Applied
3061                    }
3062                }
3063            }
3064            Mutation::DropSequence(drop_seq) => {
3065                if !drop_seq.if_exists {
3066                    let missing: Vec<ObjectId> = drop_seq
3067                        .ids
3068                        .iter()
3069                        .filter(|id| !self.sequence_is_present(id))
3070                        .cloned()
3071                        .collect();
3072                    for id in &missing {
3073                        if self.baseline_covers_object(id) && self.baseline_available {
3074                            return MutationResult::Conflict {
3075                                reason: format!("sequence '{}' does not exist", id),
3076                            };
3077                        }
3078                        self.snapshot_confidence();
3079                        self.local.confidence = Confidence::Tainted;
3080                    }
3081                }
3082                let present: Vec<ObjectId> = drop_seq
3083                    .ids
3084                    .iter()
3085                    .filter(|id| self.sequence_is_present(id))
3086                    .cloned()
3087                    .collect();
3088                if present.is_empty() {
3089                    return MutationResult::Skipped;
3090                }
3091                for id in &present {
3092                    let Some(SequenceOverlay::Present(sequence)) = self.local.sequences.get(id)
3093                    else {
3094                        continue;
3095                    };
3096                    if sequence.kind == SequenceKind::Identity {
3097                        return MutationResult::Conflict {
3098                            reason: format!("cannot drop identity sequence '{}' independently", id),
3099                        };
3100                    }
3101                    if sequence.kind == SequenceKind::SerialLike && !drop_seq.cascade {
3102                        return MutationResult::Conflict {
3103                            reason: format!("sequence '{}' still has dependent defaults", id),
3104                        };
3105                    }
3106                }
3107                if drop_seq.cascade {
3108                    let serial_owners: Vec<(ObjectId, String)> = present
3109                        .iter()
3110                        .filter_map(|id| match self.local.sequences.get(id) {
3111                            Some(SequenceOverlay::Present(sequence))
3112                                if sequence.kind == SequenceKind::SerialLike =>
3113                            {
3114                                sequence.owned_by.clone()
3115                            }
3116                            _ => None,
3117                        })
3118                        .collect();
3119                    for (table_id, column) in serial_owners {
3120                        self.snapshot_relation(&table_id);
3121                        if let Some(RelationOverlay::Present(table)) =
3122                            self.local.relations.get_mut(&table_id)
3123                            && let Some(column) =
3124                                table.columns.iter_mut().find(|item| item.name == column)
3125                        {
3126                            column.default = None;
3127                            column.default_expr_text = None;
3128                        }
3129                    }
3130                }
3131                for id in &present {
3132                    self.snapshot_sequence(id);
3133                    self.local
3134                        .sequences
3135                        .insert(id.clone(), SequenceOverlay::Dropped);
3136                }
3137                self.snapshot_graph_full();
3138                self.local.graph.edges.retain(|e| {
3139                    !(matches!(e.kind, DependencyKind::SequenceOwnedBy { .. })
3140                        && present.contains(&e.dependent))
3141                });
3142                MutationResult::Applied
3143            }
3144            Mutation::Rename(rename) => {
3145                let renames_relation = self.relation_is_present(&rename.old_id);
3146                let renames_index = self.index_is_present(&rename.old_id);
3147                if !renames_relation && !renames_index {
3148                    if self.baseline_covers_object(&rename.old_id) {
3149                        return MutationResult::Conflict {
3150                            reason: format!("relation '{}' does not exist", rename.old_id),
3151                        };
3152                    }
3153                    self.snapshot_confidence();
3154                    self.local.confidence = Confidence::Tainted;
3155                    return MutationResult::Skipped;
3156                }
3157                if rename.old_id != rename.new_id
3158                    && self.relation_namespace_is_taken(&rename.new_id)
3159                {
3160                    return MutationResult::Conflict {
3161                        reason: format!("relation '{}' already exists", rename.new_id),
3162                    };
3163                }
3164                if rename.old_id.schema != rename.new_id.schema
3165                    && !self.schema_is_present(&rename.new_id.schema)
3166                {
3167                    if self.schema_absence_is_authoritative(&rename.new_id.schema) {
3168                        return MutationResult::Conflict {
3169                            reason: format!("schema '{}' does not exist", rename.new_id.schema),
3170                        };
3171                    }
3172                    self.snapshot_confidence();
3173                    self.local.confidence = Confidence::Tainted;
3174                    return MutationResult::Skipped;
3175                }
3176
3177                self.snapshot_namespace();
3178                if let Some(RelationOverlay::Present(mut state)) =
3179                    self.local.relations.remove(&rename.old_id)
3180                {
3181                    state.id = rename.new_id.clone();
3182                    self.local
3183                        .relations
3184                        .insert(rename.new_id.clone(), RelationOverlay::Present(state));
3185                }
3186                let owned_sequence_ids: Vec<ObjectId> = self
3187                    .local
3188                    .sequences
3189                    .iter()
3190                    .filter_map(|(id, overlay)| match overlay {
3191                        SequenceOverlay::Present(sequence)
3192                            if sequence
3193                                .owned_by
3194                                .as_ref()
3195                                .is_some_and(|(table, _)| table == &rename.old_id) =>
3196                        {
3197                            Some(id.clone())
3198                        }
3199                        _ => None,
3200                    })
3201                    .collect();
3202                for sequence_id in owned_sequence_ids {
3203                    self.snapshot_sequence(&sequence_id);
3204                    if let Some(SequenceOverlay::Present(sequence)) =
3205                        self.local.sequences.get_mut(&sequence_id)
3206                        && let Some((table, _)) = &mut sequence.owned_by
3207                    {
3208                        *table = rename.new_id.clone();
3209                    }
3210                }
3211                let triggers_to_move: Vec<(ObjectId, crate::model::trigger::TriggerState)> = self
3212                    .local
3213                    .triggers
3214                    .iter()
3215                    .filter_map(|(id, overlay)| match overlay {
3216                        TriggerOverlay::Present(trigger) if trigger.table_id == rename.old_id => {
3217                            Some((id.clone(), trigger.clone()))
3218                        }
3219                        _ => None,
3220                    })
3221                    .collect();
3222                for (old_trigger_id, mut trigger) in triggers_to_move {
3223                    let new_trigger_id = Self::trigger_key(&rename.new_id, &trigger.name);
3224                    self.local.triggers.remove(&old_trigger_id);
3225                    trigger.id = new_trigger_id.clone();
3226                    trigger.table_id = rename.new_id.clone();
3227                    self.local
3228                        .triggers
3229                        .insert(new_trigger_id.clone(), TriggerOverlay::Present(trigger));
3230                    self.local
3231                        .graph
3232                        .propagate_rename(&old_trigger_id, &new_trigger_id);
3233                    self.local.graph.edges.push(DependencyEdge::new(
3234                        old_trigger_id,
3235                        new_trigger_id,
3236                        DependencyKind::RenameTo,
3237                    ));
3238                }
3239                let constraints_to_move: Vec<(String, ConstraintState)> = self
3240                    .local
3241                    .constraints
3242                    .iter()
3243                    .filter(|((table_id, _), _)| table_id == &rename.old_id)
3244                    .map(|((_, name), constraint)| (name.clone(), constraint.clone()))
3245                    .collect();
3246                for (name, mut constraint) in constraints_to_move {
3247                    self.snapshot_constraint(&rename.old_id, &name);
3248                    self.snapshot_constraint(&rename.new_id, &name);
3249                    self.local
3250                        .constraints
3251                        .remove(&(rename.old_id.clone(), name.clone()));
3252                    constraint.table_id = rename.new_id.clone();
3253                    self.local
3254                        .constraints
3255                        .insert((rename.new_id.clone(), name), constraint);
3256                }
3257                self.local.pending_validation = std::mem::take(&mut self.local.pending_validation)
3258                    .into_iter()
3259                    .map(|(table, name)| {
3260                        if table == rename.old_id {
3261                            (rename.new_id.clone(), name)
3262                        } else {
3263                            (table, name)
3264                        }
3265                    })
3266                    .collect();
3267                self.local.graph.edges.push(DependencyEdge::new(
3268                    rename.old_id.clone(),
3269                    rename.new_id.clone(),
3270                    DependencyKind::RenameTo,
3271                ));
3272                self.local
3273                    .graph
3274                    .propagate_rename(&rename.old_id, &rename.new_id);
3275
3276                if renames_relation {
3277                    if self.baseline_relations.remove(&rename.old_id) {
3278                        self.baseline_relations.insert(rename.new_id.clone());
3279                    }
3280                    if self.baseline_fk_dependencies.remove(&rename.old_id) {
3281                        self.baseline_fk_dependencies.insert(rename.new_id.clone());
3282                    }
3283                    self.baseline_foreign_keys = std::mem::take(&mut self.baseline_foreign_keys)
3284                        .into_iter()
3285                        .map(|(table, name)| {
3286                            if table == rename.old_id {
3287                                (rename.new_id.clone(), name)
3288                            } else {
3289                                (table, name)
3290                            }
3291                        })
3292                        .collect();
3293                }
3294                if renames_index && self.baseline_indexes.remove(&rename.old_id) {
3295                    self.baseline_indexes.insert(rename.new_id.clone());
3296                }
3297
3298                MutationResult::Applied
3299            }
3300            Mutation::DropView(drop_view) => {
3301                for id in &drop_view.ids {
3302                    self.snapshot_relation(id);
3303                    self.local
3304                        .relations
3305                        .insert(id.clone(), RelationOverlay::Dropped);
3306                }
3307                self.snapshot_graph_full();
3308                self.local.graph.edges.retain(|e| {
3309                    !(matches!(e.kind, DependencyKind::ViewDependency { .. })
3310                        && drop_view.ids.contains(&e.dependent))
3311                });
3312                MutationResult::Applied
3313            }
3314            Mutation::DropMaterializedView(drop_mv) => {
3315                for id in &drop_mv.ids {
3316                    self.snapshot_relation(id);
3317                    self.local
3318                        .relations
3319                        .insert(id.clone(), RelationOverlay::Dropped);
3320                }
3321                self.snapshot_graph_full();
3322                self.local.graph.edges.retain(|e| {
3323                    !((matches!(e.kind, DependencyKind::ViewDependency { .. })
3324                        && drop_mv.ids.contains(&e.dependent))
3325                        || (matches!(e.kind, DependencyKind::IndexOnRelation { .. })
3326                            && drop_mv.ids.contains(&e.referenced)))
3327                });
3328                MutationResult::Applied
3329            }
3330            Mutation::DropIndex(drop_idx) => {
3331                self.snapshot_graph();
3332                self.local.graph.edges.retain(|e| {
3333                    !(matches!(e.kind, DependencyKind::IndexOnRelation { .. })
3334                        && e.dependent == drop_idx.id)
3335                });
3336                MutationResult::Applied
3337            }
3338            Mutation::ChangeRelationOwner { id, new_owner } => {
3339                let Some((owner, known)) = self.role_fact_identity(new_owner) else {
3340                    self.snapshot_confidence();
3341                    self.local.confidence = Confidence::Tainted;
3342                    return MutationResult::Skipped;
3343                };
3344                if !known {
3345                    self.snapshot_confidence();
3346                    self.local.confidence = Confidence::Tainted;
3347                }
3348                self.snapshot_relation(id);
3349                if let Some(RelationOverlay::Present(relation)) = self.local.relations.get_mut(id) {
3350                    relation.owner = ObjectId::new("", owner);
3351                    MutationResult::Applied
3352                } else {
3353                    MutationResult::Conflict {
3354                        reason: format!("relation '{}' does not exist", id),
3355                    }
3356                }
3357            }
3358            Mutation::SearchPath(sp) => {
3359                self.snapshot_search_path();
3360                self.snapshot_confidence();
3361                match &sp.target {
3362                    SearchPathTarget::Default => {
3363                        self.local.search_path_template =
3364                            self.local.default_search_path_template.clone();
3365                        self.refresh_role_sensitive_search_path();
3366                    }
3367                    SearchPathTarget::Schemas(schemas) => {
3368                        self.local.search_path_template = schemas.clone();
3369                        self.refresh_role_sensitive_search_path();
3370                    }
3371                }
3372                MutationResult::Applied
3373            }
3374            Mutation::SwitchRole {
3375                role,
3376                local,
3377                is_session_auth,
3378            } => {
3379                if *local && self.local.transactions.is_empty() {
3380                    // PostgreSQL warns and leaves the setting unchanged.
3381                    return MutationResult::Skipped;
3382                }
3383
3384                let target = if let Some(role) = role {
3385                    let Some(identity) = self.role_fact_identity(role) else {
3386                        self.snapshot_confidence();
3387                        self.local.confidence = Confidence::Tainted;
3388                        return MutationResult::Skipped;
3389                    };
3390                    Some(identity)
3391                } else if *is_session_auth {
3392                    Some((
3393                        self.local.authenticated_role.clone(),
3394                        self.local.authenticated_role_known,
3395                    ))
3396                } else {
3397                    Some((
3398                        self.local.session_role.clone(),
3399                        self.local.session_role_known,
3400                    ))
3401                };
3402                let (target_name, target_known) = target.expect("role reset always has a target");
3403                let persistent_role_reset_target = if role.is_none() && !*is_session_auth {
3404                    Some((
3405                        self.local.persistent_session_role.clone(),
3406                        self.local.persistent_session_role_known,
3407                    ))
3408                } else {
3409                    None
3410                };
3411
3412                let authorized = if role.is_none() {
3413                    Some(true)
3414                } else if *is_session_auth {
3415                    self.can_set_session_authorization_to(&target_name)
3416                } else {
3417                    self.can_set_role_to(&target_name)
3418                };
3419                match authorized {
3420                    Some(false) => {
3421                        return MutationResult::Conflict {
3422                            reason: if self.present_role(&target_name).is_none() {
3423                                format!("role '{}' does not exist", target_name)
3424                            } else {
3425                                format!("permission denied to set role '{}'", target_name)
3426                            },
3427                        };
3428                    }
3429                    None => {
3430                        self.snapshot_confidence();
3431                        self.local.confidence = Confidence::Tainted;
3432                    }
3433                    Some(true) => {}
3434                }
3435
3436                self.snapshot_role_context();
3437                self.snapshot_search_path();
3438                self.snapshot_confidence();
3439                if *is_session_auth {
3440                    self.local.session_role = target_name.clone();
3441                    self.local.session_role_known = target_known;
3442                    self.local.current_role = target_name.clone();
3443                    self.local.current_role_known = target_known;
3444                    if !local {
3445                        self.local.persistent_session_role = target_name.clone();
3446                        self.local.persistent_session_role_known = target_known;
3447                        self.local.persistent_current_role = target_name;
3448                        self.local.persistent_current_role_known = target_known;
3449                    }
3450                } else {
3451                    self.local.current_role = target_name.clone();
3452                    self.local.current_role_known = target_known;
3453                    if !local {
3454                        let (persistent_name, persistent_known) =
3455                            persistent_role_reset_target.unwrap_or((target_name, target_known));
3456                        self.local.persistent_current_role = persistent_name;
3457                        self.local.persistent_current_role_known = persistent_known;
3458                    }
3459                }
3460                self.refresh_role_sensitive_search_path();
3461                MutationResult::Applied
3462            }
3463            Mutation::BeginTransaction => {
3464                if self.local.transactions.is_empty() {
3465                    self.local.transactions.push(TransactionFrame::root());
3466                    MutationResult::Applied
3467                } else {
3468                    // PostgreSQL emits a warning and leaves the current
3469                    // transaction active for a nested BEGIN.
3470                    MutationResult::Skipped
3471                }
3472            }
3473            Mutation::CommitTransaction => {
3474                if self.local.transaction_aborted {
3475                    while let Some(frame) = self.local.transactions.pop() {
3476                        self.rollback_frame(frame);
3477                    }
3478                } else {
3479                    while self.local.transactions.pop().is_some() {}
3480                    self.restore_persistent_role_context();
3481                }
3482                self.local.transaction_aborted = false;
3483                MutationResult::Applied
3484            }
3485            Mutation::CommitAndChain => {
3486                if self.local.transactions.is_empty() {
3487                    self.local.confidence = Confidence::Tainted;
3488                    return MutationResult::Conflict {
3489                        reason: "COMMIT AND CHAIN can only be used in transaction blocks"
3490                            .to_string(),
3491                    };
3492                }
3493                if self.local.transaction_aborted {
3494                    while let Some(frame) = self.local.transactions.pop() {
3495                        self.rollback_frame(frame);
3496                    }
3497                } else {
3498                    while self.local.transactions.pop().is_some() {}
3499                    self.restore_persistent_role_context();
3500                }
3501                self.local.transaction_aborted = false;
3502                self.local.transactions.push(TransactionFrame::root());
3503                MutationResult::Applied
3504            }
3505            Mutation::RollbackTransaction => {
3506                while let Some(frame) = self.local.transactions.pop() {
3507                    self.rollback_frame(frame);
3508                }
3509                self.local.transaction_aborted = false;
3510                MutationResult::Applied
3511            }
3512            Mutation::RollbackAndChain => {
3513                if self.local.transactions.is_empty() {
3514                    self.local.confidence = Confidence::Tainted;
3515                    return MutationResult::Conflict {
3516                        reason: "ROLLBACK AND CHAIN can only be used in transaction blocks"
3517                            .to_string(),
3518                    };
3519                }
3520                while let Some(frame) = self.local.transactions.pop() {
3521                    self.rollback_frame(frame);
3522                }
3523                self.local.transaction_aborted = false;
3524                self.local.transactions.push(TransactionFrame::root());
3525                MutationResult::Applied
3526            }
3527            Mutation::RollbackToSavepoint(rts) => {
3528                let Some(position) = self
3529                    .local
3530                    .transactions
3531                    .iter()
3532                    .rposition(|frame| frame.is_named_savepoint(&rts.name))
3533                else {
3534                    self.local.confidence = Confidence::Tainted;
3535                    if !self.local.transactions.is_empty() {
3536                        self.local.transaction_aborted = true;
3537                    }
3538                    return MutationResult::Conflict {
3539                        reason: format!("savepoint '{}' does not exist", rts.name),
3540                    };
3541                };
3542                let rolled_back = self.local.transactions.split_off(position + 1);
3543                // Frames are popped newest-first. Restore them in that same
3544                // order before restoring changes made after the target
3545                // savepoint itself; undo logs are chronological.
3546                for frame in rolled_back.into_iter().rev() {
3547                    self.rollback_frame(frame);
3548                }
3549                let undo_log = std::mem::take(&mut self.local.transactions[position].undo_log);
3550                self.rollback_undo_log(undo_log);
3551                self.local.transaction_aborted = false;
3552                MutationResult::Applied
3553            }
3554            Mutation::Savepoint(sp) => {
3555                if self.local.transactions.is_empty() {
3556                    self.local.confidence = Confidence::Tainted;
3557                    return MutationResult::Conflict {
3558                        reason: "SAVEPOINT can only be used in transaction blocks".to_string(),
3559                    };
3560                }
3561                self.local
3562                    .transactions
3563                    .push(TransactionFrame::savepoint(sp.name.clone()));
3564                MutationResult::Applied
3565            }
3566            Mutation::ReleaseSavepoint(rsp) => {
3567                let Some(position) = self
3568                    .local
3569                    .transactions
3570                    .iter()
3571                    .rposition(|frame| frame.is_named_savepoint(&rsp.name))
3572                else {
3573                    self.local.confidence = Confidence::Tainted;
3574                    if !self.local.transactions.is_empty() {
3575                        self.local.transaction_aborted = true;
3576                    }
3577                    return MutationResult::Conflict {
3578                        reason: format!("savepoint '{}' does not exist", rsp.name),
3579                    };
3580                };
3581                if position == 0 {
3582                    self.local.confidence = Confidence::Tainted;
3583                    return MutationResult::Conflict {
3584                        reason: format!("savepoint '{}' is not inside a transaction", rsp.name),
3585                    };
3586                }
3587
3588                let released = self.local.transactions.split_off(position);
3589                let outer = self
3590                    .local
3591                    .transactions
3592                    .last_mut()
3593                    .expect("a released savepoint always has an outer transaction frame");
3594                for frame in released {
3595                    outer.undo_log.extend(frame.undo_log);
3596                }
3597                MutationResult::Applied
3598            }
3599            Mutation::Opaque(_) => {
3600                self.snapshot_confidence();
3601                self.local.confidence = Confidence::Tainted;
3602                MutationResult::Applied
3603            }
3604            Mutation::CreateFunction(f) => {
3605                if matches!(
3606                    self.local.functions.get(&f.id),
3607                    Some(crate::model::function::FunctionOverlay::Present(_))
3608                ) && !f.or_replace
3609                {
3610                    return MutationResult::Conflict {
3611                        reason: format!("routine '{}' already exists", f.id),
3612                    };
3613                }
3614                self.snapshot_function(&f.id);
3615                self.snapshot_generation_counter();
3616                self.local.generation_counter += 1;
3617                let _generation = self.local.generation_counter;
3618
3619                let volatility = f
3620                    .options
3621                    .iter()
3622                    .find_map(|opt| {
3623                        if let crate::analysis::facts::FuncOptionFact::Volatility(v) = opt {
3624                            Some(match v {
3625                                crate::analysis::facts::VolatilityKind::Volatile => {
3626                                    crate::model::function::Volatility::Volatile
3627                                }
3628                                crate::analysis::facts::VolatilityKind::Stable => {
3629                                    crate::model::function::Volatility::Stable
3630                                }
3631                                crate::analysis::facts::VolatilityKind::Immutable => {
3632                                    crate::model::function::Volatility::Immutable
3633                                }
3634                            })
3635                        } else {
3636                            None
3637                        }
3638                    })
3639                    .unwrap_or(crate::model::function::Volatility::Volatile);
3640
3641                let security = f
3642                    .options
3643                    .iter()
3644                    .find_map(|opt| {
3645                        if let crate::analysis::facts::FuncOptionFact::Security(s) = opt {
3646                            Some(match s {
3647                                crate::analysis::facts::SecurityKind::Invoker => {
3648                                    crate::model::function::SecurityMode::Invoker
3649                                }
3650                                crate::analysis::facts::SecurityKind::Definer => {
3651                                    crate::model::function::SecurityMode::Definer
3652                                }
3653                            })
3654                        } else {
3655                            None
3656                        }
3657                    })
3658                    .unwrap_or(crate::model::function::SecurityMode::Invoker);
3659
3660                let language = f
3661                    .options
3662                    .iter()
3663                    .find_map(|opt| {
3664                        if let crate::analysis::facts::FuncOptionFact::Language(l) = opt {
3665                            Some(l.clone())
3666                        } else {
3667                            None
3668                        }
3669                    })
3670                    .unwrap_or_else(|| "sql".to_string());
3671
3672                self.local.functions.insert(
3673                    f.id.clone(),
3674                    crate::model::function::FunctionOverlay::Present(
3675                        crate::model::function::FunctionState {
3676                            id: f.id.clone(),
3677                            arg_types: f.params.iter().map(|p| p.ty.clone()).collect(),
3678                            return_type: f
3679                                .return_type
3680                                .as_ref()
3681                                .map(|rt| format!("{:?}", rt))
3682                                .unwrap_or_default(),
3683                            volatility,
3684                            language,
3685                            security,
3686                        },
3687                    ),
3688                );
3689                MutationResult::Applied
3690            }
3691            Mutation::AlterFunction(f) => {
3692                use crate::analysis::facts::{AlterFunctionAction, FuncOptionFact};
3693                use crate::model::function::{FunctionOverlay, SecurityMode, Volatility};
3694
3695                match &f.action {
3696                    AlterFunctionAction::OptionsChange(options) => {
3697                        self.snapshot_function(&f.id);
3698                        if let Some(FunctionOverlay::Present(function)) =
3699                            self.local.functions.get_mut(&f.id)
3700                        {
3701                            for option in options {
3702                                match option {
3703                                    FuncOptionFact::Volatility(volatility) => {
3704                                        function.volatility = match volatility {
3705                                            crate::analysis::facts::VolatilityKind::Volatile => {
3706                                                Volatility::Volatile
3707                                            }
3708                                            crate::analysis::facts::VolatilityKind::Stable => {
3709                                                Volatility::Stable
3710                                            }
3711                                            crate::analysis::facts::VolatilityKind::Immutable => {
3712                                                Volatility::Immutable
3713                                            }
3714                                        };
3715                                    }
3716                                    FuncOptionFact::Security(security) => {
3717                                        function.security = match security {
3718                                            crate::analysis::facts::SecurityKind::Invoker => {
3719                                                SecurityMode::Invoker
3720                                            }
3721                                            crate::analysis::facts::SecurityKind::Definer => {
3722                                                SecurityMode::Definer
3723                                            }
3724                                        };
3725                                    }
3726                                    FuncOptionFact::Language(language) => {
3727                                        function.language = language.clone();
3728                                    }
3729                                    _ => {}
3730                                }
3731                            }
3732                        }
3733                    }
3734                    AlterFunctionAction::Rename { to, .. } => {
3735                        let signature =
3736                            f.id.name
3737                                .find('(')
3738                                .map(|index| &f.id.name[index..])
3739                                .unwrap_or("");
3740                        let new_id = ObjectId::new(f.id.schema.clone(), format!("{to}{signature}"));
3741                        self.move_function(&f.id, &new_id);
3742                    }
3743                    AlterFunctionAction::SchemaChange { new_schema } => {
3744                        let new_id = ObjectId::new(new_schema.clone(), f.id.name.clone());
3745                        self.move_function(&f.id, &new_id);
3746                    }
3747                    AlterFunctionAction::OwnerChange(_)
3748                    | AlterFunctionAction::DependsOnExtension { .. }
3749                    | AlterFunctionAction::NoDependsOnExtension { .. } => {
3750                        self.snapshot_function(&f.id);
3751                    }
3752                }
3753                MutationResult::Applied
3754            }
3755            Mutation::DropFunction(f) => {
3756                let mut any_applied = false;
3757                for sig in &f.signatures {
3758                    let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
3759                    let schema = self.resolve_function_schema(&sig.name, &sig_str);
3760                    let id = ObjectId::new(schema, sig_str);
3761                    if !matches!(
3762                        self.local.functions.get(&id),
3763                        Some(crate::model::function::FunctionOverlay::Present(_))
3764                    ) {
3765                        if !f.if_exists {
3766                            return MutationResult::Conflict {
3767                                reason: format!("function '{}' does not exist", id),
3768                            };
3769                        }
3770                    } else {
3771                        let dependent_triggers: Vec<(ObjectId, ObjectId)> = self
3772                            .local
3773                            .graph
3774                            .edges
3775                            .iter()
3776                            .filter_map(|edge| {
3777                                let DependencyKind::TriggerOnTable { function_id, .. } = &edge.kind
3778                                else {
3779                                    return None;
3780                                };
3781                                (function_id == &id)
3782                                    .then(|| (edge.dependent.clone(), edge.referenced.clone()))
3783                            })
3784                            .collect();
3785                        if !dependent_triggers.is_empty() && !f.cascade {
3786                            return MutationResult::Conflict {
3787                                reason: format!(
3788                                    "function '{}' still has dependent triggers; use CASCADE",
3789                                    id
3790                                ),
3791                            };
3792                        }
3793
3794                        any_applied = true;
3795                        self.snapshot_function(&id);
3796                        self.local
3797                            .functions
3798                            .insert(id.clone(), crate::model::function::FunctionOverlay::Dropped);
3799
3800                        if f.cascade {
3801                            for (trigger_id, table_id) in &dependent_triggers {
3802                                let trigger_name =
3803                                    self.local.triggers.get(trigger_id).and_then(|overlay| {
3804                                        match overlay {
3805                                            TriggerOverlay::Present(trigger) => {
3806                                                Some(trigger.name.clone())
3807                                            }
3808                                            TriggerOverlay::Dropped => None,
3809                                        }
3810                                    });
3811                                self.snapshot_trigger(trigger_id);
3812                                self.local
3813                                    .triggers
3814                                    .insert(trigger_id.clone(), TriggerOverlay::Dropped);
3815                                self.snapshot_relation(table_id);
3816                                if let Some(RelationOverlay::Present(relation)) =
3817                                    self.local.relations.get_mut(table_id)
3818                                {
3819                                    if let Some(trigger_name) = trigger_name {
3820                                        relation.triggers.remove(&trigger_name);
3821                                    }
3822                                }
3823                            }
3824                            if !dependent_triggers.is_empty() {
3825                                self.snapshot_graph_full();
3826                                self.local.graph.edges.retain(|edge| {
3827                                    !dependent_triggers
3828                                        .iter()
3829                                        .any(|(trigger_id, _)| edge.dependent == *trigger_id)
3830                                });
3831                            }
3832                        }
3833                    }
3834                }
3835                if any_applied {
3836                    MutationResult::Applied
3837                } else {
3838                    MutationResult::Skipped
3839                }
3840            }
3841            Mutation::CreateProcedure(p) => {
3842                if matches!(
3843                    self.local.functions.get(&p.id),
3844                    Some(crate::model::function::FunctionOverlay::Present(_))
3845                ) && !p.or_replace
3846                {
3847                    return MutationResult::Conflict {
3848                        reason: format!("routine '{}' already exists", p.id),
3849                    };
3850                }
3851                self.snapshot_function(&p.id);
3852                self.snapshot_generation_counter();
3853                self.local.generation_counter += 1;
3854                let _generation = self.local.generation_counter;
3855
3856                self.local.functions.insert(
3857                    p.id.clone(),
3858                    crate::model::function::FunctionOverlay::Present(
3859                        crate::model::function::FunctionState {
3860                            id: p.id.clone(),
3861                            arg_types: p.params.iter().map(|p| p.ty.clone()).collect(),
3862                            return_type: "void".to_string(),
3863                            volatility: crate::model::function::Volatility::Volatile,
3864                            language: "sql".to_string(),
3865                            security: crate::model::function::SecurityMode::Invoker,
3866                        },
3867                    ),
3868                );
3869                MutationResult::Applied
3870            }
3871            Mutation::AlterProcedure(p) => {
3872                self.snapshot_function(&p.id);
3873                // No generation tracking in FunctionState
3874                MutationResult::Applied
3875            }
3876            Mutation::DropProcedure(p) => {
3877                let mut any_applied = false;
3878                for sig in &p.signatures {
3879                    let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
3880                    let schema = self.resolve_function_schema(&sig.name, &sig_str);
3881                    let id = ObjectId::new(schema, sig_str);
3882                    if !matches!(
3883                        self.local.functions.get(&id),
3884                        Some(crate::model::function::FunctionOverlay::Present(_))
3885                    ) {
3886                        if !p.if_exists {
3887                            return MutationResult::Conflict {
3888                                reason: format!("procedure '{}' does not exist", id),
3889                            };
3890                        }
3891                    } else {
3892                        any_applied = true;
3893                        self.snapshot_function(&id);
3894                        self.local
3895                            .functions
3896                            .insert(id, crate::model::function::FunctionOverlay::Dropped);
3897                    }
3898                }
3899                if any_applied {
3900                    MutationResult::Applied
3901                } else {
3902                    MutationResult::Skipped
3903                }
3904            }
3905            Mutation::CreatePublication(p) => {
3906                self.snapshot_publication(&p.name);
3907                self.snapshot_generation_counter();
3908                self.local.generation_counter += 1;
3909                let generation = self.local.generation_counter;
3910
3911                self.local.publications.insert(
3912                    p.name.clone(),
3913                    crate::model::replication::PublicationOverlay::Present(
3914                        crate::model::replication::PublicationState {
3915                            name: p.name.clone(),
3916                            scope: p.scope.clone(),
3917                            params: p.params.clone(),
3918                            generation,
3919                        },
3920                    ),
3921                );
3922
3923                if let crate::analysis::facts::PublicationScope::Explicit(objects) = &p.scope {
3924                    self.snapshot_graph_full();
3925                    for obj in objects {
3926                        if let crate::analysis::facts::PublicationObjectFact::Table {
3927                            name, ..
3928                        } = obj
3929                        {
3930                            let table_id = self.resolve_relation_id(name);
3931                            self.local.graph.edges.push(DependencyEdge::new(
3932                                table_id,
3933                                ObjectId::new("public", &p.name),
3934                                DependencyKind::PublicationIncludes {
3935                                    publication_name: p.name.clone(),
3936                                },
3937                            ));
3938                        }
3939                    }
3940                }
3941                MutationResult::Applied
3942            }
3943            Mutation::AlterPublication(p) => {
3944                self.snapshot_publication(&p.name);
3945                if !self.local.publications.contains_key(&p.name) {
3946                    self.local.confidence = Confidence::Tainted;
3947                    return MutationResult::Skipped;
3948                }
3949                self.snapshot_generation_counter();
3950                self.local.generation_counter += 1;
3951                let new_gen = self.local.generation_counter;
3952
3953                if let Some(crate::model::replication::PublicationOverlay::Present(publ)) =
3954                    self.local.publications.get_mut(&p.name)
3955                {
3956                    publ.generation = new_gen;
3957                }
3958                MutationResult::Applied
3959            }
3960            Mutation::DropPublication(p) => {
3961                for name in &p.names {
3962                    self.snapshot_publication(name);
3963                    if !p.if_exists && !self.local.publications.contains_key(name) {
3964                        self.local.confidence = Confidence::Tainted;
3965                        return MutationResult::Skipped;
3966                    }
3967                    self.local.publications.insert(
3968                        name.clone(),
3969                        crate::model::replication::PublicationOverlay::Dropped,
3970                    );
3971                }
3972                self.snapshot_graph_full();
3973                self.local.graph.edges.retain(|e| {
3974                    !(matches!(e.kind, DependencyKind::PublicationIncludes { .. })
3975                        && p.names.contains(&e.referenced.name))
3976                });
3977                MutationResult::Applied
3978            }
3979            Mutation::CreateSubscription(s) => {
3980                let name = s.name.clone().unwrap_or_else(|| "unnamed_sub".into());
3981                self.snapshot_subscription(&name);
3982                self.snapshot_generation_counter();
3983                self.local.generation_counter += 1;
3984                let generation = self.local.generation_counter;
3985
3986                self.local.subscriptions.insert(
3987                    name.clone(),
3988                    crate::model::replication::SubscriptionOverlay::Present(
3989                        crate::model::replication::SubscriptionState {
3990                            name,
3991                            connection: s.connection.clone(),
3992                            publications: s.publications.clone(),
3993                            params: s.params.clone(),
3994                            generation,
3995                        },
3996                    ),
3997                );
3998                MutationResult::Applied
3999            }
4000            Mutation::AlterSubscription(s) => {
4001                self.snapshot_subscription(&s.name);
4002                if !self.local.subscriptions.contains_key(&s.name) {
4003                    self.local.confidence = Confidence::Tainted;
4004                    return MutationResult::Skipped;
4005                }
4006                self.snapshot_generation_counter();
4007                self.local.generation_counter += 1;
4008                let new_gen = self.local.generation_counter;
4009
4010                if let Some(crate::model::replication::SubscriptionOverlay::Present(sub)) =
4011                    self.local.subscriptions.get_mut(&s.name)
4012                {
4013                    sub.generation = new_gen;
4014                }
4015                MutationResult::Applied
4016            }
4017            Mutation::DropSubscription(s) => {
4018                self.snapshot_subscription(&s.name);
4019                if !s.if_exists && !self.local.subscriptions.contains_key(&s.name) {
4020                    self.local.confidence = Confidence::Tainted;
4021                    return MutationResult::Skipped;
4022                }
4023                self.local.subscriptions.insert(
4024                    s.name.clone(),
4025                    crate::model::replication::SubscriptionOverlay::Dropped,
4026                );
4027                MutationResult::Applied
4028            }
4029            Mutation::CreateRole(r) => {
4030                let role_id = ObjectId::new("", &r.name);
4031                if matches!(
4032                    self.local.roles.get(&role_id),
4033                    Some(crate::model::role::RoleOverlay::Present(_))
4034                ) {
4035                    return MutationResult::Conflict {
4036                        reason: format!("role '{}' already exists", r.name),
4037                    };
4038                }
4039                self.snapshot_role(&role_id);
4040                self.snapshot_generation_counter();
4041                self.local.generation_counter += 1;
4042                let _generation = self.local.generation_counter;
4043
4044                self.local.roles.insert(
4045                    role_id.clone(),
4046                    crate::model::role::RoleOverlay::Present(crate::model::role::RoleState {
4047                        id: role_id,
4048                        can_login: true,
4049                        is_superuser: false,
4050                        member_of: Vec::new(),
4051                        can_set_role_to: Vec::new(),
4052                        granted_privileges: Vec::new(),
4053                    }),
4054                );
4055                MutationResult::Applied
4056            }
4057            Mutation::AlterRole(r) => {
4058                if let Some(role_id) = Self::resolve_role_name(
4059                    &r.name,
4060                    &self.local.current_role,
4061                    &self.local.session_role,
4062                ) {
4063                    self.snapshot_role(&role_id);
4064                    if !self.local.roles.contains_key(&role_id) {
4065                        self.local.confidence = Confidence::Tainted;
4066                        return MutationResult::Skipped;
4067                    }
4068                    self.snapshot_generation_counter();
4069                    self.local.generation_counter += 1;
4070                    let _new_gen = self.local.generation_counter;
4071
4072                    if let Some(crate::model::role::RoleOverlay::Present(_role)) =
4073                        self.local.roles.get_mut(&role_id)
4074                    {
4075                        // No further action as fields have been simplified
4076                    }
4077                    MutationResult::Applied
4078                } else {
4079                    MutationResult::Skipped
4080                }
4081            }
4082            Mutation::DropRole(r) => {
4083                for name in &r.names {
4084                    if let Some(role_id) = Self::resolve_role_name(
4085                        &crate::analysis::facts::RoleFact::Named {
4086                            name: name.clone(),
4087                            via_legacy_group_syntax: false,
4088                        },
4089                        &self.local.current_role,
4090                        &self.local.session_role,
4091                    ) {
4092                        self.snapshot_role(&role_id);
4093                        if !r.if_exists
4094                            && !matches!(
4095                                self.local.roles.get(&role_id),
4096                                Some(crate::model::role::RoleOverlay::Present(_))
4097                            )
4098                        {
4099                            return MutationResult::Conflict {
4100                                reason: format!("role '{}' does not exist", name),
4101                            };
4102                        }
4103                        self.local
4104                            .roles
4105                            .insert(role_id, crate::model::role::RoleOverlay::Dropped);
4106                    }
4107                }
4108                MutationResult::Applied
4109            }
4110            Mutation::Grant(grant) => {
4111                let privileges = Self::resolve_grant_privileges(&grant.privileges);
4112                let grantees = &grant.grantees;
4113                match &grant.target {
4114                    crate::analysis::mutations::ResolvedGrantTarget::Tables(ids) => {
4115                        for id in ids {
4116                            self.apply_grant_to_relation(id, &privileges, grantees);
4117                        }
4118                    }
4119                    crate::analysis::mutations::ResolvedGrantTarget::AllTablesInSchema(schemas) => {
4120                        let target_ids: Vec<ObjectId> = self
4121                            .local
4122                            .relations
4123                            .keys()
4124                            .filter(|id| schemas.contains(&id.schema))
4125                            .cloned()
4126                            .collect();
4127                        for id in &target_ids {
4128                            self.apply_grant_to_relation(id, &privileges, grantees);
4129                        }
4130                    }
4131                }
4132                MutationResult::Applied
4133            }
4134            Mutation::Revoke(revoke) => {
4135                let privileges = Self::resolve_grant_privileges(&revoke.privileges);
4136                let revokees = &revoke.revokees;
4137                match &revoke.target {
4138                    crate::analysis::mutations::ResolvedGrantTarget::Tables(ids) => {
4139                        for id in ids {
4140                            self.apply_revoke_to_relation(id, &privileges, revokees);
4141                        }
4142                    }
4143                    crate::analysis::mutations::ResolvedGrantTarget::AllTablesInSchema(schemas) => {
4144                        let target_ids: Vec<ObjectId> = self
4145                            .local
4146                            .relations
4147                            .keys()
4148                            .filter(|id| schemas.contains(&id.schema))
4149                            .cloned()
4150                            .collect();
4151                        for id in &target_ids {
4152                            self.apply_revoke_to_relation(id, &privileges, revokees);
4153                        }
4154                    }
4155                }
4156                MutationResult::Applied
4157            }
4158            Mutation::CreateDatabase(_) => MutationResult::Applied,
4159            Mutation::AlterDatabase(_) => MutationResult::Applied,
4160            Mutation::DropDatabase(_) => MutationResult::Applied,
4161            Mutation::Vacuum { .. } => MutationResult::Applied,
4162        }
4163    }
4164
4165    fn snapshot_relation(&mut self, id: &ObjectId) {
4166        if let Some(frame) = self.local.transactions.last_mut() {
4167            let previous = self.local.relations.get(id).cloned();
4168            frame.undo_log.push(StateChange::RelationSnapshot {
4169                id: id.clone(),
4170                previous: Box::new(previous),
4171            });
4172        }
4173    }
4174
4175    fn snapshot_schema(&mut self, name: &str) {
4176        if let Some(frame) = self.local.transactions.last_mut() {
4177            frame.undo_log.push(StateChange::SchemaSnapshot {
4178                name: name.to_string(),
4179                previous: self.local.schemas.get(name).cloned(),
4180            });
4181        }
4182    }
4183
4184    fn snapshot_namespace(&mut self) {
4185        if let Some(frame) = self.local.transactions.last_mut() {
4186            frame.undo_log.push(StateChange::NamespaceSnapshot(Box::new(
4187                NamespaceSnapshot {
4188                    schemas: self.local.schemas.clone(),
4189                    relations: self.local.relations.clone(),
4190                    types: self.local.types.clone(),
4191                    functions: self.local.functions.clone(),
4192                    sequences: self.local.sequences.clone(),
4193                    publications: self.local.publications.clone(),
4194                    triggers: self.local.triggers.clone(),
4195                    constraints: self.local.constraints.clone(),
4196                    graph: self.local.graph.edges.clone(),
4197                    pending_validation: self.local.pending_validation.clone(),
4198                    baseline_relations: self.baseline_relations.clone(),
4199                    baseline_indexes: self.baseline_indexes.clone(),
4200                    baseline_foreign_keys: self.baseline_foreign_keys.clone(),
4201                    baseline_fk_dependencies: self.baseline_fk_dependencies.clone(),
4202                    baseline_sequences: self.baseline_sequences.clone(),
4203                },
4204            )));
4205        }
4206    }
4207
4208    fn snapshot_type(&mut self, id: &ObjectId) {
4209        if let Some(frame) = self.local.transactions.last_mut() {
4210            let previous = self.local.types.get(id).cloned();
4211            frame.undo_log.push(StateChange::TypeSnapshot {
4212                id: id.clone(),
4213                previous,
4214            });
4215        }
4216    }
4217
4218    fn snapshot_sequence(&mut self, id: &ObjectId) {
4219        if let Some(frame) = self.local.transactions.last_mut() {
4220            let previous = self.local.sequences.get(id).cloned();
4221            frame.undo_log.push(StateChange::SequenceSnapshot {
4222                id: id.clone(),
4223                previous,
4224            });
4225        }
4226    }
4227
4228    fn move_function(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
4229        self.snapshot_function(old_id);
4230        self.snapshot_function(new_id);
4231        if let Some(crate::model::function::FunctionOverlay::Present(mut function)) =
4232            self.local.functions.remove(old_id)
4233        {
4234            function.id = new_id.clone();
4235            self.local.functions.insert(
4236                new_id.clone(),
4237                crate::model::function::FunctionOverlay::Present(function),
4238            );
4239        }
4240
4241        self.snapshot_graph_full();
4242        self.local.graph.propagate_rename(old_id, new_id);
4243        self.local.graph.edges.push(DependencyEdge::new(
4244            old_id.clone(),
4245            new_id.clone(),
4246            DependencyKind::RenameTo,
4247        ));
4248    }
4249
4250    fn snapshot_function(&mut self, id: &ObjectId) {
4251        if let Some(frame) = self.local.transactions.last_mut() {
4252            let previous = self.local.functions.get(id).cloned();
4253            frame.undo_log.push(StateChange::FunctionSnapshot {
4254                id: id.clone(),
4255                previous,
4256            });
4257        }
4258    }
4259
4260    fn snapshot_publication(&mut self, name: &str) {
4261        if let Some(frame) = self.local.transactions.last_mut() {
4262            let previous = self.local.publications.get(name).cloned();
4263            frame.undo_log.push(StateChange::PublicationSnapshot {
4264                id: ObjectId::new("", name),
4265                previous,
4266            });
4267        }
4268    }
4269
4270    fn snapshot_subscription(&mut self, name: &str) {
4271        if let Some(frame) = self.local.transactions.last_mut() {
4272            let previous = self.local.subscriptions.get(name).cloned();
4273            frame.undo_log.push(StateChange::SubscriptionSnapshot {
4274                id: ObjectId::new("", name),
4275                previous,
4276            });
4277        }
4278    }
4279
4280    fn snapshot_role(&mut self, id: &ObjectId) {
4281        if let Some(frame) = self.local.transactions.last_mut() {
4282            let previous = self.local.roles.get(id).cloned();
4283            frame.undo_log.push(StateChange::RoleSnapshot {
4284                id: id.clone(),
4285                previous,
4286            });
4287        }
4288    }
4289
4290    fn snapshot_trigger(&mut self, id: &ObjectId) {
4291        if let Some(frame) = self.local.transactions.last_mut() {
4292            let previous = self.local.triggers.get(id).cloned();
4293            frame.undo_log.push(StateChange::TriggerSnapshot {
4294                id: id.clone(),
4295                previous,
4296            });
4297        }
4298    }
4299
4300    fn snapshot_constraint(&mut self, table_id: &ObjectId, name: &str) {
4301        if let Some(frame) = self.local.transactions.last_mut() {
4302            let key = (table_id.clone(), name.to_string());
4303            let previous = self.local.constraints.get(&key).cloned();
4304            frame.undo_log.push(StateChange::ConstraintSnapshot {
4305                table_id: table_id.clone(),
4306                name: name.to_string(),
4307                previous,
4308            });
4309        }
4310    }
4311
4312    fn snapshot_role_context(&mut self) {
4313        if let Some(frame) = self.local.transactions.last_mut() {
4314            frame.undo_log.push(StateChange::RoleContextSnapshot {
4315                current_role: self.local.current_role.clone(),
4316                current_role_known: self.local.current_role_known,
4317                persistent_current_role: self.local.persistent_current_role.clone(),
4318                persistent_current_role_known: self.local.persistent_current_role_known,
4319                session_role: self.local.session_role.clone(),
4320                session_role_known: self.local.session_role_known,
4321                persistent_session_role: self.local.persistent_session_role.clone(),
4322                persistent_session_role_known: self.local.persistent_session_role_known,
4323            });
4324        }
4325    }
4326
4327    fn snapshot_search_path(&mut self) {
4328        if let Some(frame) = self.local.transactions.last_mut() {
4329            frame.undo_log.push(StateChange::SearchPathSnapshot {
4330                previous: self.local.search_path.clone(),
4331                previous_template: self.local.search_path_template.clone(),
4332            });
4333        }
4334    }
4335
4336    fn snapshot_generation_counter(&mut self) {
4337        if let Some(frame) = self.local.transactions.last_mut() {
4338            frame.undo_log.push(StateChange::GenerationCounterSnapshot {
4339                previous: self.local.generation_counter,
4340            });
4341        }
4342    }
4343
4344    #[allow(dead_code)]
4345    fn snapshot_pending_validation(&mut self) {
4346        if let Some(frame) = self.local.transactions.last_mut() {
4347            frame.undo_log.push(StateChange::PendingValidationSnapshot {
4348                previous: self.local.pending_validation.clone(),
4349            });
4350        }
4351    }
4352
4353    fn snapshot_confidence(&mut self) {
4354        if let Some(frame) = self.local.transactions.last_mut() {
4355            frame.undo_log.push(StateChange::ConfidenceSnapshot {
4356                previous: self.local.confidence.clone(),
4357            });
4358        }
4359    }
4360
4361    fn snapshot_graph(&mut self) {
4362        if let Some(frame) = self.local.transactions.last_mut() {
4363            frame.undo_log.push(StateChange::GraphLengthMarker {
4364                len: self.local.graph.edges.len(),
4365            });
4366        }
4367    }
4368
4369    fn snapshot_graph_full(&mut self) {
4370        if let Some(frame) = self.local.transactions.last_mut() {
4371            frame.undo_log.push(StateChange::GraphSnapshot {
4372                previous: self.local.graph.edges.clone(),
4373            });
4374        }
4375    }
4376
4377    fn rollback_frame(&mut self, mut frame: TransactionFrame) {
4378        self.rollback_undo_log(std::mem::take(&mut frame.undo_log));
4379    }
4380
4381    fn rollback_undo_log(&mut self, mut undo_log: Vec<StateChange>) {
4382        while let Some(change) = undo_log.pop() {
4383            match change {
4384                StateChange::SchemaSnapshot { name, previous } => match previous {
4385                    Some(overlay) => {
4386                        self.local.schemas.insert(name, overlay);
4387                    }
4388                    None => {
4389                        self.local.schemas.remove(&name);
4390                    }
4391                },
4392                StateChange::NamespaceSnapshot(snapshot) => {
4393                    self.local.schemas = snapshot.schemas;
4394                    self.local.relations = snapshot.relations;
4395                    self.local.types = snapshot.types;
4396                    self.local.functions = snapshot.functions;
4397                    self.local.sequences = snapshot.sequences;
4398                    self.local.publications = snapshot.publications;
4399                    self.local.triggers = snapshot.triggers;
4400                    self.local.constraints = snapshot.constraints;
4401                    self.local.graph.edges = snapshot.graph;
4402                    self.local.pending_validation = snapshot.pending_validation;
4403                    self.baseline_relations = snapshot.baseline_relations;
4404                    self.baseline_indexes = snapshot.baseline_indexes;
4405                    self.baseline_foreign_keys = snapshot.baseline_foreign_keys;
4406                    self.baseline_fk_dependencies = snapshot.baseline_fk_dependencies;
4407                    self.baseline_sequences = snapshot.baseline_sequences;
4408                }
4409                StateChange::RelationSnapshot { id, previous } => {
4410                    if let Some(prev) = *previous {
4411                        self.local.relations.insert(id, prev);
4412                    } else {
4413                        self.local.relations.remove(&id);
4414                    }
4415                }
4416                StateChange::TypeSnapshot { id, previous } => {
4417                    if let Some(prev) = previous {
4418                        self.local.types.insert(id, prev);
4419                    } else {
4420                        self.local.types.remove(&id);
4421                    }
4422                }
4423                StateChange::SequenceSnapshot { id, previous } => {
4424                    if let Some(prev) = previous {
4425                        self.local.sequences.insert(id, prev);
4426                    } else {
4427                        self.local.sequences.remove(&id);
4428                    }
4429                }
4430                StateChange::FunctionSnapshot { id, previous } => {
4431                    if let Some(prev) = previous {
4432                        self.local.functions.insert(id, prev);
4433                    } else {
4434                        self.local.functions.remove(&id);
4435                    }
4436                }
4437                StateChange::PublicationSnapshot { id, previous } => {
4438                    if let Some(prev) = previous {
4439                        self.local.publications.insert(id.name, prev);
4440                    } else {
4441                        self.local.publications.remove(&id.name);
4442                    }
4443                }
4444                StateChange::SubscriptionSnapshot { id, previous } => {
4445                    if let Some(prev) = previous {
4446                        self.local.subscriptions.insert(id.name, prev);
4447                    } else {
4448                        self.local.subscriptions.remove(&id.name);
4449                    }
4450                }
4451                StateChange::RoleSnapshot { id, previous } => {
4452                    if let Some(prev) = previous {
4453                        self.local.roles.insert(id, prev);
4454                    } else {
4455                        self.local.roles.remove(&id);
4456                    }
4457                }
4458                StateChange::TriggerSnapshot { id, previous } => {
4459                    if let Some(prev) = previous {
4460                        self.local.triggers.insert(id, prev);
4461                    } else {
4462                        self.local.triggers.remove(&id);
4463                    }
4464                }
4465                StateChange::ConstraintSnapshot {
4466                    table_id,
4467                    name,
4468                    previous,
4469                } => {
4470                    let key = (table_id, name);
4471                    if let Some(previous) = previous {
4472                        self.local.constraints.insert(key, previous);
4473                    } else {
4474                        self.local.constraints.remove(&key);
4475                    }
4476                }
4477                StateChange::GraphLengthMarker { len } => {
4478                    self.local.graph.edges.truncate(len);
4479                }
4480                StateChange::GraphSnapshot { previous } => {
4481                    self.local.graph.edges = previous;
4482                }
4483                StateChange::RoleContextSnapshot {
4484                    current_role,
4485                    current_role_known,
4486                    persistent_current_role,
4487                    persistent_current_role_known,
4488                    session_role,
4489                    session_role_known,
4490                    persistent_session_role,
4491                    persistent_session_role_known,
4492                } => {
4493                    self.local.current_role = current_role;
4494                    self.local.current_role_known = current_role_known;
4495                    self.local.persistent_current_role = persistent_current_role;
4496                    self.local.persistent_current_role_known = persistent_current_role_known;
4497                    self.local.session_role = session_role;
4498                    self.local.session_role_known = session_role_known;
4499                    self.local.persistent_session_role = persistent_session_role;
4500                    self.local.persistent_session_role_known = persistent_session_role_known;
4501                }
4502                StateChange::SearchPathSnapshot {
4503                    previous,
4504                    previous_template,
4505                } => {
4506                    self.local.search_path = previous;
4507                    self.local.search_path_template = previous_template;
4508                }
4509                StateChange::GenerationCounterSnapshot { previous } => {
4510                    self.local.generation_counter = previous;
4511                }
4512                StateChange::PendingValidationSnapshot { previous } => {
4513                    self.local.pending_validation = previous;
4514                }
4515                StateChange::ConfidenceSnapshot { previous } => {
4516                    self.local.confidence = previous;
4517                }
4518            }
4519        }
4520    }
4521}