Skip to main content

safe_migrate/analysis/
state.rs

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