Skip to main content

safe_migrate/analysis/
state.rs

1use crate::analysis::facts::{
2    ResetSettingTarget, SearchPathTarget, TableConstraintFact, TimeoutSetting, TimeoutSettingValue,
3};
4use crate::analysis::graph::{DependencyEdge, DependencyGraph, DependencyKind};
5use crate::analysis::mutations::{
6    AlterTableActionMutation, AlterTypeActionMutation, Mutation, PersistenceMutation,
7};
8use crate::analysis::settings::ScopedSetting;
9use crate::analysis::transaction::{NamespaceSnapshot, StateChange, TransactionFrame};
10use crate::ast::identifiers::ObjectId;
11use crate::db::cache::DbCache;
12use crate::model::constraint::{ConstraintKind, ConstraintState};
13pub use crate::model::relation::RelationOverlay;
14use crate::model::relation::{ColumnAction, Persistence, Privilege, RelationKind, RelationState};
15use crate::model::schema::SchemaOverlay;
16use crate::model::sequence::{SequenceKind, SequenceOverlay, SequenceState};
17use crate::model::trigger::TriggerOverlay;
18use crate::model::types::{TypeKind, TypeOverlay, TypeState};
19use std::collections::{HashMap, HashSet};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum Confidence {
23    Exact,
24    Tainted,
25}
26
27#[derive(Debug, PartialEq, Eq)]
28pub enum MutationResult {
29    Applied,
30    Skipped,
31    /// PostgreSQL did not execute this statement because an earlier statement
32    /// aborted the active transaction.
33    NotExecuted,
34    Conflict {
35        reason: String,
36    },
37}
38
39#[derive(Debug, Default, Clone)]
40pub struct CascadeResult {
41    pub dropped_relations: HashSet<ObjectId>,
42    pub dropped_indexes: HashSet<ObjectId>,
43    pub dropped_constraints: HashSet<(ObjectId, String)>,
44}
45
46#[derive(Clone)]
47pub struct LocalState {
48    pub schemas: HashMap<String, SchemaOverlay>,
49    pub relations: HashMap<ObjectId, RelationOverlay>,
50    pub types: HashMap<ObjectId, TypeOverlay>,
51    pub functions: HashMap<ObjectId, crate::model::function::FunctionOverlay>,
52    pub sequences: HashMap<ObjectId, SequenceOverlay>,
53    pub publications: HashMap<String, crate::model::replication::PublicationOverlay>,
54    pub subscriptions: HashMap<String, crate::model::replication::SubscriptionOverlay>,
55    pub roles: HashMap<ObjectId, crate::model::role::RoleOverlay>,
56    pub triggers: HashMap<ObjectId, TriggerOverlay>,
57    pub constraints: HashMap<(ObjectId, String), ConstraintState>,
58    pub graph: DependencyGraph,
59    pub search_path: Vec<String>,
60    pub default_search_path: Vec<String>,
61    pub search_path_template: Vec<String>,
62    pub session_search_path_template: Vec<String>,
63    pub default_search_path_template: Vec<String>,
64    pub lock_timeout: ScopedSetting<Option<u64>>,
65    pub statement_timeout: ScopedSetting<Option<u64>>,
66    /// Role currently active for this session context (updated by SET ROLE /
67    /// SET SESSION AUTHORIZATION). Begins equal to `session_role`.
68    pub current_role: String,
69    /// Whether `current_role` is statically known. False when no synchronized cache was
70    /// loaded and no SET ROLE statement has been processed yet.
71    pub current_role_known: bool,
72    /// Effective role setting that survives transaction commit. A LOCAL role
73    /// change updates `current_role` without changing this value.
74    pub persistent_current_role: String,
75    pub persistent_current_role_known: bool,
76    /// The session-level role as captured from the cache's `source_role`. This
77    /// is the value `SET ROLE NONE` / `SET SESSION AUTHORIZATION DEFAULT`
78    /// reverts to.
79    pub session_role: String,
80    /// Whether `session_role` is statically known (mirrors `current_role_known`
81    /// at baseline; a SET SESSION AUTHORIZATION updates this too).
82    pub session_role_known: bool,
83    /// Session authorization that survives transaction commit.
84    pub persistent_session_role: String,
85    pub persistent_session_role_known: bool,
86    /// Login identity restored by `SET SESSION AUTHORIZATION DEFAULT`.
87    pub authenticated_role: String,
88    pub authenticated_role_known: bool,
89    /// Whether the cache contains a complete PostgreSQL role catalog.
90    pub roles_known: bool,
91    pub confidence: Confidence,
92    pub transactions: Vec<TransactionFrame>,
93    pub transaction_aborted: bool,
94    pub pending_validation: HashSet<(ObjectId, String)>,
95    pub generation_counter: u64,
96}
97
98#[derive(Clone, Debug)]
99pub struct PreState {
100    pub relations: HashMap<ObjectId, crate::model::relation::RelationState>,
101    pub functions: HashMap<ObjectId, crate::model::function::FunctionState>,
102    pub roles: HashMap<ObjectId, crate::model::role::RoleState>,
103    pub publications: HashMap<String, crate::model::replication::PublicationState>,
104    pub subscriptions: HashMap<String, crate::model::replication::SubscriptionState>,
105    pub sequences: HashMap<ObjectId, crate::model::sequence::SequenceState>,
106    pub types: HashMap<ObjectId, crate::model::types::TypeState>,
107    pub indexes: Vec<crate::analysis::graph::DependencyEdge>,
108}
109
110#[derive(Clone)]
111pub struct AnalysisState {
112    pub pg_version_num: Option<u32>,
113    /// Whether the initial cache was loaded from a real cache file. An empty
114    /// cache can be a valid baseline for an empty database, so availability
115    /// must not be inferred from the number of modeled objects.
116    pub baseline_available: bool,
117    /// `None` means the cache covered all non-system schemas. A populated set
118    /// records an explicitly scoped sync, for which objects outside the set
119    /// are unknown rather than known absent.
120    pub baseline_schemas: Option<HashSet<String>>,
121    pub baseline_relations: HashSet<ObjectId>,
122    pub baseline_indexes: HashSet<ObjectId>,
123    pub baseline_foreign_keys: HashSet<(ObjectId, String)>,
124    pub baseline_fk_dependencies: HashSet<ObjectId>,
125    pub baseline_sequences: HashSet<ObjectId>,
126    pub local: LocalState,
127}
128
129impl AnalysisState {
130    fn trigger_key(table_id: &ObjectId, name: &str) -> ObjectId {
131        // PostgreSQL identifiers cannot contain NUL, so this is an unambiguous
132        // internal composite key while keeping the public cache representation
133        // as the trigger's actual name.
134        ObjectId::new(&table_id.schema, format!("{}\0{name}", table_id.name))
135    }
136
137    fn publication_object_key(
138        &self,
139        object: &crate::analysis::facts::PublicationObjectFact,
140    ) -> String {
141        match object {
142            crate::analysis::facts::PublicationObjectFact::Table { name, .. } => {
143                format!("table\0{}", self.resolve_relation_id(name))
144            }
145            crate::analysis::facts::PublicationObjectFact::SchemaTables { schema, .. } => {
146                format!("schema\0{schema}")
147            }
148            crate::analysis::facts::PublicationObjectFact::CurrentSchemaShorthand => {
149                format!(
150                    "schema\0{}",
151                    self.local
152                        .search_path
153                        .first()
154                        .map(String::as_str)
155                        .unwrap_or("public")
156                )
157            }
158            crate::analysis::facts::PublicationObjectFact::Unknown => "unknown".to_string(),
159        }
160    }
161
162    fn replace_publication_edges(
163        &mut self,
164        publication_name: &str,
165        scope: &crate::analysis::facts::PublicationScope,
166    ) {
167        self.snapshot_graph_full();
168        self.local.graph.edges.retain(|edge| {
169            !matches!(
170                &edge.kind,
171                DependencyKind::PublicationIncludes { publication_name: name }
172                    if name == publication_name
173            )
174        });
175        if let crate::analysis::facts::PublicationScope::Explicit(objects) = scope {
176            for object in objects {
177                if let crate::analysis::facts::PublicationObjectFact::Table { name, .. } = object {
178                    self.local.graph.edges.push(DependencyEdge::new(
179                        self.resolve_relation_id(name),
180                        ObjectId::new("public", publication_name),
181                        DependencyKind::PublicationIncludes {
182                            publication_name: publication_name.to_string(),
183                        },
184                    ));
185                }
186            }
187        }
188    }
189
190    fn validate_publication_scope(
191        &mut self,
192        scope: &crate::analysis::facts::PublicationScope,
193    ) -> Result<(), String> {
194        let crate::analysis::facts::PublicationScope::Explicit(objects) = scope else {
195            return Ok(());
196        };
197        let mut object_keys = HashSet::new();
198        for object in objects {
199            if !object_keys.insert(self.publication_object_key(object)) {
200                return Err("publication contains the same object more than once".to_string());
201            }
202            match object {
203                crate::analysis::facts::PublicationObjectFact::Table { name, columns, .. } => {
204                    let id = self.resolve_relation_id(name);
205                    match self.local.relations.get(&id) {
206                        Some(RelationOverlay::Present(relation))
207                            if relation.kind == RelationKind::Table
208                                && relation.persistence == Persistence::Permanent =>
209                        {
210                            if let Some(columns) = columns {
211                                let mut seen = HashSet::new();
212                                for column in columns {
213                                    if !seen.insert(column) {
214                                        return Err(format!(
215                                            "publication lists column '{}' more than once for '{}'",
216                                            column, id
217                                        ));
218                                    }
219                                    if !relation.has_column(column) {
220                                        return Err(format!(
221                                            "publication column '{}.{}' does not exist",
222                                            id, column
223                                        ));
224                                    }
225                                }
226                            }
227                        }
228                        Some(RelationOverlay::Present(_)) => {
229                            return Err(format!(
230                                "publication target '{}' is not a permanent table",
231                                id
232                            ));
233                        }
234                        Some(RelationOverlay::Dropped) => {
235                            return Err(format!("publication table '{}' does not exist", id));
236                        }
237                        None if self.baseline_available && self.baseline_covers_object(&id) => {
238                            return Err(format!("publication table '{}' does not exist", id));
239                        }
240                        None => {
241                            self.snapshot_confidence();
242                            self.local.confidence = Confidence::Tainted;
243                        }
244                    }
245                }
246                crate::analysis::facts::PublicationObjectFact::SchemaTables { schema, .. } => {
247                    if !self.schema_is_present(schema) {
248                        if self.schema_absence_is_authoritative(schema) {
249                            return Err(format!("publication schema '{}' does not exist", schema));
250                        }
251                        self.snapshot_confidence();
252                        self.local.confidence = Confidence::Tainted;
253                    }
254                }
255                crate::analysis::facts::PublicationObjectFact::CurrentSchemaShorthand
256                | crate::analysis::facts::PublicationObjectFact::Unknown => {
257                    self.snapshot_confidence();
258                    self.local.confidence = Confidence::Tainted;
259                }
260            }
261        }
262        Ok(())
263    }
264
265    fn publication_scope_needs_inheritance_knowledge(
266        &self,
267        scope: &crate::analysis::facts::PublicationScope,
268    ) -> bool {
269        let crate::analysis::facts::PublicationScope::Explicit(objects) = scope else {
270            return false;
271        };
272        objects.iter().any(|object| match object {
273            crate::analysis::facts::PublicationObjectFact::Table {
274                name,
275                only,
276                include_partitions,
277                ..
278            } if !only || *include_partitions => {
279                let id = self.resolve_relation_id(name);
280                !matches!(
281                    self.local.relations.get(&id),
282                    Some(RelationOverlay::Present(relation)) if relation.generation > 0
283                ) || self.local.graph.edges.iter().any(|edge| {
284                    matches!(edge.kind, DependencyKind::PartitionOf)
285                        && self.local.graph.resolve_rename(&edge.referenced)
286                            == self.local.graph.resolve_rename(&id)
287                })
288            }
289            _ => false,
290        })
291    }
292
293    fn taint_inheritance_sensitive_publication_scope(
294        &mut self,
295        scope: &crate::analysis::facts::PublicationScope,
296    ) {
297        if self.publication_scope_needs_inheritance_knowledge(scope) {
298            self.snapshot_confidence();
299            self.local.confidence = Confidence::Tainted;
300        }
301    }
302
303    fn subscription_option<'a>(
304        params: Option<&'a [crate::analysis::facts::AttributeFact]>,
305        name: &str,
306    ) -> Option<&'a str> {
307        params?
308            .iter()
309            .rev()
310            .find(|param| param.name.eq_ignore_ascii_case(name))
311            .map(|param| param.value.as_str())
312    }
313
314    fn postgres_boolean(value: &str) -> Option<bool> {
315        let value = value.trim().to_ascii_lowercase();
316        match value.as_str() {
317            "1" => return Some(true),
318            "0" => return Some(false),
319            "" => return None,
320            _ => {}
321        }
322
323        let mut matched = None;
324        for (spelling, parsed) in [
325            ("true", true),
326            ("yes", true),
327            ("on", true),
328            ("false", false),
329            ("no", false),
330            ("off", false),
331        ] {
332            if spelling.starts_with(&value) {
333                if matched.is_some() {
334                    return None;
335                }
336                matched = Some(parsed);
337            }
338        }
339        matched
340    }
341
342    fn subscription_boolean_option(
343        params: Option<&[crate::analysis::facts::AttributeFact]>,
344        name: &str,
345    ) -> Option<bool> {
346        Self::subscription_option(params, name).and_then(Self::postgres_boolean)
347    }
348
349    fn validate_subscription_boolean_options(
350        params: Option<&[crate::analysis::facts::AttributeFact]>,
351        names: &[&str],
352    ) -> Result<(), String> {
353        let Some(params) = params else {
354            return Ok(());
355        };
356        for option in params {
357            if names
358                .iter()
359                .any(|name| option.name.eq_ignore_ascii_case(name))
360                && Self::postgres_boolean(&option.value).is_none()
361            {
362                return Err(format!(
363                    "subscription option '{}' requires a PostgreSQL boolean value",
364                    option.name
365                ));
366            }
367        }
368        Ok(())
369    }
370
371    fn set_subscription_option(
372        subscription: &mut crate::model::replication::SubscriptionState,
373        option: &crate::analysis::facts::AttributeFact,
374    ) {
375        let params = subscription.params.get_or_insert_with(Vec::new);
376        params.retain(|existing| !existing.name.eq_ignore_ascii_case(&option.name));
377        params.push(option.clone());
378    }
379
380    pub fn new(cache: DbCache) -> Self {
381        Self::with_baseline(cache, true)
382    }
383
384    pub fn with_baseline(cache: DbCache, baseline_available: bool) -> Self {
385        let source_lock_timeout =
386            baseline_available.then_some(cache.metadata.source_lock_timeout_ms);
387        let source_statement_timeout =
388            baseline_available.then_some(cache.metadata.source_statement_timeout_ms);
389        let default_search_path = cache.search_path.clone();
390        let default_search_path_template = if cache.metadata.schemas.is_none() {
391            cache
392                .metadata
393                .source_search_path
394                .clone()
395                .unwrap_or_else(|| default_search_path.clone())
396        } else {
397            default_search_path.clone()
398        };
399        let current_role_known = cache.metadata.source_role.is_some();
400        let current_role = cache
401            .metadata
402            .source_role
403            .clone()
404            .unwrap_or_else(|| "postgres".to_string());
405        let session_role_known = cache.metadata.source_session_role.is_some();
406        let session_role = cache
407            .metadata
408            .source_session_role
409            .clone()
410            .unwrap_or_else(|| current_role.clone());
411        let authenticated_role = session_role.clone();
412        let authenticated_role_known = session_role_known;
413        let persistent_current_role = current_role.clone();
414        let persistent_current_role_known = current_role_known;
415        let persistent_session_role = session_role.clone();
416        let persistent_session_role_known = session_role_known;
417        let roles_known = cache.metadata.source_session_role.is_some();
418        let baseline_schemas = cache
419            .metadata
420            .schemas
421            .as_ref()
422            .map(|schemas| schemas.iter().cloned().collect());
423        let mut relations: HashMap<ObjectId, RelationOverlay> = HashMap::new();
424        let mut baseline_relations = HashSet::new();
425        let mut baseline_indexes = HashSet::new();
426        let mut baseline_foreign_keys = HashSet::new();
427        let mut baseline_fk_dependencies = HashSet::new();
428        let mut triggers = HashMap::new();
429        let mut constraints = HashMap::new();
430        let mut types = HashMap::new();
431        let mut graph = DependencyGraph::new();
432
433        let mut schemas: HashMap<String, SchemaOverlay> = cache
434            .schemas
435            .iter()
436            .map(|(name, schema)| (name.clone(), SchemaOverlay::Present(schema.clone())))
437            .collect();
438        // Effective cached search-path entries and modeled objects are direct
439        // evidence that their namespaces existed at synchronization time.
440        // This also keeps programmatically assembled caches internally
441        // consistent without treating unrelated out-of-scope schemas as
442        // authoritative catalogs.
443        let inferred_schema_owner = ObjectId::new(
444            "",
445            cache.metadata.source_role.as_deref().unwrap_or("postgres"),
446        );
447        for name in cache
448            .relations
449            .keys()
450            .map(|id| &id.schema)
451            .chain(cache.types.keys().map(|id| &id.schema))
452            .chain(cache.functions.keys().map(|id| &id.schema))
453            .chain(cache.sequences.keys().map(|id| &id.schema))
454        {
455            schemas.entry(name.clone()).or_insert_with(|| {
456                SchemaOverlay::Present(crate::model::schema::SchemaState {
457                    name: name.clone(),
458                    owner: inferred_schema_owner.clone(),
459                    generation: 0,
460                })
461            });
462        }
463        if cache.schemas.is_empty() && cache.metadata.schemas.is_none() {
464            for name in &cache.search_path {
465                schemas.entry(name.clone()).or_insert_with(|| {
466                    SchemaOverlay::Present(crate::model::schema::SchemaState {
467                        name: name.clone(),
468                        owner: inferred_schema_owner.clone(),
469                        generation: 0,
470                    })
471                });
472            }
473        }
474
475        let sequences = cache
476            .sequences
477            .iter()
478            .map(|(id, sequence)| (id.clone(), SequenceOverlay::Present(sequence.clone())))
479            .collect();
480        let baseline_sequences = cache.sequences.keys().cloned().collect();
481        for sequence in cache.sequences.values() {
482            if let Some((table, column)) = &sequence.owned_by {
483                graph.edges.push(DependencyEdge::new(
484                    sequence.id.clone(),
485                    table.clone(),
486                    DependencyKind::SequenceOwnedBy {
487                        column: column.clone(),
488                    },
489                ));
490            }
491        }
492
493        for (id, rel_state) in cache.baseline_relations() {
494            if rel_state.is_fk_dependency {
495                baseline_fk_dependencies.insert(id.clone());
496            }
497            relations.insert(id.clone(), RelationOverlay::Present(rel_state.clone()));
498            baseline_relations.insert(id.clone());
499        }
500
501        for (id, type_state) in &cache.types {
502            types.insert(id.clone(), TypeOverlay::Present(type_state.clone()));
503        }
504        let type_catalog = types.clone();
505        for overlay in relations.values_mut() {
506            if let RelationOverlay::Present(relation) = overlay {
507                for column in &mut relation.columns {
508                    column.type_id = column.data_type.as_deref().and_then(|raw| {
509                        Self::resolve_type_reference_from_catalog(
510                            raw,
511                            &type_catalog,
512                            &default_search_path,
513                        )
514                    });
515                }
516            }
517        }
518        for overlay in types.values_mut() {
519            if let TypeOverlay::Present(TypeState {
520                kind:
521                    TypeKind::Domain {
522                        base_type,
523                        base_type_id,
524                    },
525                ..
526            }) = overlay
527            {
528                *base_type_id = Self::resolve_type_reference_from_catalog(
529                    base_type,
530                    &type_catalog,
531                    &default_search_path,
532                );
533            }
534        }
535        for fk in cache.foreign_keys {
536            baseline_foreign_keys.insert((fk.from_table.clone(), fk.constraint_name.clone()));
537            graph.edges.push(DependencyEdge::new(
538                fk.from_table,
539                fk.to_table,
540                DependencyKind::ForeignKey {
541                    constraint_name: Some(fk.constraint_name),
542                    from_columns: Vec::new(),
543                    to_columns: Vec::new(),
544                    from_generation: 0,
545                },
546            ));
547        }
548
549        for idx in cache.indexes {
550            // Index identities are tracked separately from relation identities.
551            baseline_indexes.insert(idx.index_id.clone());
552            graph.edges.push(DependencyEdge::new(
553                idx.index_id,
554                idx.table_id,
555                DependencyKind::IndexOnRelation {
556                    using_method: None,
557                    has_predicate: false,
558                    is_concurrent: false,
559                    is_unique: false,
560                    eligibility_known: false,
561                },
562            ));
563        }
564
565        for dependency in cache.dependencies {
566            if dependency.deptype != "view" {
567                continue;
568            }
569            let (Some(obj_schema), Some(obj_name), Some(ref_schema), Some(ref_name)) = (
570                dependency.obj_schema,
571                dependency.obj_name,
572                dependency.ref_schema,
573                dependency.ref_name,
574            ) else {
575                continue;
576            };
577            let dependent = ObjectId::new(obj_schema, obj_name);
578            let referenced = ObjectId::new(ref_schema, ref_name);
579            // Older caches created on PostgreSQL 14/15 can contain an
580            // internal pg_rewrite self-edge for a view. Ignore it while
581            // loading so upgrading safe-migrate does not require a re-sync to
582            // restore a meaningful dependency graph.
583            if dependent == referenced {
584                continue;
585            }
586            let is_view = relations.get(&dependent).is_some_and(|relation| {
587                matches!(
588                    relation,
589                    RelationOverlay::Present(state)
590                        if matches!(
591                            state.kind,
592                            crate::model::relation::RelationKind::View
593                                | crate::model::relation::RelationKind::MaterializedView
594                        )
595                )
596            });
597            if is_view && relations.contains_key(&referenced) {
598                graph.edges.push(DependencyEdge::new(
599                    dependent,
600                    referenced,
601                    DependencyKind::ViewDependency { view_generation: 0 },
602                ));
603            }
604        }
605
606        for constraint in cache.constraints {
607            constraints.insert(
608                (constraint.table_id.clone(), constraint.name.clone()),
609                constraint,
610            );
611        }
612
613        for t in cache.triggers {
614            let trigger_key = Self::trigger_key(&t.table_id, &t.trigger_id.name);
615            triggers.insert(
616                trigger_key.clone(),
617                TriggerOverlay::Present(crate::model::trigger::TriggerState {
618                    name: t.trigger_id.name.clone(),
619                    id: trigger_key.clone(),
620                    table_id: t.table_id.clone(),
621                    enabled_mode: t.enabled_mode,
622                    generation: 0,
623                }),
624            );
625            graph.edges.push(DependencyEdge::new(
626                trigger_key.clone(),
627                t.table_id,
628                DependencyKind::TriggerOnTable {
629                    trigger_id: trigger_key,
630                    function_id: t.function_id,
631                },
632            ));
633        }
634
635        let mut functions: HashMap<ObjectId, crate::model::function::FunctionOverlay> =
636            HashMap::new();
637        for (id, func_state) in &cache.functions {
638            functions.insert(
639                id.clone(),
640                crate::model::function::FunctionOverlay::Present(func_state.clone()),
641            );
642        }
643        for overlay in functions.values_mut() {
644            if let crate::model::function::FunctionOverlay::Present(function) = overlay {
645                function.arg_type_ids = function
646                    .arg_types
647                    .iter()
648                    .map(|raw| {
649                        Self::resolve_type_reference_from_catalog(
650                            raw,
651                            &type_catalog,
652                            &default_search_path,
653                        )
654                    })
655                    .collect();
656                function.return_type_id = Self::resolve_type_reference_from_catalog(
657                    &function.return_type,
658                    &type_catalog,
659                    &default_search_path,
660                );
661            }
662        }
663
664        let publications = cache
665            .publications
666            .into_iter()
667            .map(|(name, publication)| {
668                if let crate::analysis::facts::PublicationScope::Explicit(objects) =
669                    &publication.scope
670                {
671                    for object in objects {
672                        if let crate::analysis::facts::PublicationObjectFact::Table {
673                            name: relation,
674                            ..
675                        } = object
676                        {
677                            let table_id = ObjectId::new(
678                                relation
679                                    .schema
680                                    .as_ref()
681                                    .map(|schema| schema.resolve())
682                                    .unwrap_or_else(|| "public".to_string()),
683                                relation.name.resolve(),
684                            );
685                            graph.edges.push(DependencyEdge::new(
686                                table_id,
687                                ObjectId::new("public", &name),
688                                DependencyKind::PublicationIncludes {
689                                    publication_name: name.clone(),
690                                },
691                            ));
692                        }
693                    }
694                }
695                (
696                    name,
697                    crate::model::replication::PublicationOverlay::Present(publication),
698                )
699            })
700            .collect();
701        let subscriptions = cache
702            .subscriptions
703            .into_iter()
704            .map(|(name, subscription)| {
705                (
706                    name,
707                    crate::model::replication::SubscriptionOverlay::Present(subscription),
708                )
709            })
710            .collect();
711
712        let mut state = Self {
713            pg_version_num: cache.pg_version_num,
714            baseline_available,
715            baseline_schemas,
716            baseline_relations,
717            baseline_indexes,
718            baseline_foreign_keys,
719            baseline_fk_dependencies,
720            baseline_sequences,
721            local: LocalState {
722                schemas,
723                relations,
724                types,
725                functions,
726                sequences,
727                publications,
728                subscriptions,
729                roles: cache
730                    .roles
731                    .into_iter()
732                    .map(|(id, role)| (id, crate::model::role::RoleOverlay::Present(role)))
733                    .collect(),
734                triggers,
735                constraints,
736                graph,
737                search_path: default_search_path.clone(),
738                default_search_path,
739                search_path_template: default_search_path_template.clone(),
740                session_search_path_template: default_search_path_template.clone(),
741                default_search_path_template,
742                lock_timeout: ScopedSetting::new(source_lock_timeout),
743                statement_timeout: ScopedSetting::new(source_statement_timeout),
744                current_role,
745                current_role_known,
746                persistent_current_role,
747                persistent_current_role_known,
748                session_role,
749                session_role_known,
750                persistent_session_role,
751                persistent_session_role_known,
752                authenticated_role,
753                authenticated_role_known,
754                roles_known,
755                confidence: Confidence::Exact,
756                transactions: Vec::new(),
757                transaction_aborted: false,
758                pending_validation: HashSet::new(),
759                generation_counter: 0,
760            },
761        };
762        state.refresh_role_sensitive_search_path();
763        state.local.default_search_path = state.local.search_path.clone();
764        state
765    }
766
767    pub fn get_relation(&self, id: &ObjectId) -> Option<&RelationOverlay> {
768        self.local.relations.get(id)
769    }
770
771    pub fn resolve_function_schema(
772        &self,
773        name: &crate::ast::identifiers::QualifiedName,
774        sig_str: &str,
775    ) -> String {
776        if let Some(schema) = &name.schema {
777            return schema.resolve();
778        }
779        for schema in &self.local.search_path {
780            let candidate = ObjectId::new(schema.clone(), sig_str.to_string());
781            if self.local.functions.contains_key(&candidate) {
782                return schema.clone();
783            }
784        }
785        self.local
786            .search_path
787            .first()
788            .cloned()
789            .unwrap_or_else(|| "public".to_string())
790    }
791
792    pub fn resolve_relation_id(&self, name: &crate::ast::identifiers::QualifiedName) -> ObjectId {
793        if let Some(schema) = &name.schema {
794            return ObjectId::new(schema.resolve(), name.name.resolve());
795        }
796        let resolved_name = name.name.resolve();
797        for schema in &self.local.search_path {
798            let mut candidate = ObjectId::new(schema.clone(), resolved_name.clone());
799            if self.local.relations.contains_key(&candidate) {
800                candidate.inferred_schema = true;
801                return candidate;
802            }
803        }
804        let schema = self
805            .local
806            .search_path
807            .first()
808            .cloned()
809            .unwrap_or_else(|| "public".to_string());
810        let mut id = ObjectId::new(schema, resolved_name);
811        id.inferred_schema = true;
812        id
813    }
814
815    pub fn relation_is_present(&self, id: &ObjectId) -> bool {
816        matches!(
817            self.local.relations.get(id),
818            Some(RelationOverlay::Present(_))
819        )
820    }
821
822    /// Returns whether a cache-backed absence is authoritative for an object.
823    /// A scoped cache only establishes absence in the schemas it actually
824    /// synchronized.
825    pub fn baseline_covers_object(&self, id: &ObjectId) -> bool {
826        self.baseline_schemas
827            .as_ref()
828            .is_none_or(|schemas| schemas.contains(&id.schema))
829    }
830
831    pub fn baseline_scope_omits_displayed_object<'a>(
832        &self,
833        object_name: &'a str,
834    ) -> Option<&'a str> {
835        let schemas = self.baseline_schemas.as_ref()?;
836        let (schema, _) = object_name.split_once('.')?;
837        (!schemas.contains(schema)).then_some(schema)
838    }
839
840    fn sequence_is_present(&self, id: &ObjectId) -> bool {
841        matches!(
842            self.local.sequences.get(id),
843            Some(SequenceOverlay::Present(_))
844        )
845    }
846
847    fn type_is_present(&self, id: &ObjectId) -> bool {
848        matches!(self.local.types.get(id), Some(TypeOverlay::Present(_)))
849    }
850
851    fn resolve_type_reference_from_catalog(
852        raw: &str,
853        types: &HashMap<ObjectId, TypeOverlay>,
854        search_path: &[String],
855    ) -> Option<ObjectId> {
856        let (schema, name) = Self::parse_type_reference(raw)?;
857        if let Some(schema) = schema {
858            let candidate = ObjectId::new(schema, name);
859            return matches!(types.get(&candidate), Some(TypeOverlay::Present(_)))
860                .then_some(candidate);
861        }
862        search_path.iter().find_map(|schema| {
863            let candidate = ObjectId::new(schema, &name);
864            matches!(types.get(&candidate), Some(TypeOverlay::Present(_))).then_some(candidate)
865        })
866    }
867
868    /// Parse a SQL type name exactly enough to resolve modeled named types.
869    /// Quoted identifiers retain their case, unquoted identifiers fold to
870    /// lowercase, and any number of array suffixes are ignored for lookup.
871    fn parse_type_reference(raw: &str) -> Option<(Option<String>, String)> {
872        let mut token = raw.trim();
873        while let Some(without_array) = token.strip_suffix("[]") {
874            token = without_array.trim_end();
875        }
876
877        let mut parts = Vec::new();
878        let mut current = String::new();
879        let mut quoted = false;
880        let mut part_is_quoted = false;
881        let mut chars = token.chars().peekable();
882        while let Some(character) = chars.next() {
883            match character {
884                '"' if quoted && chars.peek() == Some(&'"') => {
885                    current.push('"');
886                    chars.next();
887                }
888                '"' => {
889                    quoted = !quoted;
890                    part_is_quoted = true;
891                }
892                '.' if !quoted => {
893                    parts.push(Self::resolve_type_identifier(&current, part_is_quoted)?);
894                    current.clear();
895                    part_is_quoted = false;
896                }
897                character if !quoted && character.is_whitespace() => {}
898                character => current.push(character),
899            }
900        }
901        if quoted {
902            return None;
903        }
904        parts.push(Self::resolve_type_identifier(&current, part_is_quoted)?);
905        match parts.as_slice() {
906            [name] => Some((None, name.clone())),
907            [schema, name] => Some((Some(schema.clone()), name.clone())),
908            _ => None,
909        }
910    }
911
912    fn resolve_type_identifier(identifier: &str, quoted: bool) -> Option<String> {
913        (!identifier.is_empty()).then(|| {
914            if quoted {
915                identifier.to_string()
916            } else {
917                identifier.to_lowercase()
918            }
919        })
920    }
921
922    fn resolve_type_reference(&self, raw: &str) -> Option<ObjectId> {
923        Self::resolve_type_reference_from_catalog(raw, &self.local.types, &self.local.search_path)
924    }
925
926    fn type_reference_name(id: &ObjectId, qualified: bool) -> String {
927        let quote = |identifier: &str| {
928            let unquoted = identifier
929                .chars()
930                .enumerate()
931                .all(|(index, character)| match index {
932                    0 => character.is_ascii_lowercase() || character == '_',
933                    _ => {
934                        character.is_ascii_lowercase()
935                            || character.is_ascii_digit()
936                            || character == '_'
937                            || character == '$'
938                    }
939                });
940            if unquoted {
941                identifier.to_string()
942            } else {
943                format!("\"{}\"", identifier.replace('"', "\"\""))
944            }
945        };
946
947        if qualified {
948            format!("{}.{}", quote(&id.schema), quote(&id.name))
949        } else {
950            quote(&id.name)
951        }
952    }
953
954    fn remapped_type_display(raw: &str, new_id: &ObjectId, schema_changed: bool) -> String {
955        let suffix = raw.find('[').map(|index| &raw[index..]).unwrap_or("");
956        format!(
957            "{}{}",
958            Self::type_reference_name(new_id, schema_changed),
959            suffix
960        )
961    }
962
963    fn index_is_present(&self, id: &ObjectId) -> bool {
964        self.local.graph.edges.iter().any(|edge| {
965            matches!(edge.kind, DependencyKind::IndexOnRelation { .. }) && edge.dependent == *id
966        })
967    }
968
969    fn next_generated_constraint_name(
970        &self,
971        table: &ObjectId,
972        name1: &str,
973        name2: Option<&str>,
974        label: &str,
975    ) -> String {
976        (0..)
977            .map(|suffix| {
978                let label = if suffix == 0 {
979                    label.to_string()
980                } else {
981                    format!("{label}{suffix}")
982                };
983                Self::postgres_object_name(name1, name2, &label)
984            })
985            .find(|candidate| {
986                !self
987                    .local
988                    .constraints
989                    .contains_key(&(table.clone(), candidate.clone()))
990            })
991            .expect("constraint suffix space is unbounded")
992    }
993
994    fn postgres_object_name(name1: &str, name2: Option<&str>, label: &str) -> String {
995        const MAX_IDENTIFIER_BYTES: usize = 63;
996
997        fn truncate(value: &str, max_bytes: usize) -> &str {
998            let mut end = max_bytes.min(value.len());
999            while !value.is_char_boundary(end) {
1000                end -= 1;
1001            }
1002            &value[..end]
1003        }
1004
1005        let separators = usize::from(name2.is_some()) + 1;
1006        let available = MAX_IDENTIFIER_BYTES.saturating_sub(label.len() + separators);
1007        let mut name1_bytes = name1.len();
1008        let mut name2_bytes = name2.map_or(0, str::len);
1009        while name1_bytes + name2_bytes > available {
1010            if name1_bytes > name2_bytes {
1011                name1_bytes -= 1;
1012            } else {
1013                name2_bytes -= 1;
1014            }
1015        }
1016
1017        let name1 = truncate(name1, name1_bytes);
1018        match name2 {
1019            Some(name2) => format!("{name1}_{}_{}", truncate(name2, name2_bytes), label),
1020            None => format!("{name1}_{label}"),
1021        }
1022    }
1023
1024    fn relation_namespace_is_taken(&self, id: &ObjectId) -> bool {
1025        self.relation_is_present(id)
1026            || self.sequence_is_present(id)
1027            || self.index_is_present(id)
1028            || self.type_is_present(id)
1029    }
1030
1031    fn next_implicit_sequence_id(
1032        &self,
1033        table: &ObjectId,
1034        column: &str,
1035        reserved: &HashSet<ObjectId>,
1036    ) -> ObjectId {
1037        (0..)
1038            .map(|suffix| {
1039                let label = if suffix == 0 {
1040                    "seq".to_string()
1041                } else {
1042                    format!("seq{suffix}")
1043                };
1044                ObjectId::new(
1045                    &table.schema,
1046                    Self::postgres_object_name(&table.name, Some(column), &label),
1047                )
1048            })
1049            .find(|candidate| {
1050                !reserved.contains(candidate) && !self.relation_namespace_is_taken(candidate)
1051            })
1052            .expect("implicit sequence suffix space is unbounded")
1053    }
1054
1055    fn sequence_nextval_default(id: &ObjectId) -> crate::analysis::expr_ir::ExprIr {
1056        crate::analysis::expr_ir::ExprIr::FunctionCall {
1057            name: "nextval".to_string(),
1058            args: vec![crate::analysis::expr_ir::ExprIr::Literal(format!(
1059                "{}.{}",
1060                id.schema, id.name
1061            ))],
1062        }
1063    }
1064
1065    pub fn column_was_added_in_transaction(&self, table_id: &ObjectId, column: &str) -> bool {
1066        if self.local.transactions.is_empty() {
1067            return false;
1068        }
1069
1070        // Search from the oldest transaction frame to the newest
1071        for frame in &self.local.transactions {
1072            for change in &frame.undo_log {
1073                if let StateChange::RelationSnapshot { id, previous } = change
1074                    && id == table_id
1075                {
1076                    match previous.as_ref() {
1077                        None | Some(RelationOverlay::Dropped) => {
1078                            return true;
1079                        }
1080                        Some(RelationOverlay::Present(r)) => {
1081                            let col_existed = r.columns.iter().any(|c| c.name == column);
1082                            return !col_existed;
1083                        }
1084                    }
1085                }
1086            }
1087        }
1088        false
1089    }
1090
1091    pub fn capture_pre_state(&self) -> PreState {
1092        let mut relations = HashMap::new();
1093        for (id, overlay) in &self.local.relations {
1094            if let RelationOverlay::Present(s) = overlay {
1095                relations.insert(id.clone(), s.clone());
1096            }
1097        }
1098
1099        let mut functions = HashMap::new();
1100        for (id, overlay) in &self.local.functions {
1101            if let crate::model::function::FunctionOverlay::Present(s) = overlay {
1102                functions.insert(id.clone(), s.clone());
1103            }
1104        }
1105
1106        let mut roles = HashMap::new();
1107        for (name, overlay) in &self.local.roles {
1108            if let crate::model::role::RoleOverlay::Present(s) = overlay {
1109                roles.insert(name.clone(), s.clone());
1110            }
1111        }
1112
1113        let mut publications = HashMap::new();
1114        for (name, overlay) in &self.local.publications {
1115            if let crate::model::replication::PublicationOverlay::Present(s) = overlay {
1116                publications.insert(name.clone(), s.clone());
1117            }
1118        }
1119
1120        let mut subscriptions = HashMap::new();
1121        for (name, overlay) in &self.local.subscriptions {
1122            if let crate::model::replication::SubscriptionOverlay::Present(s) = overlay {
1123                subscriptions.insert(name.clone(), s.clone());
1124            }
1125        }
1126
1127        let mut sequences = HashMap::new();
1128        for (id, overlay) in &self.local.sequences {
1129            if let SequenceOverlay::Present(s) = overlay {
1130                sequences.insert(id.clone(), s.clone());
1131            }
1132        }
1133
1134        let mut types = HashMap::new();
1135        for (id, overlay) in &self.local.types {
1136            if let TypeOverlay::Present(s) = overlay {
1137                types.insert(id.clone(), s.clone());
1138            }
1139        }
1140
1141        let indexes = self
1142            .local
1143            .graph
1144            .edges
1145            .iter()
1146            .filter(|e| matches!(e.kind, DependencyKind::IndexOnRelation { .. }))
1147            .cloned()
1148            .collect();
1149
1150        PreState {
1151            relations,
1152            functions,
1153            roles,
1154            publications,
1155            subscriptions,
1156            sequences,
1157            types,
1158            indexes,
1159        }
1160    }
1161
1162    pub fn get_cascade_closure(&self, target_oid: &ObjectId) -> CascadeResult {
1163        let mut result = CascadeResult::default();
1164        let mut visited = HashSet::new();
1165        self.walk_cascade(target_oid, &mut visited, &mut result);
1166        result
1167    }
1168
1169    fn walk_cascade(
1170        &self,
1171        current: &ObjectId,
1172        visited: &mut HashSet<ObjectId>,
1173        result: &mut CascadeResult,
1174    ) {
1175        let resolved_current = self.local.graph.resolve_rename(current).clone();
1176
1177        if !visited.insert(resolved_current.clone()) {
1178            return;
1179        }
1180
1181        result.dropped_relations.insert(resolved_current.clone());
1182
1183        for edge in &self.local.graph.edges {
1184            match &edge.kind {
1185                DependencyKind::ViewDependency { .. } => {
1186                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current {
1187                        let resolved_view_id =
1188                            self.local.graph.resolve_rename(&edge.dependent).clone();
1189                        if !visited.contains(&resolved_view_id) {
1190                            self.walk_cascade(&resolved_view_id, visited, result);
1191                        }
1192                    }
1193                }
1194                DependencyKind::IndexOnRelation { .. } => {
1195                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current {
1196                        result
1197                            .dropped_indexes
1198                            .insert(self.local.graph.resolve_rename(&edge.dependent).clone());
1199                    }
1200                }
1201                DependencyKind::ForeignKey {
1202                    constraint_name, ..
1203                } => {
1204                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current
1205                        && let Some(cname) = constraint_name
1206                    {
1207                        result.dropped_constraints.insert((
1208                            self.local.graph.resolve_rename(&edge.dependent).clone(),
1209                            cname.clone(),
1210                        ));
1211                    }
1212                }
1213                DependencyKind::PartitionOf
1214                    if self.local.graph.resolve_rename(&edge.referenced) == &resolved_current =>
1215                {
1216                    let resolved_child = self.local.graph.resolve_rename(&edge.dependent).clone();
1217                    if !visited.contains(&resolved_child) {
1218                        self.walk_cascade(&resolved_child, visited, result);
1219                    }
1220                }
1221                _ => {}
1222            }
1223        }
1224    }
1225
1226    fn resolve_grant_privileges(
1227        spec: &crate::analysis::facts::PrivilegeSpec,
1228    ) -> HashSet<Privilege> {
1229        match spec {
1230            crate::analysis::facts::PrivilegeSpec::All => vec![
1231                Privilege::Select,
1232                Privilege::Insert,
1233                Privilege::Update,
1234                Privilege::Delete,
1235                Privilege::Truncate,
1236                Privilege::References,
1237                Privilege::Trigger,
1238            ]
1239            .into_iter()
1240            .collect(),
1241            crate::analysis::facts::PrivilegeSpec::List(list) => list
1242                .iter()
1243                .filter_map(|p| match p {
1244                    crate::analysis::facts::PrivilegeFact::Select => Some(Privilege::Select),
1245                    crate::analysis::facts::PrivilegeFact::Insert => Some(Privilege::Insert),
1246                    crate::analysis::facts::PrivilegeFact::Update => Some(Privilege::Update),
1247                    crate::analysis::facts::PrivilegeFact::Delete => Some(Privilege::Delete),
1248                    crate::analysis::facts::PrivilegeFact::Truncate => Some(Privilege::Truncate),
1249                    crate::analysis::facts::PrivilegeFact::References => {
1250                        Some(Privilege::References)
1251                    }
1252                    crate::analysis::facts::PrivilegeFact::Trigger => Some(Privilege::Trigger),
1253                    _ => None,
1254                })
1255                .collect(),
1256        }
1257    }
1258
1259    fn resolve_role_name(
1260        role: &crate::analysis::facts::RoleFact,
1261        current_role: &str,
1262        session_role: &str,
1263    ) -> Option<ObjectId> {
1264        let name = match role {
1265            crate::analysis::facts::RoleFact::Named { name, .. } => Some(name.clone()),
1266            crate::analysis::facts::RoleFact::CurrentUser
1267            | crate::analysis::facts::RoleFact::CurrentRole => Some(current_role.to_string()),
1268            crate::analysis::facts::RoleFact::SessionUser => Some(session_role.to_string()),
1269            crate::analysis::facts::RoleFact::Unknown => None,
1270        }?;
1271        Some(ObjectId::new("", name))
1272    }
1273
1274    fn role_fact_identity(
1275        &self,
1276        role: &crate::analysis::facts::RoleFact,
1277    ) -> Option<(String, bool)> {
1278        match role {
1279            crate::analysis::facts::RoleFact::Named { name, .. } => Some((name.clone(), true)),
1280            crate::analysis::facts::RoleFact::CurrentUser
1281            | crate::analysis::facts::RoleFact::CurrentRole => Some((
1282                self.local.current_role.clone(),
1283                self.local.current_role_known,
1284            )),
1285            crate::analysis::facts::RoleFact::SessionUser => Some((
1286                self.local.session_role.clone(),
1287                self.local.session_role_known,
1288            )),
1289            crate::analysis::facts::RoleFact::Unknown => None,
1290        }
1291    }
1292
1293    fn present_role(&self, name: &str) -> Option<&crate::model::role::RoleState> {
1294        match self.local.roles.get(&ObjectId::new("", name)) {
1295            Some(crate::model::role::RoleOverlay::Present(role)) => Some(role),
1296            _ => None,
1297        }
1298    }
1299
1300    fn can_set_role_to(&self, target: &str) -> Option<bool> {
1301        if !self.local.roles_known || !self.local.session_role_known {
1302            return None;
1303        }
1304        if self.present_role(target).is_none() {
1305            return Some(false);
1306        }
1307        if self.local.session_role == target {
1308            return Some(true);
1309        }
1310        let session = self.present_role(&self.local.session_role)?;
1311        if session.is_superuser {
1312            return Some(true);
1313        }
1314
1315        let mut pending = session.can_set_role_to.clone();
1316        let mut visited = HashSet::new();
1317        while let Some(role_id) = pending.pop() {
1318            if !visited.insert(role_id.clone()) {
1319                continue;
1320            }
1321            if role_id.name == target {
1322                return Some(true);
1323            }
1324            if let Some(role) = self.present_role(&role_id.name) {
1325                pending.extend(role.can_set_role_to.iter().cloned());
1326            }
1327        }
1328        Some(false)
1329    }
1330
1331    fn can_set_session_authorization_to(&self, target: &str) -> Option<bool> {
1332        if !self.local.roles_known || !self.local.authenticated_role_known {
1333            return None;
1334        }
1335        if self.present_role(target).is_none() {
1336            return Some(false);
1337        }
1338        if self.local.authenticated_role == target {
1339            return Some(true);
1340        }
1341        Some(
1342            self.present_role(&self.local.authenticated_role)
1343                .is_some_and(|role| role.is_superuser),
1344        )
1345    }
1346
1347    fn schema_is_present(&self, name: &str) -> bool {
1348        matches!(
1349            self.local.schemas.get(name),
1350            Some(SchemaOverlay::Present(_))
1351        )
1352    }
1353
1354    fn schema_absence_is_authoritative(&self, name: &str) -> bool {
1355        if matches!(self.local.schemas.get(name), Some(SchemaOverlay::Dropped)) {
1356            return true;
1357        }
1358        self.baseline_available
1359            && self
1360                .baseline_schemas
1361                .as_ref()
1362                .is_none_or(|schemas| schemas.contains(name))
1363    }
1364
1365    fn refresh_role_sensitive_search_path(&mut self) {
1366        let template = self.local.search_path_template.clone();
1367        let mut effective = Vec::new();
1368        for entry in template {
1369            let schema = if entry == "$user" {
1370                if self.local.current_role_known {
1371                    self.local.current_role.clone()
1372                } else {
1373                    self.local.confidence = Confidence::Tainted;
1374                    continue;
1375                }
1376            } else {
1377                entry
1378            };
1379            if self.schema_is_present(&schema) {
1380                if !effective.contains(&schema) {
1381                    effective.push(schema);
1382                }
1383            } else if !self.schema_absence_is_authoritative(&schema) {
1384                self.local.confidence = Confidence::Tainted;
1385                if !effective.contains(&schema) {
1386                    effective.push(schema);
1387                }
1388            }
1389        }
1390        self.local.search_path = effective;
1391    }
1392
1393    fn remap_schema_id(id: &mut ObjectId, old_name: &str, new_name: &str) {
1394        if id.schema == old_name {
1395            id.schema = new_name.to_string();
1396        }
1397    }
1398
1399    fn rename_schema_namespace(&mut self, old_name: &str, new_name: &str) {
1400        self.snapshot_namespace();
1401
1402        let mut aliases = Vec::new();
1403        let mut relations = HashMap::new();
1404        for (mut id, mut overlay) in std::mem::take(&mut self.local.relations) {
1405            let old_id = id.clone();
1406            Self::remap_schema_id(&mut id, old_name, new_name);
1407            if let RelationOverlay::Present(state) = &mut overlay {
1408                Self::remap_schema_id(&mut state.id, old_name, new_name);
1409            }
1410            if id != old_id {
1411                aliases.push((old_id, id.clone()));
1412            }
1413            relations.insert(id, overlay);
1414        }
1415        self.local.relations = relations;
1416
1417        let mut types = HashMap::new();
1418        for (mut id, mut overlay) in std::mem::take(&mut self.local.types) {
1419            let old_id = id.clone();
1420            Self::remap_schema_id(&mut id, old_name, new_name);
1421            if let TypeOverlay::Present(state) = &mut overlay {
1422                Self::remap_schema_id(&mut state.id, old_name, new_name);
1423            }
1424            if id != old_id {
1425                aliases.push((old_id, id.clone()));
1426            }
1427            types.insert(id, overlay);
1428        }
1429        self.local.types = types;
1430
1431        let mut functions = HashMap::new();
1432        for (mut id, mut overlay) in std::mem::take(&mut self.local.functions) {
1433            let old_id = id.clone();
1434            Self::remap_schema_id(&mut id, old_name, new_name);
1435            if let crate::model::function::FunctionOverlay::Present(state) = &mut overlay {
1436                Self::remap_schema_id(&mut state.id, old_name, new_name);
1437            }
1438            if id != old_id {
1439                aliases.push((old_id, id.clone()));
1440            }
1441            functions.insert(id, overlay);
1442        }
1443        self.local.functions = functions;
1444
1445        let mut sequences = HashMap::new();
1446        for (mut id, mut overlay) in std::mem::take(&mut self.local.sequences) {
1447            let old_id = id.clone();
1448            Self::remap_schema_id(&mut id, old_name, new_name);
1449            if let SequenceOverlay::Present(state) = &mut overlay {
1450                Self::remap_schema_id(&mut state.id, old_name, new_name);
1451                if let Some((table, _)) = &mut state.owned_by {
1452                    Self::remap_schema_id(table, old_name, new_name);
1453                }
1454            }
1455            if id != old_id {
1456                aliases.push((old_id, id.clone()));
1457            }
1458            sequences.insert(id, overlay);
1459        }
1460        self.local.sequences = sequences;
1461
1462        let mut triggers = HashMap::new();
1463        for (mut id, mut overlay) in std::mem::take(&mut self.local.triggers) {
1464            let old_id = id.clone();
1465            Self::remap_schema_id(&mut id, old_name, new_name);
1466            if let TriggerOverlay::Present(state) = &mut overlay {
1467                Self::remap_schema_id(&mut state.id, old_name, new_name);
1468                Self::remap_schema_id(&mut state.table_id, old_name, new_name);
1469            }
1470            if id != old_id {
1471                aliases.push((old_id, id.clone()));
1472            }
1473            triggers.insert(id, overlay);
1474        }
1475        self.local.triggers = triggers;
1476
1477        for overlay in self.local.publications.values_mut() {
1478            let crate::model::replication::PublicationOverlay::Present(publication) = overlay
1479            else {
1480                continue;
1481            };
1482            let crate::analysis::facts::PublicationScope::Explicit(objects) =
1483                &mut publication.scope
1484            else {
1485                continue;
1486            };
1487            for object in objects {
1488                match object {
1489                    crate::analysis::facts::PublicationObjectFact::Table { name, .. } => {
1490                        if name
1491                            .schema
1492                            .as_ref()
1493                            .is_some_and(|schema| schema.resolve() == old_name)
1494                        {
1495                            name.schema = Some(crate::ast::identifiers::Ident::new(new_name, true));
1496                        }
1497                    }
1498                    crate::analysis::facts::PublicationObjectFact::SchemaTables {
1499                        schema, ..
1500                    } if schema == old_name => *schema = new_name.to_string(),
1501                    _ => {}
1502                }
1503            }
1504        }
1505
1506        self.local.constraints = std::mem::take(&mut self.local.constraints)
1507            .into_iter()
1508            .map(|((mut table, name), mut constraint)| {
1509                Self::remap_schema_id(&mut table, old_name, new_name);
1510                Self::remap_schema_id(&mut constraint.table_id, old_name, new_name);
1511                ((table, name), constraint)
1512            })
1513            .collect();
1514        self.local.pending_validation = std::mem::take(&mut self.local.pending_validation)
1515            .into_iter()
1516            .map(|(mut table, name)| {
1517                Self::remap_schema_id(&mut table, old_name, new_name);
1518                (table, name)
1519            })
1520            .collect();
1521
1522        for edge in &mut self.local.graph.edges {
1523            Self::remap_schema_id(&mut edge.dependent, old_name, new_name);
1524            Self::remap_schema_id(&mut edge.referenced, old_name, new_name);
1525            if let DependencyKind::TriggerOnTable {
1526                trigger_id,
1527                function_id,
1528            } = &mut edge.kind
1529            {
1530                Self::remap_schema_id(trigger_id, old_name, new_name);
1531                Self::remap_schema_id(function_id, old_name, new_name);
1532            }
1533        }
1534        for (old_id, new_id) in aliases {
1535            self.local.graph.edges.push(DependencyEdge::new(
1536                old_id,
1537                new_id,
1538                DependencyKind::RenameTo,
1539            ));
1540        }
1541
1542        let remap_set = |set: &mut HashSet<ObjectId>| {
1543            *set = std::mem::take(set)
1544                .into_iter()
1545                .map(|mut id| {
1546                    Self::remap_schema_id(&mut id, old_name, new_name);
1547                    id
1548                })
1549                .collect();
1550        };
1551        remap_set(&mut self.baseline_relations);
1552        remap_set(&mut self.baseline_indexes);
1553        remap_set(&mut self.baseline_fk_dependencies);
1554        remap_set(&mut self.baseline_sequences);
1555        self.baseline_foreign_keys = std::mem::take(&mut self.baseline_foreign_keys)
1556            .into_iter()
1557            .map(|(mut table, name)| {
1558                Self::remap_schema_id(&mut table, old_name, new_name);
1559                (table, name)
1560            })
1561            .collect();
1562
1563        if let Some(SchemaOverlay::Present(mut schema)) = self.local.schemas.remove(old_name) {
1564            schema.name = new_name.to_string();
1565            self.local
1566                .schemas
1567                .insert(new_name.to_string(), SchemaOverlay::Present(schema));
1568        }
1569        self.refresh_role_sensitive_search_path();
1570    }
1571
1572    fn restore_persistent_role_context(&mut self) {
1573        self.local.current_role = self.local.persistent_current_role.clone();
1574        self.local.current_role_known = self.local.persistent_current_role_known;
1575        self.local.session_role = self.local.persistent_session_role.clone();
1576        self.local.session_role_known = self.local.persistent_session_role_known;
1577        self.local.search_path_template = self.local.session_search_path_template.clone();
1578        self.local.lock_timeout.reset_effective_to_session();
1579        self.local.statement_timeout.reset_effective_to_session();
1580        self.refresh_role_sensitive_search_path();
1581    }
1582
1583    fn apply_grant_to_relation(
1584        &mut self,
1585        id: &ObjectId,
1586        privileges: &HashSet<Privilege>,
1587        grantees: &[crate::analysis::facts::RoleFact],
1588    ) {
1589        self.snapshot_relation(id);
1590        if let Some(RelationOverlay::Present(rel)) = self.local.relations.get_mut(id) {
1591            for grantee in grantees {
1592                if let Some(role_id) = Self::resolve_role_name(
1593                    grantee,
1594                    &self.local.current_role,
1595                    &self.local.session_role,
1596                ) {
1597                    rel.privileges.grant(role_id, privileges.clone());
1598                }
1599            }
1600        }
1601    }
1602
1603    fn apply_revoke_to_relation(
1604        &mut self,
1605        id: &ObjectId,
1606        privileges: &HashSet<Privilege>,
1607        revokees: &[crate::analysis::facts::RoleFact],
1608    ) {
1609        self.snapshot_relation(id);
1610        if let Some(RelationOverlay::Present(rel)) = self.local.relations.get_mut(id) {
1611            for revokee in revokees {
1612                if let Some(role_id) = Self::resolve_role_name(
1613                    revokee,
1614                    &self.local.current_role,
1615                    &self.local.session_role,
1616                ) {
1617                    rel.privileges.revoke(&role_id, privileges);
1618                }
1619            }
1620        }
1621    }
1622
1623    pub fn apply(
1624        &mut self,
1625        mutation: &Mutation,
1626        precomputed_cascade: Option<&CascadeResult>,
1627    ) -> MutationResult {
1628        if self.local.transaction_aborted
1629            && !matches!(
1630                mutation,
1631                Mutation::CommitTransaction
1632                    | Mutation::CommitAndChain
1633                    | Mutation::RollbackTransaction
1634                    | Mutation::RollbackAndChain
1635                    | Mutation::RollbackToSavepoint(_)
1636            )
1637        {
1638            return MutationResult::NotExecuted;
1639        }
1640
1641        let result = self.apply_inner(mutation, precomputed_cascade);
1642        if matches!(result, MutationResult::Conflict { .. }) && !self.local.transactions.is_empty()
1643        {
1644            self.local.transaction_aborted = true;
1645        }
1646        result
1647    }
1648
1649    fn apply_inner(
1650        &mut self,
1651        mutation: &Mutation,
1652        precomputed_cascade: Option<&CascadeResult>,
1653    ) -> MutationResult {
1654        match mutation {
1655            Mutation::CreateSchema(create_schema) => {
1656                if self.schema_is_present(&create_schema.name) {
1657                    return if create_schema.if_not_exists {
1658                        MutationResult::Skipped
1659                    } else {
1660                        MutationResult::Conflict {
1661                            reason: format!("schema '{}' already exists", create_schema.name),
1662                        }
1663                    };
1664                }
1665                let (owner_name, owner_known) = match &create_schema.authorization {
1666                    Some(role) => match self.role_fact_identity(role) {
1667                        Some(identity) => identity,
1668                        None => {
1669                            self.snapshot_confidence();
1670                            self.local.confidence = Confidence::Tainted;
1671                            (self.local.current_role.clone(), false)
1672                        }
1673                    },
1674                    None => (
1675                        self.local.current_role.clone(),
1676                        self.local.current_role_known,
1677                    ),
1678                };
1679                if owner_known && self.local.roles_known && self.present_role(&owner_name).is_none()
1680                {
1681                    return MutationResult::Conflict {
1682                        reason: format!("role '{}' does not exist", owner_name),
1683                    };
1684                }
1685                if !owner_known || !self.local.roles_known {
1686                    self.snapshot_confidence();
1687                    self.local.confidence = Confidence::Tainted;
1688                }
1689                self.snapshot_generation_counter();
1690                self.local.generation_counter += 1;
1691                let generation = self.local.generation_counter;
1692                self.snapshot_schema(&create_schema.name);
1693                self.local.schemas.insert(
1694                    create_schema.name.clone(),
1695                    SchemaOverlay::Present(crate::model::schema::SchemaState {
1696                        name: create_schema.name.clone(),
1697                        owner: ObjectId::new("", owner_name),
1698                        generation,
1699                    }),
1700                );
1701                self.snapshot_search_path();
1702                self.refresh_role_sensitive_search_path();
1703                MutationResult::Applied
1704            }
1705            Mutation::AlterSchema(alter_schema) => match alter_schema {
1706                crate::analysis::mutations::AlterSchemaMutation::OwnerTo { name, new_owner } => {
1707                    if !self.schema_is_present(name) {
1708                        if self.schema_absence_is_authoritative(name) {
1709                            return MutationResult::Conflict {
1710                                reason: format!("schema '{}' does not exist", name),
1711                            };
1712                        }
1713                        self.snapshot_confidence();
1714                        self.local.confidence = Confidence::Tainted;
1715                        return MutationResult::Skipped;
1716                    }
1717                    let Some((owner_name, owner_known)) = self.role_fact_identity(new_owner) else {
1718                        self.snapshot_confidence();
1719                        self.local.confidence = Confidence::Tainted;
1720                        return MutationResult::Skipped;
1721                    };
1722                    if owner_known
1723                        && self.local.roles_known
1724                        && self.present_role(&owner_name).is_none()
1725                    {
1726                        return MutationResult::Conflict {
1727                            reason: format!("role '{}' does not exist", owner_name),
1728                        };
1729                    }
1730                    if !owner_known || !self.local.roles_known {
1731                        self.snapshot_confidence();
1732                        self.local.confidence = Confidence::Tainted;
1733                    }
1734                    self.snapshot_schema(name);
1735                    if let Some(SchemaOverlay::Present(schema)) = self.local.schemas.get_mut(name) {
1736                        schema.owner = ObjectId::new("", owner_name);
1737                    }
1738                    MutationResult::Applied
1739                }
1740                crate::analysis::mutations::AlterSchemaMutation::Rename { old_name, new_name } => {
1741                    if !self.schema_is_present(old_name) {
1742                        if !self.schema_absence_is_authoritative(old_name) {
1743                            self.snapshot_confidence();
1744                            self.local.confidence = Confidence::Tainted;
1745                            return MutationResult::Skipped;
1746                        }
1747                        return MutationResult::Conflict {
1748                            reason: format!("schema '{}' does not exist", old_name),
1749                        };
1750                    }
1751                    if self.schema_is_present(new_name) {
1752                        return MutationResult::Conflict {
1753                            reason: format!("schema '{}' already exists", new_name),
1754                        };
1755                    }
1756                    if !self.schema_absence_is_authoritative(new_name) {
1757                        self.snapshot_confidence();
1758                        self.local.confidence = Confidence::Tainted;
1759                    }
1760                    self.snapshot_search_path();
1761                    self.rename_schema_namespace(old_name, new_name);
1762                    MutationResult::Applied
1763                }
1764            },
1765            Mutation::DropSchema(drop_schema) => {
1766                for name in &drop_schema.names {
1767                    if !self.schema_is_present(name) && self.schema_absence_is_authoritative(name) {
1768                        if !drop_schema.if_exists {
1769                            return MutationResult::Conflict {
1770                                reason: format!("schema '{}' does not exist", name),
1771                            };
1772                        }
1773                    } else if !self.schema_is_present(name) {
1774                        self.snapshot_confidence();
1775                        self.local.confidence = Confidence::Tainted;
1776                    }
1777                }
1778                let present_names: Vec<String> = drop_schema
1779                    .names
1780                    .iter()
1781                    .filter(|name| self.schema_is_present(name))
1782                    .cloned()
1783                    .collect();
1784                if present_names.is_empty() {
1785                    return MutationResult::Skipped;
1786                }
1787                if drop_schema.cascade {
1788                    self.snapshot_namespace();
1789                    let mut relations_to_drop = Vec::new();
1790                    for id in self.local.relations.keys() {
1791                        if drop_schema.names.contains(&id.schema) {
1792                            relations_to_drop.push(id.clone());
1793                        }
1794                    }
1795                    for id in relations_to_drop {
1796                        self.snapshot_relation(&id);
1797                        self.local.relations.insert(id, RelationOverlay::Dropped);
1798                    }
1799
1800                    let constraints_to_drop: Vec<(ObjectId, String)> = self
1801                        .local
1802                        .constraints
1803                        .keys()
1804                        .filter(|(table_id, _)| drop_schema.names.contains(&table_id.schema))
1805                        .cloned()
1806                        .collect();
1807                    for (table_id, name) in constraints_to_drop {
1808                        self.snapshot_constraint(&table_id, &name);
1809                        self.local.constraints.remove(&(table_id, name));
1810                    }
1811
1812                    let mut types_to_drop = Vec::new();
1813                    for id in self.local.types.keys() {
1814                        if drop_schema.names.contains(&id.schema) {
1815                            types_to_drop.push(id.clone());
1816                        }
1817                    }
1818                    for id in types_to_drop {
1819                        self.snapshot_type(&id);
1820                        self.local.types.insert(id, TypeOverlay::Dropped);
1821                    }
1822
1823                    let mut seqs_to_drop = Vec::new();
1824                    for id in self.local.sequences.keys() {
1825                        if drop_schema.names.contains(&id.schema) {
1826                            seqs_to_drop.push(id.clone());
1827                        }
1828                    }
1829                    for id in seqs_to_drop {
1830                        self.snapshot_sequence(&id);
1831                        self.local.sequences.insert(id, SequenceOverlay::Dropped);
1832                    }
1833
1834                    let functions_to_drop: Vec<ObjectId> = self
1835                        .local
1836                        .functions
1837                        .keys()
1838                        .filter(|id| drop_schema.names.contains(&id.schema))
1839                        .cloned()
1840                        .collect();
1841                    for id in functions_to_drop {
1842                        self.snapshot_function(&id);
1843                        self.local
1844                            .functions
1845                            .insert(id, crate::model::function::FunctionOverlay::Dropped);
1846                    }
1847
1848                    let triggers_to_drop: Vec<ObjectId> = self
1849                        .local
1850                        .triggers
1851                        .keys()
1852                        .filter(|id| drop_schema.names.contains(&id.schema))
1853                        .cloned()
1854                        .collect();
1855                    for id in triggers_to_drop {
1856                        self.snapshot_trigger(&id);
1857                        self.local.triggers.insert(id, TriggerOverlay::Dropped);
1858                    }
1859
1860                    self.local
1861                        .pending_validation
1862                        .retain(|(table, _)| !drop_schema.names.contains(&table.schema));
1863                    let publication_names: Vec<String> =
1864                        self.local.publications.keys().cloned().collect();
1865                    for publication_name in publication_names {
1866                        self.snapshot_publication(&publication_name);
1867                    }
1868                    for overlay in self.local.publications.values_mut() {
1869                        let crate::model::replication::PublicationOverlay::Present(publication) =
1870                            overlay
1871                        else {
1872                            continue;
1873                        };
1874                        let crate::analysis::facts::PublicationScope::Explicit(objects) =
1875                            &mut publication.scope
1876                        else {
1877                            continue;
1878                        };
1879                        objects.retain(|object| match object {
1880                            crate::analysis::facts::PublicationObjectFact::Table {
1881                                name, ..
1882                            } => name.schema.as_ref().is_none_or(|schema| {
1883                                !drop_schema.names.contains(&schema.resolve())
1884                            }),
1885                            crate::analysis::facts::PublicationObjectFact::SchemaTables {
1886                                schema,
1887                                ..
1888                            } => !drop_schema.names.contains(schema),
1889                            _ => true,
1890                        });
1891                    }
1892
1893                    self.snapshot_graph_full();
1894
1895                    let g = &mut self.local.graph;
1896                    g.edges.retain(|e| {
1897                        !drop_schema.names.contains(&e.dependent.schema)
1898                            && !drop_schema.names.contains(&e.referenced.schema)
1899                            && match &e.kind {
1900                                DependencyKind::TriggerOnTable { function_id, .. } => {
1901                                    !drop_schema.names.contains(&function_id.schema)
1902                                }
1903                                _ => true,
1904                            }
1905                    });
1906                } else {
1907                    // Non-cascade: fail if any objects in the schema still exist
1908                    let has_relation = self.local.relations.iter().any(|(id, ov)| {
1909                        drop_schema.names.contains(&id.schema)
1910                            && !matches!(ov, RelationOverlay::Dropped)
1911                    });
1912                    let has_type = self.local.types.iter().any(|(id, ov)| {
1913                        drop_schema.names.contains(&id.schema)
1914                            && !matches!(ov, TypeOverlay::Dropped)
1915                    });
1916                    let has_sequence = self.local.sequences.iter().any(|(id, ov)| {
1917                        drop_schema.names.contains(&id.schema)
1918                            && !matches!(ov, SequenceOverlay::Dropped)
1919                    });
1920                    let has_function = self.local.functions.iter().any(|(id, ov)| {
1921                        drop_schema.names.contains(&id.schema)
1922                            && !matches!(ov, crate::model::function::FunctionOverlay::Dropped)
1923                    });
1924                    let has_trigger = self.local.triggers.iter().any(|(id, ov)| {
1925                        drop_schema.names.contains(&id.schema)
1926                            && !matches!(ov, TriggerOverlay::Dropped)
1927                    });
1928                    if has_relation || has_type || has_sequence || has_function || has_trigger {
1929                        return MutationResult::Conflict {
1930                            reason: format!(
1931                                "schema(s) {:?} still contain objects; use CASCADE to drop them",
1932                                drop_schema.names
1933                            ),
1934                        };
1935                    }
1936                }
1937                for name in present_names {
1938                    self.snapshot_schema(&name);
1939                    self.local.schemas.insert(name, SchemaOverlay::Dropped);
1940                }
1941                self.snapshot_search_path();
1942                self.refresh_role_sensitive_search_path();
1943                MutationResult::Applied
1944            }
1945            Mutation::DropTable(drop_table) => {
1946                if !self.relation_is_present(&drop_table.id) {
1947                    if drop_table.if_exists {
1948                        return MutationResult::Skipped;
1949                    }
1950                    if self.baseline_available && self.baseline_covers_object(&drop_table.id) {
1951                        return MutationResult::Conflict {
1952                            reason: format!("table '{}' does not exist", drop_table.id),
1953                        };
1954                    }
1955                    self.snapshot_confidence();
1956                    self.local.confidence = Confidence::Tainted;
1957                    return MutationResult::Skipped;
1958                }
1959                if !matches!(
1960                    self.local.relations.get(&drop_table.id),
1961                    Some(RelationOverlay::Present(relation))
1962                        if relation.kind == RelationKind::Table
1963                ) {
1964                    return MutationResult::Conflict {
1965                        reason: format!("'{}' is not a table", drop_table.id),
1966                    };
1967                }
1968
1969                let renames: Vec<DependencyEdge> = self
1970                    .local
1971                    .graph
1972                    .edges
1973                    .iter()
1974                    .filter(|e| matches!(e.kind, DependencyKind::RenameTo))
1975                    .cloned()
1976                    .collect();
1977                let resolve = |id: &ObjectId| -> ObjectId {
1978                    let mut current = id;
1979                    let mut visited = HashSet::new();
1980                    loop {
1981                        if !visited.insert(current.clone()) {
1982                            return id.clone();
1983                        }
1984                        match renames.iter().find(|r| &r.dependent == current) {
1985                            Some(edge) => current = &edge.referenced,
1986                            None => return current.clone(),
1987                        }
1988                    }
1989                };
1990
1991                let resolved_drop = resolve(&drop_table.id);
1992                let mut dropped_relations = HashSet::from([resolved_drop.clone()]);
1993
1994                if drop_table.cascade {
1995                    let local_closure;
1996                    let closure = match precomputed_cascade {
1997                        Some(c) => c,
1998                        None => {
1999                            local_closure = self.get_cascade_closure(&drop_table.id);
2000                            &local_closure
2001                        }
2002                    };
2003                    dropped_relations = closure.dropped_relations.clone();
2004
2005                    for dropped_rel_id in &closure.dropped_relations {
2006                        self.snapshot_relation(dropped_rel_id);
2007                        self.local
2008                            .relations
2009                            .insert(dropped_rel_id.clone(), RelationOverlay::Dropped);
2010                    }
2011
2012                    self.snapshot_graph_full();
2013                    self.local.graph.edges.retain(|e| match &e.kind {
2014                        DependencyKind::IndexOnRelation { .. } => {
2015                            !closure.dropped_indexes.contains(&resolve(&e.dependent))
2016                        }
2017                        DependencyKind::ForeignKey {
2018                            constraint_name, ..
2019                        } => {
2020                            let from_dropped =
2021                                closure.dropped_relations.contains(&resolve(&e.dependent));
2022                            let to_dropped =
2023                                closure.dropped_relations.contains(&resolve(&e.referenced));
2024                            let constraint_explicitly_dropped = if let Some(cname) = constraint_name
2025                            {
2026                                closure
2027                                    .dropped_constraints
2028                                    .contains(&(resolve(&e.dependent), cname.clone()))
2029                            } else {
2030                                false
2031                            };
2032                            !(from_dropped || to_dropped || constraint_explicitly_dropped)
2033                        }
2034                        DependencyKind::ViewDependency { .. } => {
2035                            !closure.dropped_relations.contains(&resolve(&e.dependent))
2036                        }
2037                        DependencyKind::SequenceOwnedBy { .. } => {
2038                            !closure.dropped_relations.contains(&resolve(&e.referenced))
2039                        }
2040                        _ => true,
2041                    });
2042                } else {
2043                    let has_view_deps = self.local.graph.edges.iter().any(|e| {
2044                        matches!(e.kind, DependencyKind::ViewDependency { .. })
2045                            && resolve(&e.referenced) == resolved_drop
2046                    });
2047                    let has_fk_deps = self.local.graph.edges.iter().any(|e| {
2048                        matches!(e.kind, DependencyKind::ForeignKey { .. })
2049                            && resolve(&e.referenced) == resolved_drop
2050                            && resolve(&e.dependent) != resolved_drop
2051                    });
2052                    let has_partition_deps = self.local.graph.edges.iter().any(|e| {
2053                        matches!(e.kind, DependencyKind::PartitionOf)
2054                            && resolve(&e.referenced) == resolved_drop
2055                    });
2056
2057                    if has_view_deps || has_fk_deps || has_partition_deps {
2058                        return MutationResult::Conflict {
2059                            reason: format!(
2060                                "relation '{}' still has dependent objects; use CASCADE",
2061                                drop_table.id
2062                            ),
2063                        };
2064                    }
2065
2066                    self.snapshot_relation(&drop_table.id);
2067                    self.local
2068                        .relations
2069                        .insert(drop_table.id.clone(), RelationOverlay::Dropped);
2070
2071                    self.snapshot_graph_full();
2072                    self.local.graph.edges.retain(|e| {
2073                        !(matches!(e.kind, DependencyKind::SequenceOwnedBy { .. })
2074                            && resolve(&e.referenced) == resolved_drop)
2075                    });
2076                }
2077
2078                let owned_sequences_to_drop: Vec<ObjectId> = self
2079                    .local
2080                    .sequences
2081                    .iter()
2082                    .filter_map(|(id, overlay)| match overlay {
2083                        SequenceOverlay::Present(sequence)
2084                            if sequence.owned_by.as_ref().is_some_and(|(table, _)| {
2085                                dropped_relations.contains(&resolve(table))
2086                            }) =>
2087                        {
2088                            Some(id.clone())
2089                        }
2090                        _ => None,
2091                    })
2092                    .collect();
2093                for sequence_id in owned_sequences_to_drop {
2094                    self.snapshot_sequence(&sequence_id);
2095                    self.local
2096                        .sequences
2097                        .insert(sequence_id, SequenceOverlay::Dropped);
2098                }
2099
2100                let constraints_to_drop: Vec<(ObjectId, String)> = self
2101                    .local
2102                    .constraints
2103                    .keys()
2104                    .filter(|(table_id, _)| dropped_relations.contains(&resolve(table_id)))
2105                    .cloned()
2106                    .collect();
2107                for (table_id, name) in constraints_to_drop {
2108                    self.snapshot_constraint(&table_id, &name);
2109                    self.local.constraints.remove(&(table_id, name));
2110                }
2111
2112                let triggers_to_drop: Vec<ObjectId> = self
2113                    .local
2114                    .triggers
2115                    .iter()
2116                    .filter_map(|(id, overlay)| {
2117                        let TriggerOverlay::Present(trigger) = overlay else {
2118                            return None;
2119                        };
2120                        let graph_matches = self.local.graph.edges.iter().any(|edge| {
2121                            matches!(edge.kind, DependencyKind::TriggerOnTable { .. })
2122                                && edge.dependent == *id
2123                                && dropped_relations.contains(&resolve(&edge.referenced))
2124                        });
2125                        (dropped_relations.contains(&resolve(&trigger.table_id)) || graph_matches)
2126                            .then(|| id.clone())
2127                    })
2128                    .collect();
2129                for trigger_id in triggers_to_drop {
2130                    self.snapshot_trigger(&trigger_id);
2131                    self.local
2132                        .triggers
2133                        .insert(trigger_id, TriggerOverlay::Dropped);
2134                }
2135
2136                // PostgreSQL drops triggers only after the table drop succeeds.
2137                self.snapshot_graph_full();
2138                self.local.graph.edges.retain(|e| {
2139                    !(matches!(e.kind, DependencyKind::TriggerOnTable { .. })
2140                        && dropped_relations.contains(&resolve(&e.referenced)))
2141                });
2142
2143                self.snapshot_graph_full();
2144                self.local.graph.edges.retain(|e| {
2145                    if let DependencyKind::PartitionOf = e.kind {
2146                        resolve(&e.referenced) != resolved_drop
2147                            && resolve(&e.dependent) != resolved_drop
2148                    } else {
2149                        true
2150                    }
2151                });
2152
2153                let publication_updates: Vec<(String, Vec<_>)> = self
2154                    .local
2155                    .publications
2156                    .iter()
2157                    .filter_map(|(name, overlay)| {
2158                        let crate::model::replication::PublicationOverlay::Present(publication) =
2159                            overlay
2160                        else {
2161                            return None;
2162                        };
2163                        let crate::analysis::facts::PublicationScope::Explicit(objects) =
2164                            &publication.scope
2165                        else {
2166                            return None;
2167                        };
2168                        let retained = objects
2169                            .iter()
2170                            .filter(|object| {
2171                                let crate::analysis::facts::PublicationObjectFact::Table {
2172                                    name,
2173                                    ..
2174                                } = object
2175                                else {
2176                                    return true;
2177                                };
2178                                !dropped_relations
2179                                    .contains(&resolve(&self.resolve_relation_id(name)))
2180                            })
2181                            .cloned()
2182                            .collect::<Vec<_>>();
2183                        (retained.len() != objects.len()).then(|| (name.clone(), retained))
2184                    })
2185                    .collect();
2186                for (publication_name, retained) in publication_updates {
2187                    self.snapshot_publication(&publication_name);
2188                    if let Some(crate::model::replication::PublicationOverlay::Present(publication)) =
2189                        self.local.publications.get_mut(&publication_name)
2190                        && let crate::analysis::facts::PublicationScope::Explicit(objects) =
2191                            &mut publication.scope
2192                    {
2193                        *objects = retained;
2194                    }
2195                }
2196                self.snapshot_graph_full();
2197                self.local.graph.edges.retain(|edge| {
2198                    !matches!(edge.kind, DependencyKind::PublicationIncludes { .. })
2199                        || !dropped_relations.contains(&resolve(&edge.dependent))
2200                });
2201
2202                MutationResult::Applied
2203            }
2204            Mutation::CreateTable(create) => {
2205                if create.if_not_exists && self.relation_namespace_is_taken(&create.id) {
2206                    return MutationResult::Skipped;
2207                }
2208                if self.relation_namespace_is_taken(&create.id) {
2209                    return MutationResult::Conflict {
2210                        reason: format!("relation '{}' already exists", create.id),
2211                    };
2212                }
2213
2214                // PostgreSQL chooses all implicit sequence names before the
2215                // table becomes visible. Reserve them up front so a collision
2216                // or malformed statement cannot leave partial local state.
2217                let mut reserved_sequences = HashSet::new();
2218                let mut implicit_sequences = Vec::new();
2219                for column in &create.columns {
2220                    let kind = match column.generation {
2221                        crate::analysis::facts::ColumnGeneration::Serial => {
2222                            Some(SequenceKind::SerialLike)
2223                        }
2224                        crate::analysis::facts::ColumnGeneration::Identity => {
2225                            Some(SequenceKind::Identity)
2226                        }
2227                        crate::analysis::facts::ColumnGeneration::Ordinary => None,
2228                    };
2229                    if let Some(kind) = kind {
2230                        let sequence_id = self.next_implicit_sequence_id(
2231                            &create.id,
2232                            &column.name,
2233                            &reserved_sequences,
2234                        );
2235                        reserved_sequences.insert(sequence_id.clone());
2236                        implicit_sequences.push((sequence_id, column.name.clone(), kind));
2237                    }
2238                }
2239
2240                self.snapshot_relation(&create.id);
2241
2242                self.snapshot_generation_counter();
2243                self.local.generation_counter += 1;
2244                let generation = self.local.generation_counter;
2245
2246                let resolved_persistence = match create.persistence {
2247                    PersistenceMutation::Permanent => {
2248                        crate::model::relation::Persistence::Permanent
2249                    }
2250                    PersistenceMutation::Temporary => {
2251                        crate::model::relation::Persistence::Temporary
2252                    }
2253                    PersistenceMutation::Unlogged => crate::model::relation::Persistence::Unlogged,
2254                };
2255
2256                let mut rel_state = RelationState::new(
2257                    create.id.clone(),
2258                    ObjectId::new("", &self.local.current_role),
2259                    generation,
2260                    if create.as_select { None } else { Some(0) },
2261                    RelationKind::Table,
2262                    resolved_persistence,
2263                    self.local.transactions.len(),
2264                );
2265
2266                // Store partition strategy information
2267                rel_state.partition_type = create
2268                    .partition_by
2269                    .as_ref()
2270                    .and_then(|pb| pb.split_whitespace().nth(2).map(|s| s.to_uppercase()))
2271                    .or_else(|| {
2272                        create.partition_of.as_ref().and_then(|parent_id| {
2273                            self.local.relations.get(parent_id).and_then(|r| {
2274                                if let RelationOverlay::Present(rel) = r {
2275                                    rel.partition_type.clone()
2276                                } else {
2277                                    None
2278                                }
2279                            })
2280                        })
2281                    });
2282                rel_state.partition_by = create.partition_by.clone();
2283
2284                let pk_columns: HashSet<&str> = create
2285                    .table_constraints
2286                    .iter()
2287                    .filter_map(|tc| {
2288                        if let TableConstraintFact::PrimaryKey { columns, .. } = tc {
2289                            Some(columns.iter().map(|s| s.as_str()))
2290                        } else {
2291                            None
2292                        }
2293                    })
2294                    .flatten()
2295                    .collect();
2296
2297                for col in &create.columns {
2298                    let is_pk = col.is_primary_key || pk_columns.contains(col.name.as_str());
2299                    rel_state.apply_column_action(&ColumnAction::Add {
2300                        name: col.name.clone(),
2301                        data_type: col.ty.clone(),
2302                        not_null: col.not_null || is_pk,
2303                        default: col.default.clone(),
2304                    });
2305                    if let Some(column) = rel_state
2306                        .columns
2307                        .iter_mut()
2308                        .find(|column| column.name == col.name)
2309                    {
2310                        column.type_id = column
2311                            .data_type
2312                            .as_deref()
2313                            .and_then(|raw| self.resolve_type_reference(raw));
2314                    }
2315                }
2316
2317                for (sequence_id, column_name, _) in &implicit_sequences {
2318                    if let Some(column) = rel_state
2319                        .columns
2320                        .iter_mut()
2321                        .find(|column| column.name == *column_name)
2322                    {
2323                        column.default = Some(Self::sequence_nextval_default(sequence_id));
2324                        column.default_expr_text = Some(format!(
2325                            "nextval('{}.{}'::regclass)",
2326                            sequence_id.schema, sequence_id.name
2327                        ));
2328                        column.is_nullable = false;
2329                    }
2330                }
2331
2332                self.local
2333                    .relations
2334                    .insert(create.id.clone(), RelationOverlay::Present(rel_state));
2335
2336                for (sequence_id, column_name, kind) in implicit_sequences {
2337                    self.snapshot_sequence(&sequence_id);
2338                    self.snapshot_generation_counter();
2339                    self.local.generation_counter += 1;
2340                    self.local.sequences.insert(
2341                        sequence_id.clone(),
2342                        SequenceOverlay::Present(SequenceState {
2343                            id: sequence_id.clone(),
2344                            owner: ObjectId::new("", &self.local.current_role),
2345                            owned_by: Some((create.id.clone(), column_name.clone())),
2346                            kind,
2347                            generation: self.local.generation_counter,
2348                        }),
2349                    );
2350                    self.snapshot_graph();
2351                    self.local.graph.edges.push(DependencyEdge::new(
2352                        sequence_id,
2353                        create.id.clone(),
2354                        DependencyKind::SequenceOwnedBy {
2355                            column: column_name,
2356                        },
2357                    ));
2358                }
2359
2360                let primary_key_name = create
2361                    .columns
2362                    .iter()
2363                    .find(|column| column.is_primary_key)
2364                    .map(|column| column.primary_key_constraint_name.clone())
2365                    .or_else(|| {
2366                        create.table_constraints.iter().find_map(|constraint| {
2367                            if let TableConstraintFact::PrimaryKey {
2368                                constraint_name, ..
2369                            } = constraint
2370                            {
2371                                Some(constraint_name.clone())
2372                            } else {
2373                                None
2374                            }
2375                        })
2376                    });
2377                if let Some(explicit_name) = primary_key_name {
2378                    let name = explicit_name.unwrap_or_else(|| {
2379                        self.next_generated_constraint_name(
2380                            &create.id,
2381                            &create.id.name,
2382                            None,
2383                            "pkey",
2384                        )
2385                    });
2386                    self.snapshot_constraint(&create.id, &name);
2387                    self.local.constraints.insert(
2388                        (create.id.clone(), name.clone()),
2389                        ConstraintState {
2390                            table_id: create.id.clone(),
2391                            name,
2392                            kind: ConstraintKind::PrimaryKey,
2393                            validated: true,
2394                        },
2395                    );
2396                }
2397
2398                let unique_constraints = create
2399                    .columns
2400                    .iter()
2401                    .filter(|column| column.is_unique)
2402                    .map(|column| {
2403                        (
2404                            column.unique_constraint_name.as_ref(),
2405                            vec![column.name.as_str()],
2406                        )
2407                    })
2408                    .chain(create.table_constraints.iter().filter_map(|constraint| {
2409                        if let TableConstraintFact::Unique {
2410                            constraint_name,
2411                            columns,
2412                        } = constraint
2413                        {
2414                            Some((
2415                                constraint_name.as_ref(),
2416                                columns.iter().map(String::as_str).collect(),
2417                            ))
2418                        } else {
2419                            None
2420                        }
2421                    }))
2422                    .collect::<Vec<_>>();
2423                for (explicit_name, columns) in unique_constraints {
2424                    let name = explicit_name.cloned().unwrap_or_else(|| {
2425                        self.next_generated_constraint_name(
2426                            &create.id,
2427                            &create.id.name,
2428                            Some(&columns.join("_")),
2429                            "key",
2430                        )
2431                    });
2432                    self.snapshot_constraint(&create.id, &name);
2433                    self.local.constraints.insert(
2434                        (create.id.clone(), name.clone()),
2435                        ConstraintState {
2436                            table_id: create.id.clone(),
2437                            name,
2438                            kind: ConstraintKind::Unique,
2439                            validated: true,
2440                        },
2441                    );
2442                }
2443
2444                if let Some(parent_id) = &create.partition_of {
2445                    self.snapshot_graph();
2446                    self.local.graph.edges.push(DependencyEdge::new(
2447                        create.id.clone(),
2448                        parent_id.clone(),
2449                        DependencyKind::PartitionOf,
2450                    ));
2451                }
2452
2453                if !create.foreign_keys.is_empty() {
2454                    self.snapshot_graph();
2455                }
2456
2457                for fk in &create.foreign_keys {
2458                    self.local.graph.edges.push(DependencyEdge::new(
2459                        create.id.clone(),
2460                        fk.to_table.clone(),
2461                        DependencyKind::ForeignKey {
2462                            constraint_name: fk.constraint_name.clone(),
2463                            from_columns: fk.from_columns.clone(),
2464                            to_columns: fk.to_columns.clone(),
2465                            from_generation: generation,
2466                        },
2467                    ));
2468                }
2469                MutationResult::Applied
2470            }
2471            Mutation::CreateView(create_view) => {
2472                if self.relation_namespace_is_taken(&create_view.id) {
2473                    let is_replaceable_view = matches!(
2474                        self.local.relations.get(&create_view.id),
2475                        Some(RelationOverlay::Present(relation))
2476                            if relation.kind == RelationKind::View
2477                    );
2478                    if !create_view.or_replace || !is_replaceable_view {
2479                        return MutationResult::Conflict {
2480                            reason: format!("relation '{}' already exists", create_view.id),
2481                        };
2482                    }
2483                }
2484                self.snapshot_relation(&create_view.id);
2485                self.snapshot_generation_counter();
2486                self.local.generation_counter += 1;
2487                let generation = self.local.generation_counter;
2488
2489                self.local.relations.insert(
2490                    create_view.id.clone(),
2491                    RelationOverlay::Present(RelationState::new(
2492                        create_view.id.clone(),
2493                        ObjectId::new("", &self.local.current_role),
2494                        generation,
2495                        None,
2496                        RelationKind::View,
2497                        Persistence::Permanent,
2498                        self.local.transactions.len(),
2499                    )),
2500                );
2501
2502                self.snapshot_graph();
2503                for dep in &create_view.depends_on {
2504                    self.local.graph.edges.push(DependencyEdge::new(
2505                        create_view.id.clone(),
2506                        dep.clone(),
2507                        DependencyKind::ViewDependency {
2508                            view_generation: generation,
2509                        },
2510                    ));
2511                }
2512                MutationResult::Applied
2513            }
2514            Mutation::CreateMaterializedView(create_mv) => {
2515                if self.relation_namespace_is_taken(&create_mv.id) {
2516                    return MutationResult::Conflict {
2517                        reason: format!("relation '{}' already exists", create_mv.id),
2518                    };
2519                }
2520                self.snapshot_relation(&create_mv.id);
2521                self.snapshot_generation_counter();
2522                self.local.generation_counter += 1;
2523                let generation = self.local.generation_counter;
2524
2525                self.local.relations.insert(
2526                    create_mv.id.clone(),
2527                    RelationOverlay::Present(RelationState::new(
2528                        create_mv.id.clone(),
2529                        ObjectId::new("", &self.local.current_role),
2530                        generation,
2531                        None,
2532                        RelationKind::MaterializedView,
2533                        Persistence::Permanent,
2534                        self.local.transactions.len(),
2535                    )),
2536                );
2537
2538                self.snapshot_graph();
2539                for dep in &create_mv.depends_on {
2540                    self.local.graph.edges.push(DependencyEdge::new(
2541                        create_mv.id.clone(),
2542                        dep.clone(),
2543                        DependencyKind::ViewDependency {
2544                            view_generation: generation,
2545                        },
2546                    ));
2547                }
2548                MutationResult::Applied
2549            }
2550            Mutation::RefreshMaterializedView(_) => MutationResult::Applied,
2551            Mutation::CreateIndex(create_idx) => {
2552                let exists = self.index_is_present(&create_idx.id);
2553                if create_idx.if_not_exists && exists {
2554                    return MutationResult::Skipped;
2555                }
2556                if self.relation_namespace_is_taken(&create_idx.id) {
2557                    return MutationResult::Conflict {
2558                        reason: format!("relation '{}' already exists", create_idx.id),
2559                    };
2560                }
2561                self.snapshot_graph();
2562                self.local.graph.edges.push(DependencyEdge::new(
2563                    create_idx.id.clone(),
2564                    create_idx.table.clone(),
2565                    DependencyKind::IndexOnRelation {
2566                        using_method: create_idx.using_method.clone(),
2567                        has_predicate: create_idx.has_predicate,
2568                        is_concurrent: create_idx.concurrently,
2569                        is_unique: create_idx.unique,
2570                        eligibility_known: true,
2571                    },
2572                ));
2573                MutationResult::Applied
2574            }
2575            Mutation::CreatePolicy(create_policy) => {
2576                self.snapshot_relation(&create_policy.table);
2577                if let Some(RelationOverlay::Present(rel)) =
2578                    self.local.relations.get_mut(&create_policy.table)
2579                {
2580                    if rel.policies.contains(&create_policy.name) {
2581                        return MutationResult::Conflict {
2582                            reason: format!(
2583                                "policy '{}' already exists on relation '{}'",
2584                                create_policy.name, create_policy.table
2585                            ),
2586                        };
2587                    }
2588                    rel.policies.insert(create_policy.name.clone());
2589                } else {
2590                    return MutationResult::Conflict {
2591                        reason: format!("relation '{}' does not exist", create_policy.table),
2592                    };
2593                }
2594                MutationResult::Applied
2595            }
2596            Mutation::DropPolicy(drop_policy) => {
2597                self.snapshot_relation(&drop_policy.table);
2598                if let Some(RelationOverlay::Present(rel)) =
2599                    self.local.relations.get_mut(&drop_policy.table)
2600                {
2601                    if !rel.policies.contains(&drop_policy.name) {
2602                        return if drop_policy.if_exists {
2603                            MutationResult::Skipped
2604                        } else {
2605                            MutationResult::Conflict {
2606                                reason: format!(
2607                                    "policy '{}' does not exist on relation '{}'",
2608                                    drop_policy.name, drop_policy.table
2609                                ),
2610                            }
2611                        };
2612                    }
2613                    rel.policies.remove(&drop_policy.name);
2614                } else {
2615                    return MutationResult::Conflict {
2616                        reason: format!("relation '{}' does not exist", drop_policy.table),
2617                    };
2618                }
2619                MutationResult::Applied
2620            }
2621            Mutation::CreateTrigger(create_trigger) => {
2622                let trigger_id = Self::trigger_key(&create_trigger.table, &create_trigger.name);
2623                if matches!(
2624                    self.local.triggers.get(&trigger_id),
2625                    Some(TriggerOverlay::Present(_))
2626                ) {
2627                    return MutationResult::Conflict {
2628                        reason: format!(
2629                            "trigger '{}' already exists on relation '{}'",
2630                            create_trigger.name, create_trigger.table
2631                        ),
2632                    };
2633                }
2634                self.snapshot_trigger(&trigger_id);
2635                self.local.triggers.insert(
2636                    trigger_id.clone(),
2637                    TriggerOverlay::Present(crate::model::trigger::TriggerState {
2638                        name: create_trigger.name.clone(),
2639                        id: trigger_id.clone(),
2640                        table_id: create_trigger.table.clone(),
2641                        enabled_mode: crate::model::trigger::TriggerEnableMode::Origin,
2642                        generation: self.local.generation_counter,
2643                    }),
2644                );
2645
2646                self.snapshot_relation(&create_trigger.table);
2647                if let Some(RelationOverlay::Present(rel)) =
2648                    self.local.relations.get_mut(&create_trigger.table)
2649                {
2650                    rel.triggers.insert(create_trigger.name.clone());
2651                }
2652
2653                self.snapshot_graph_full();
2654                self.local.graph.edges.push(DependencyEdge::new(
2655                    trigger_id.clone(),
2656                    create_trigger.table.clone(),
2657                    DependencyKind::TriggerOnTable {
2658                        trigger_id: trigger_id.clone(),
2659                        function_id: create_trigger.function_id.clone(),
2660                    },
2661                ));
2662
2663                MutationResult::Applied
2664            }
2665            Mutation::DropTrigger(drop_trigger) => {
2666                let trigger_id = Self::trigger_key(&drop_trigger.table, &drop_trigger.name);
2667                if !matches!(
2668                    self.local.triggers.get(&trigger_id),
2669                    Some(TriggerOverlay::Present(_))
2670                ) {
2671                    return if drop_trigger.if_exists {
2672                        MutationResult::Skipped
2673                    } else {
2674                        MutationResult::Conflict {
2675                            reason: format!(
2676                                "trigger '{}' does not exist on relation '{}'",
2677                                drop_trigger.name, drop_trigger.table
2678                            ),
2679                        }
2680                    };
2681                }
2682                self.snapshot_trigger(&trigger_id);
2683                self.local
2684                    .triggers
2685                    .insert(trigger_id.clone(), TriggerOverlay::Dropped);
2686
2687                self.snapshot_relation(&drop_trigger.table);
2688                if let Some(RelationOverlay::Present(rel)) =
2689                    self.local.relations.get_mut(&drop_trigger.table)
2690                {
2691                    rel.triggers.remove(&drop_trigger.name);
2692                }
2693
2694                self.snapshot_graph_full();
2695                self.local.graph.edges.retain(|e| {
2696                    !(matches!(e.kind, DependencyKind::TriggerOnTable { .. })
2697                        && e.dependent == trigger_id)
2698                });
2699
2700                MutationResult::Applied
2701            }
2702            Mutation::RenameTrigger(rename_trigger) => {
2703                let old_id = Self::trigger_key(&rename_trigger.table, &rename_trigger.name);
2704                let new_id = Self::trigger_key(&rename_trigger.table, &rename_trigger.new_name);
2705                let Some(TriggerOverlay::Present(mut trigger)) =
2706                    self.local.triggers.get(&old_id).cloned()
2707                else {
2708                    return MutationResult::Conflict {
2709                        reason: format!(
2710                            "trigger '{}' does not exist on relation '{}'",
2711                            rename_trigger.name, rename_trigger.table
2712                        ),
2713                    };
2714                };
2715                if old_id != new_id
2716                    && matches!(
2717                        self.local.triggers.get(&new_id),
2718                        Some(TriggerOverlay::Present(_))
2719                    )
2720                {
2721                    return MutationResult::Conflict {
2722                        reason: format!(
2723                            "trigger '{}' already exists on relation '{}'",
2724                            rename_trigger.new_name, rename_trigger.table
2725                        ),
2726                    };
2727                }
2728                self.snapshot_trigger(&old_id);
2729                self.snapshot_trigger(&new_id);
2730                self.snapshot_relation(&rename_trigger.table);
2731                self.snapshot_graph_full();
2732                self.local.triggers.remove(&old_id);
2733                trigger.id = new_id.clone();
2734                trigger.name = rename_trigger.new_name.clone();
2735                self.local
2736                    .triggers
2737                    .insert(new_id.clone(), TriggerOverlay::Present(trigger));
2738                if let Some(RelationOverlay::Present(relation)) =
2739                    self.local.relations.get_mut(&rename_trigger.table)
2740                {
2741                    relation.triggers.remove(&rename_trigger.name);
2742                    relation.triggers.insert(rename_trigger.new_name.clone());
2743                }
2744                self.local.graph.propagate_rename(&old_id, &new_id);
2745                self.local.graph.edges.push(DependencyEdge::new(
2746                    old_id,
2747                    new_id,
2748                    DependencyKind::RenameTo,
2749                ));
2750                MutationResult::Applied
2751            }
2752            Mutation::AlterTable(alter) => {
2753                if let AlterTableActionMutation::OwnerTo { new_owner } = &alter.action {
2754                    let Some((owner, known)) = self.role_fact_identity(new_owner) else {
2755                        self.snapshot_confidence();
2756                        self.local.confidence = Confidence::Tainted;
2757                        return MutationResult::Skipped;
2758                    };
2759                    if !known {
2760                        self.snapshot_confidence();
2761                        self.local.confidence = Confidence::Tainted;
2762                    }
2763                    self.snapshot_relation(&alter.id);
2764                    return match self.local.relations.get_mut(&alter.id) {
2765                        Some(RelationOverlay::Present(relation)) => {
2766                            relation.owner = ObjectId::new("", owner);
2767                            MutationResult::Applied
2768                        }
2769                        _ => MutationResult::Conflict {
2770                            reason: format!("relation '{}' does not exist", alter.id),
2771                        },
2772                    };
2773                }
2774
2775                let trigger_mode = match &alter.action {
2776                    AlterTableActionMutation::DisableTrigger { trigger_name } => Some((
2777                        trigger_name.as_deref(),
2778                        crate::model::trigger::TriggerEnableMode::Disabled,
2779                    )),
2780                    AlterTableActionMutation::EnableTrigger { trigger_name } => Some((
2781                        trigger_name.as_deref(),
2782                        crate::model::trigger::TriggerEnableMode::Origin,
2783                    )),
2784                    _ => None,
2785                };
2786                if let Some((trigger_name, enabled_mode)) = trigger_mode {
2787                    let all = trigger_name.is_none_or(|name| name.eq_ignore_ascii_case("all"));
2788                    let trigger_ids: Vec<ObjectId> = self
2789                        .local
2790                        .triggers
2791                        .iter()
2792                        .filter_map(|(id, overlay)| {
2793                            let TriggerOverlay::Present(trigger) = overlay else {
2794                                return None;
2795                            };
2796                            (trigger.table_id == alter.id
2797                                && (all || trigger_name == Some(trigger.name.as_str())))
2798                            .then(|| id.clone())
2799                        })
2800                        .collect();
2801                    for trigger_id in trigger_ids {
2802                        self.snapshot_trigger(&trigger_id);
2803                        if let Some(TriggerOverlay::Present(trigger)) =
2804                            self.local.triggers.get_mut(&trigger_id)
2805                        {
2806                            trigger.enabled_mode = enabled_mode;
2807                        }
2808                    }
2809                    return MutationResult::Applied;
2810                }
2811
2812                if let AlterTableActionMutation::AddForeignKey {
2813                    to_table,
2814                    from_columns,
2815                    to_columns,
2816                    ..
2817                } = &alter.action
2818                {
2819                    if let Some(RelationOverlay::Present(child)) =
2820                        self.local.relations.get(&alter.id)
2821                        && let Some(column) =
2822                            from_columns.iter().find(|column| !child.has_column(column))
2823                    {
2824                        return MutationResult::Conflict {
2825                            reason: format!(
2826                                "foreign key column '{}' does not exist on relation '{}'",
2827                                column, alter.id
2828                            ),
2829                        };
2830                    }
2831
2832                    let Some(RelationOverlay::Present(parent)) = self.local.relations.get(to_table)
2833                    else {
2834                        return MutationResult::Conflict {
2835                            reason: format!(
2836                                "foreign key references relation '{}' which does not exist",
2837                                to_table
2838                            ),
2839                        };
2840                    };
2841                    if let Some(column) =
2842                        to_columns.iter().find(|column| !parent.has_column(column))
2843                    {
2844                        return MutationResult::Conflict {
2845                            reason: format!(
2846                                "foreign key references column '{}.{}' which does not exist",
2847                                to_table, column
2848                            ),
2849                        };
2850                    }
2851                }
2852
2853                let implicit_add = match &alter.action {
2854                    AlterTableActionMutation::AddColumn {
2855                        name, generation, ..
2856                    } => match generation {
2857                        crate::analysis::facts::ColumnGeneration::Serial => Some((
2858                            self.next_implicit_sequence_id(&alter.id, name, &HashSet::new()),
2859                            name.clone(),
2860                            SequenceKind::SerialLike,
2861                        )),
2862                        crate::analysis::facts::ColumnGeneration::Identity => Some((
2863                            self.next_implicit_sequence_id(&alter.id, name, &HashSet::new()),
2864                            name.clone(),
2865                            SequenceKind::Identity,
2866                        )),
2867                        crate::analysis::facts::ColumnGeneration::Ordinary => None,
2868                    },
2869                    _ => None,
2870                };
2871                let owned_sequences_for_column: Vec<ObjectId> = match &alter.action {
2872                    AlterTableActionMutation::DropColumn { name, .. }
2873                    | AlterTableActionMutation::RenameColumn { from: name, .. } => self
2874                        .local
2875                        .sequences
2876                        .iter()
2877                        .filter_map(|(id, overlay)| match overlay {
2878                            SequenceOverlay::Present(sequence)
2879                                if sequence.owned_by.as_ref()
2880                                    == Some(&(alter.id.clone(), name.clone())) =>
2881                            {
2882                                Some(id.clone())
2883                            }
2884                            _ => None,
2885                        })
2886                        .collect(),
2887                    _ => Vec::new(),
2888                };
2889
2890                let using_index = match &alter.action {
2891                    AlterTableActionMutation::AddUniqueConstraint { using_index, .. }
2892                    | AlterTableActionMutation::AddPrimaryKeyConstraint { using_index, .. } => {
2893                        using_index.as_ref()
2894                    }
2895                    _ => None,
2896                };
2897                if let Some(index) = using_index {
2898                    let Some(edge) = self.local.graph.edges.iter().find(|edge| {
2899                        matches!(edge.kind, DependencyKind::IndexOnRelation { .. })
2900                            && edge.dependent == *index
2901                    }) else {
2902                        return MutationResult::Conflict {
2903                            reason: format!(
2904                                "constraint references index '{}' which does not exist",
2905                                index
2906                            ),
2907                        };
2908                    };
2909                    if edge.referenced != alter.id {
2910                        return MutationResult::Conflict {
2911                            reason: format!(
2912                                "constraint index '{}' belongs to relation '{}', not '{}'",
2913                                index, edge.referenced, alter.id
2914                            ),
2915                        };
2916                    }
2917                    if let DependencyKind::IndexOnRelation {
2918                        has_predicate,
2919                        is_unique,
2920                        eligibility_known,
2921                        ..
2922                    } = &edge.kind
2923                        && *eligibility_known
2924                        && (!is_unique || *has_predicate)
2925                    {
2926                        return MutationResult::Conflict {
2927                            reason: format!(
2928                                "constraint index '{}' must be unique and non-partial",
2929                                index
2930                            ),
2931                        };
2932                    }
2933                }
2934
2935                self.snapshot_relation(&alter.id);
2936                let action_type_id = match &alter.action {
2937                    AlterTableActionMutation::AddColumn { ty, .. } => ty
2938                        .as_deref()
2939                        .and_then(|raw| self.resolve_type_reference(raw)),
2940                    AlterTableActionMutation::SetType { ty, .. } => self.resolve_type_reference(ty),
2941                    _ => None,
2942                };
2943                let rel_overlay = self.local.relations.get_mut(&alter.id);
2944                if let Some(RelationOverlay::Present(rel)) = rel_overlay {
2945                    let generation = rel.generation;
2946                    match &alter.action {
2947                        AlterTableActionMutation::AddColumn {
2948                            name,
2949                            ty,
2950                            if_not_exists,
2951                            not_null,
2952                            default,
2953                            depends_on,
2954                            generation: _,
2955                        } => {
2956                            if let Some(existing_col) = rel.columns.iter().find(|c| c.name == *name)
2957                            {
2958                                if *if_not_exists {
2959                                    return MutationResult::Skipped;
2960                                }
2961                                return MutationResult::Conflict {
2962                                    reason: format!(
2963                                        "column '{}' already exists with type {}; this statement adds it again with type {}",
2964                                        name,
2965                                        existing_col.data_type.as_deref().unwrap_or("unknown"),
2966                                        ty.as_deref().unwrap_or("unknown")
2967                                    ),
2968                                };
2969                            }
2970                            rel.apply_column_action(&ColumnAction::Add {
2971                                name: name.clone(),
2972                                data_type: ty.clone(),
2973                                not_null: *not_null,
2974                                default: default.clone(),
2975                            });
2976                            if let Some(column) =
2977                                rel.columns.iter_mut().find(|column| column.name == *name)
2978                            {
2979                                column.type_id = action_type_id.clone();
2980                            }
2981
2982                            if let Some((sequence_id, column_name, _)) = &implicit_add
2983                                && column_name == name
2984                                && let Some(column) =
2985                                    rel.columns.iter_mut().find(|column| column.name == *name)
2986                            {
2987                                column.default = Some(Self::sequence_nextval_default(sequence_id));
2988                                column.default_expr_text = Some(format!(
2989                                    "nextval('{}.{}'::regclass)",
2990                                    sequence_id.schema, sequence_id.name
2991                                ));
2992                                column.is_nullable = false;
2993                            }
2994
2995                            if let Some((source_table, source_col)) = depends_on {
2996                                self.snapshot_graph();
2997                                self.local.graph.edges.push(DependencyEdge::new(
2998                                    alter.id.clone(),
2999                                    source_table.clone(),
3000                                    DependencyKind::ColumnGeneratedFrom {
3001                                        column: name.clone(),
3002                                        depends_on_column: source_col.clone(),
3003                                    },
3004                                ));
3005                            }
3006                        }
3007                        AlterTableActionMutation::DropColumn { name, if_exists } => {
3008                            if !rel.has_column(name) {
3009                                if *if_exists {
3010                                    // Column doesn't exist and IF EXISTS was specified: no-op
3011                                    return MutationResult::Skipped;
3012                                }
3013                                return MutationResult::Conflict {
3014                                    reason: format!(
3015                                        "column '{}' does not exist on relation '{}'",
3016                                        name, alter.id
3017                                    ),
3018                                };
3019                            }
3020                            rel.apply_column_action(&ColumnAction::Drop { name: name.clone() });
3021                        }
3022                        AlterTableActionMutation::RenameColumn { from, to } => {
3023                            rel.apply_column_action(&ColumnAction::Rename {
3024                                from: from.clone(),
3025                                to: to.clone(),
3026                            });
3027                        }
3028                        AlterTableActionMutation::SetNotNull { column } => {
3029                            rel.apply_column_action(&ColumnAction::SetNotNull {
3030                                name: column.clone(),
3031                            });
3032                        }
3033                        AlterTableActionMutation::DropNotNull { column } => {
3034                            rel.apply_column_action(&ColumnAction::DropNotNull {
3035                                name: column.clone(),
3036                            });
3037                        }
3038                        AlterTableActionMutation::SetType { column, ty, .. } => {
3039                            if !rel.has_column(column) {
3040                                self.local.confidence = Confidence::Tainted;
3041                            }
3042                            rel.apply_column_action(&ColumnAction::SetType {
3043                                name: column.clone(),
3044                                data_type: ty.clone(),
3045                            });
3046                            if let Some(column) =
3047                                rel.columns.iter_mut().find(|entry| entry.name == *column)
3048                            {
3049                                column.type_id = action_type_id.clone();
3050                            }
3051                        }
3052                        AlterTableActionMutation::SetDefault { column, default } => {
3053                            if !rel.has_column(column) {
3054                                self.local.confidence = Confidence::Tainted;
3055                            }
3056                            rel.apply_column_action(&ColumnAction::SetDefault {
3057                                name: column.clone(),
3058                                default: default.clone(),
3059                            });
3060                        }
3061                        AlterTableActionMutation::AddForeignKey {
3062                            constraint_name,
3063                            to_table,
3064                            from_columns,
3065                            to_columns,
3066                            not_valid,
3067                        } => {
3068                            let constraint_name = constraint_name.clone().unwrap_or_else(|| {
3069                                format!("{}_{}_fkey", alter.id.name, from_columns.join("_"))
3070                            });
3071                            self.snapshot_constraint(&alter.id, &constraint_name);
3072                            self.local.constraints.insert(
3073                                (alter.id.clone(), constraint_name.clone()),
3074                                ConstraintState {
3075                                    table_id: alter.id.clone(),
3076                                    name: constraint_name.clone(),
3077                                    kind: ConstraintKind::ForeignKey,
3078                                    validated: !not_valid,
3079                                },
3080                            );
3081                            self.snapshot_graph();
3082                            self.local.graph.edges.push(DependencyEdge::new(
3083                                alter.id.clone(),
3084                                to_table.clone(),
3085                                DependencyKind::ForeignKey {
3086                                    constraint_name: Some(constraint_name),
3087                                    from_columns: from_columns.clone(),
3088                                    to_columns: to_columns.clone(),
3089                                    from_generation: generation,
3090                                },
3091                            ));
3092                        }
3093                        AlterTableActionMutation::DropConstraint { name } => {
3094                            self.snapshot_constraint(&alter.id, name);
3095                            self.local
3096                                .constraints
3097                                .remove(&(alter.id.clone(), name.clone()));
3098                            self.snapshot_graph();
3099                            self.local.graph.edges.retain(|e| {
3100                                if let DependencyKind::ForeignKey {
3101                                    constraint_name, ..
3102                                } = &e.kind
3103                                {
3104                                    !(e.dependent == alter.id
3105                                        && constraint_name.as_ref() == Some(name))
3106                                } else {
3107                                    true
3108                                }
3109                            });
3110                        }
3111                        AlterTableActionMutation::RenameConstraint { old_name, new_name } => {
3112                            self.snapshot_constraint(&alter.id, old_name);
3113                            self.snapshot_constraint(&alter.id, new_name);
3114                            if let Some(mut constraint) = self
3115                                .local
3116                                .constraints
3117                                .remove(&(alter.id.clone(), old_name.clone()))
3118                            {
3119                                constraint.name = new_name.clone();
3120                                self.local
3121                                    .constraints
3122                                    .insert((alter.id.clone(), new_name.clone()), constraint);
3123                            }
3124                            self.snapshot_graph_full();
3125                            for edge in &mut self.local.graph.edges {
3126                                if edge.dependent == alter.id
3127                                    && let DependencyKind::ForeignKey {
3128                                        constraint_name, ..
3129                                    } = &mut edge.kind
3130                                    && constraint_name.as_deref() == Some(old_name)
3131                                {
3132                                    *constraint_name = Some(new_name.clone());
3133                                }
3134                            }
3135                        }
3136                        AlterTableActionMutation::AddCheckConstraint {
3137                            constraint_name,
3138                            not_valid,
3139                        } => {
3140                            let constraint_name = constraint_name
3141                                .clone()
3142                                .unwrap_or_else(|| format!("{}_check", alter.id.name));
3143                            self.snapshot_constraint(&alter.id, &constraint_name);
3144                            self.local.constraints.insert(
3145                                (alter.id.clone(), constraint_name.clone()),
3146                                ConstraintState {
3147                                    table_id: alter.id.clone(),
3148                                    name: constraint_name,
3149                                    kind: ConstraintKind::Check,
3150                                    validated: !not_valid,
3151                                },
3152                            );
3153                        }
3154                        AlterTableActionMutation::AddUniqueConstraint {
3155                            constraint_name,
3156                            using_index,
3157                        } => {
3158                            let constraint_name = constraint_name
3159                                .clone()
3160                                .or_else(|| using_index.as_ref().map(|index| index.name.clone()))
3161                                .unwrap_or_else(|| format!("{}_key", alter.id.name));
3162                            self.snapshot_constraint(&alter.id, &constraint_name);
3163                            self.local.constraints.insert(
3164                                (alter.id.clone(), constraint_name.clone()),
3165                                ConstraintState {
3166                                    table_id: alter.id.clone(),
3167                                    name: constraint_name,
3168                                    kind: ConstraintKind::Unique,
3169                                    validated: true,
3170                                },
3171                            );
3172                        }
3173                        AlterTableActionMutation::AddPrimaryKeyConstraint {
3174                            constraint_name,
3175                            using_index,
3176                        } => {
3177                            let constraint_name = constraint_name
3178                                .clone()
3179                                .or_else(|| using_index.as_ref().map(|index| index.name.clone()))
3180                                .unwrap_or_else(|| format!("{}_pkey", alter.id.name));
3181                            self.snapshot_constraint(&alter.id, &constraint_name);
3182                            self.local.constraints.insert(
3183                                (alter.id.clone(), constraint_name.clone()),
3184                                ConstraintState {
3185                                    table_id: alter.id.clone(),
3186                                    name: constraint_name,
3187                                    kind: ConstraintKind::PrimaryKey,
3188                                    validated: true,
3189                                },
3190                            );
3191                        }
3192                        AlterTableActionMutation::AddExcludeConstraint { constraint_name } => {
3193                            let constraint_name = constraint_name
3194                                .clone()
3195                                .unwrap_or_else(|| format!("{}_excl", alter.id.name));
3196                            self.snapshot_constraint(&alter.id, &constraint_name);
3197                            self.local.constraints.insert(
3198                                (alter.id.clone(), constraint_name.clone()),
3199                                ConstraintState {
3200                                    table_id: alter.id.clone(),
3201                                    name: constraint_name,
3202                                    kind: ConstraintKind::Exclusion,
3203                                    validated: true,
3204                                },
3205                            );
3206                        }
3207                        AlterTableActionMutation::ValidateConstraint { constraint_name } => {
3208                            self.snapshot_constraint(&alter.id, constraint_name);
3209                            if let Some(constraint) = self
3210                                .local
3211                                .constraints
3212                                .get_mut(&(alter.id.clone(), constraint_name.clone()))
3213                            {
3214                                constraint.validated = true;
3215                            }
3216                        }
3217                        AlterTableActionMutation::AttachPartition { child, .. } => {
3218                            // Reject attachments that would make partition ancestry cyclic.
3219                            if self.local.graph.check_partition_cycle(&alter.id, child) {
3220                                self.snapshot_confidence();
3221                                self.local.confidence = Confidence::Tainted;
3222                            } else {
3223                                self.snapshot_graph();
3224                                self.local.graph.edges.push(DependencyEdge::new(
3225                                    child.clone(),
3226                                    alter.id.clone(),
3227                                    DependencyKind::PartitionOf,
3228                                ));
3229                            }
3230                        }
3231                        AlterTableActionMutation::DetachPartition { child } => {
3232                            self.snapshot_graph();
3233                            self.local.graph.edges.retain(|e| {
3234                                !(matches!(e.kind, DependencyKind::PartitionOf)
3235                                    && e.dependent == *child
3236                                    && e.referenced == alter.id)
3237                            });
3238                        }
3239                        _ => {}
3240                    }
3241                }
3242                if let Some((sequence_id, column_name, kind)) = implicit_add {
3243                    self.snapshot_sequence(&sequence_id);
3244                    self.snapshot_generation_counter();
3245                    self.local.generation_counter += 1;
3246                    self.local.sequences.insert(
3247                        sequence_id.clone(),
3248                        SequenceOverlay::Present(SequenceState {
3249                            id: sequence_id.clone(),
3250                            owner: self
3251                                .local
3252                                .relations
3253                                .get(&alter.id)
3254                                .and_then(|overlay| match overlay {
3255                                    RelationOverlay::Present(table) => Some(table.owner.clone()),
3256                                    RelationOverlay::Dropped => None,
3257                                })
3258                                .unwrap_or_else(|| ObjectId::new("", &self.local.current_role)),
3259                            owned_by: Some((alter.id.clone(), column_name.clone())),
3260                            kind,
3261                            generation: self.local.generation_counter,
3262                        }),
3263                    );
3264                    self.snapshot_graph();
3265                    self.local.graph.edges.push(DependencyEdge::new(
3266                        sequence_id,
3267                        alter.id.clone(),
3268                        DependencyKind::SequenceOwnedBy {
3269                            column: column_name,
3270                        },
3271                    ));
3272                }
3273                match &alter.action {
3274                    AlterTableActionMutation::DropColumn { .. } => {
3275                        for sequence_id in owned_sequences_for_column {
3276                            self.snapshot_sequence(&sequence_id);
3277                            self.local
3278                                .sequences
3279                                .insert(sequence_id.clone(), SequenceOverlay::Dropped);
3280                            self.snapshot_graph_full();
3281                            self.local.graph.edges.retain(|edge| {
3282                                !(matches!(edge.kind, DependencyKind::SequenceOwnedBy { .. })
3283                                    && edge.dependent == sequence_id)
3284                            });
3285                        }
3286                    }
3287                    AlterTableActionMutation::RenameColumn { to, .. } => {
3288                        for sequence_id in owned_sequences_for_column {
3289                            self.snapshot_sequence(&sequence_id);
3290                            if let Some(SequenceOverlay::Present(sequence)) =
3291                                self.local.sequences.get_mut(&sequence_id)
3292                                && let Some((_, column)) = &mut sequence.owned_by
3293                            {
3294                                *column = to.clone();
3295                            }
3296                            self.snapshot_graph_full();
3297                            for edge in &mut self.local.graph.edges {
3298                                if edge.dependent == sequence_id
3299                                    && let DependencyKind::SequenceOwnedBy { column } =
3300                                        &mut edge.kind
3301                                {
3302                                    *column = to.clone();
3303                                }
3304                            }
3305                        }
3306                    }
3307                    _ => {}
3308                }
3309                MutationResult::Applied
3310            }
3311            Mutation::CreateType(create_type) => {
3312                if self.relation_namespace_is_taken(&create_type.id) {
3313                    return MutationResult::Conflict {
3314                        reason: format!("type '{}' already exists", create_type.id),
3315                    };
3316                }
3317                self.snapshot_type(&create_type.id);
3318                self.snapshot_generation_counter();
3319                self.local.generation_counter += 1;
3320                let generation = self.local.generation_counter;
3321
3322                self.local.types.insert(
3323                    create_type.id.clone(),
3324                    TypeOverlay::Present(TypeState {
3325                        id: create_type.id.clone(),
3326                        generation,
3327                        kind: create_type.kind.clone(),
3328                    }),
3329                );
3330                MutationResult::Applied
3331            }
3332            Mutation::RenameType(rename) => {
3333                if !self.type_is_present(&rename.old_id) {
3334                    if self.baseline_covers_object(&rename.old_id) {
3335                        return MutationResult::Conflict {
3336                            reason: format!("type '{}' does not exist", rename.old_id),
3337                        };
3338                    }
3339                    self.snapshot_confidence();
3340                    self.local.confidence = Confidence::Tainted;
3341                    return MutationResult::Skipped;
3342                }
3343                if rename.old_id != rename.new_id
3344                    && self.relation_namespace_is_taken(&rename.new_id)
3345                {
3346                    return MutationResult::Conflict {
3347                        reason: format!("type '{}' already exists", rename.new_id),
3348                    };
3349                }
3350                if rename.old_id.schema != rename.new_id.schema
3351                    && !self.schema_is_present(&rename.new_id.schema)
3352                {
3353                    if self.schema_absence_is_authoritative(&rename.new_id.schema) {
3354                        return MutationResult::Conflict {
3355                            reason: format!("schema '{}' does not exist", rename.new_id.schema),
3356                        };
3357                    }
3358                    self.snapshot_confidence();
3359                    self.local.confidence = Confidence::Tainted;
3360                    return MutationResult::Skipped;
3361                }
3362
3363                let mut remapped_functions = Vec::new();
3364                for (function_id, overlay) in &self.local.functions {
3365                    let crate::model::function::FunctionOverlay::Present(function) = overlay else {
3366                        continue;
3367                    };
3368                    let new_arg_types = function
3369                        .arg_types
3370                        .iter()
3371                        .enumerate()
3372                        .map(|(index, raw)| {
3373                            if function.arg_type_ids.get(index)
3374                                == Some(&Some(rename.old_id.clone()))
3375                            {
3376                                Self::remapped_type_display(
3377                                    raw,
3378                                    &rename.new_id,
3379                                    rename.old_id.schema != rename.new_id.schema,
3380                                )
3381                            } else {
3382                                raw.clone()
3383                            }
3384                        })
3385                        .collect::<Vec<_>>();
3386                    let new_return_type = if function.return_type_id == Some(rename.old_id.clone())
3387                    {
3388                        Self::remapped_type_display(
3389                            &function.return_type,
3390                            &rename.new_id,
3391                            rename.old_id.schema != rename.new_id.schema,
3392                        )
3393                    } else {
3394                        function.return_type.clone()
3395                    };
3396                    let base_name = function_id
3397                        .name
3398                        .split_once('(')
3399                        .map(|(name, _)| name)
3400                        .unwrap_or(&function_id.name);
3401                    let mut new_function_id = ObjectId::new(
3402                        &function_id.schema,
3403                        format!("{}({})", base_name, new_arg_types.join(",")),
3404                    );
3405                    new_function_id.inferred_schema = function_id.inferred_schema;
3406                    if new_function_id != *function_id
3407                        || new_arg_types != function.arg_types
3408                        || new_return_type != function.return_type
3409                    {
3410                        remapped_functions.push((
3411                            function_id.clone(),
3412                            new_function_id,
3413                            new_arg_types,
3414                            new_return_type,
3415                        ));
3416                    }
3417                }
3418                let moved_function_ids = remapped_functions
3419                    .iter()
3420                    .map(|(old_id, _, _, _)| old_id)
3421                    .collect::<HashSet<_>>();
3422                let mut destinations = HashSet::new();
3423                for (_, new_id, _, _) in &remapped_functions {
3424                    if !destinations.insert(new_id)
3425                        || (self.local.functions.contains_key(new_id)
3426                            && !moved_function_ids.contains(new_id))
3427                    {
3428                        return MutationResult::Conflict {
3429                            reason: format!(
3430                                "routine '{}' already exists after renaming type '{}'",
3431                                new_id, rename.old_id
3432                            ),
3433                        };
3434                    }
3435                }
3436
3437                self.snapshot_namespace();
3438                if let Some(TypeOverlay::Present(mut state)) =
3439                    self.local.types.remove(&rename.old_id)
3440                {
3441                    state.id = rename.new_id.clone();
3442                    self.local
3443                        .types
3444                        .insert(rename.new_id.clone(), TypeOverlay::Present(state));
3445                }
3446                for overlay in self.local.relations.values_mut() {
3447                    if let RelationOverlay::Present(relation) = overlay {
3448                        for column in &mut relation.columns {
3449                            if column.type_id == Some(rename.old_id.clone()) {
3450                                column.data_type = Some(Self::remapped_type_display(
3451                                    column.data_type.as_deref().unwrap_or_default(),
3452                                    &rename.new_id,
3453                                    rename.old_id.schema != rename.new_id.schema,
3454                                ));
3455                                column.type_id = Some(rename.new_id.clone());
3456                            }
3457                        }
3458                    }
3459                }
3460                for overlay in self.local.types.values_mut() {
3461                    if let TypeOverlay::Present(TypeState {
3462                        kind:
3463                            TypeKind::Domain {
3464                                base_type,
3465                                base_type_id,
3466                            },
3467                        ..
3468                    }) = overlay
3469                        && *base_type_id == Some(rename.old_id.clone())
3470                    {
3471                        *base_type = Self::remapped_type_display(
3472                            base_type,
3473                            &rename.new_id,
3474                            rename.old_id.schema != rename.new_id.schema,
3475                        );
3476                        *base_type_id = Some(rename.new_id.clone());
3477                    }
3478                }
3479                for (old_id, new_id, arg_types, return_type) in remapped_functions {
3480                    if let Some(crate::model::function::FunctionOverlay::Present(mut function)) =
3481                        self.local.functions.remove(&old_id)
3482                    {
3483                        function.id = new_id.clone();
3484                        function.arg_types = arg_types;
3485                        for type_id in &mut function.arg_type_ids {
3486                            if *type_id == Some(rename.old_id.clone()) {
3487                                *type_id = Some(rename.new_id.clone());
3488                            }
3489                        }
3490                        function.return_type = return_type;
3491                        if function.return_type_id == Some(rename.old_id.clone()) {
3492                            function.return_type_id = Some(rename.new_id.clone());
3493                        }
3494                        self.local.functions.insert(
3495                            new_id.clone(),
3496                            crate::model::function::FunctionOverlay::Present(function),
3497                        );
3498                        if old_id != new_id {
3499                            self.local.graph.propagate_rename(&old_id, &new_id);
3500                            self.local.graph.edges.push(DependencyEdge::new(
3501                                old_id,
3502                                new_id,
3503                                DependencyKind::RenameTo,
3504                            ));
3505                        }
3506                    }
3507                }
3508                MutationResult::Applied
3509            }
3510            Mutation::AlterType(alter_type) => {
3511                self.snapshot_type(&alter_type.id);
3512                if let Some(TypeOverlay::Present(t)) = self.local.types.get_mut(&alter_type.id) {
3513                    match &alter_type.action {
3514                        AlterTypeActionMutation::AddValue {
3515                            new_value,
3516                            neighbor,
3517                            before,
3518                        } => {
3519                            if let TypeKind::Enum { variants } = &mut t.kind {
3520                                if variants.contains(new_value) {
3521                                    return MutationResult::Skipped;
3522                                }
3523                                let insertion_index = neighbor
3524                                    .as_ref()
3525                                    .and_then(|neighbor| {
3526                                        variants.iter().position(|value| value == neighbor)
3527                                    })
3528                                    .map(|index| if *before { index } else { index + 1 })
3529                                    .unwrap_or(variants.len());
3530                                variants.insert(insertion_index, new_value.clone());
3531                            }
3532                        }
3533                        AlterTypeActionMutation::RenameValue {
3534                            old_value,
3535                            new_value,
3536                        } => {
3537                            let TypeKind::Enum { variants } = &mut t.kind else {
3538                                return MutationResult::Conflict {
3539                                    reason: format!("type '{}' is not an enum", alter_type.id),
3540                                };
3541                            };
3542                            let Some(old_index) =
3543                                variants.iter().position(|value| value == old_value)
3544                            else {
3545                                return MutationResult::Conflict {
3546                                    reason: format!(
3547                                        "'{}' is not an existing label of enum '{}'",
3548                                        old_value, alter_type.id
3549                                    ),
3550                                };
3551                            };
3552                            if variants.iter().any(|value| value == new_value) {
3553                                return MutationResult::Conflict {
3554                                    reason: format!(
3555                                        "enum label '{}' already exists on type '{}'",
3556                                        new_value, alter_type.id
3557                                    ),
3558                                };
3559                            }
3560                            variants[old_index] = new_value.clone();
3561                        }
3562                    }
3563                } else if matches!(
3564                    alter_type.action,
3565                    AlterTypeActionMutation::RenameValue { .. }
3566                ) {
3567                    return MutationResult::Conflict {
3568                        reason: format!("type '{}' does not exist", alter_type.id),
3569                    };
3570                }
3571                MutationResult::Applied
3572            }
3573            Mutation::CreateDomain(create_domain) => {
3574                if self.relation_namespace_is_taken(&create_domain.id) {
3575                    return MutationResult::Conflict {
3576                        reason: format!("type '{}' already exists", create_domain.id),
3577                    };
3578                }
3579                self.snapshot_type(&create_domain.id);
3580                self.snapshot_generation_counter();
3581                self.local.generation_counter += 1;
3582                let generation = self.local.generation_counter;
3583
3584                self.local.types.insert(
3585                    create_domain.id.clone(),
3586                    TypeOverlay::Present(TypeState {
3587                        id: create_domain.id.clone(),
3588                        generation,
3589                        kind: TypeKind::Domain {
3590                            base_type: create_domain.base_type.clone(),
3591                            base_type_id: self.resolve_type_reference(&create_domain.base_type),
3592                        },
3593                    }),
3594                );
3595                MutationResult::Applied
3596            }
3597            Mutation::AlterDomain(_) => MutationResult::Applied,
3598            Mutation::DropDomain(drop_domain) => {
3599                for id in &drop_domain.ids {
3600                    self.snapshot_type(id);
3601                    self.local.types.insert(id.clone(), TypeOverlay::Dropped);
3602                }
3603                MutationResult::Applied
3604            }
3605            Mutation::DropType(drop_type) => {
3606                for id in &drop_type.ids {
3607                    self.snapshot_type(id);
3608                    self.local.types.insert(id.clone(), TypeOverlay::Dropped);
3609                }
3610                MutationResult::Applied
3611            }
3612            Mutation::CreateSequence(create_seq) => {
3613                if create_seq.if_not_exists && self.relation_namespace_is_taken(&create_seq.id) {
3614                    return MutationResult::Skipped;
3615                }
3616                if self.relation_namespace_is_taken(&create_seq.id) {
3617                    return MutationResult::Conflict {
3618                        reason: format!("relation '{}' already exists", create_seq.id),
3619                    };
3620                }
3621                if let Some((table_id, column)) = &create_seq.owned_by {
3622                    if table_id.schema != create_seq.id.schema {
3623                        return MutationResult::Conflict {
3624                            reason: "sequence must be in the same schema as its owning table"
3625                                .to_string(),
3626                        };
3627                    }
3628                    match self.local.relations.get(table_id) {
3629                        Some(RelationOverlay::Present(table)) => {
3630                            if !table.has_column(column) {
3631                                return MutationResult::Conflict {
3632                                    reason: format!(
3633                                        "column '{}.{}' does not exist",
3634                                        table_id, column
3635                                    ),
3636                                };
3637                            }
3638                            if self.local.current_role_known
3639                                && table.owner.name != self.local.current_role
3640                            {
3641                                return MutationResult::Conflict {
3642                                    reason: "sequence and table must have the same owner"
3643                                        .to_string(),
3644                                };
3645                            }
3646                        }
3647                        _ if self.baseline_covers_object(table_id) && self.baseline_available => {
3648                            return MutationResult::Conflict {
3649                                reason: format!("relation '{}' does not exist", table_id),
3650                            };
3651                        }
3652                        _ => {
3653                            self.snapshot_confidence();
3654                            self.local.confidence = Confidence::Tainted;
3655                        }
3656                    }
3657                }
3658                self.snapshot_sequence(&create_seq.id);
3659                self.snapshot_generation_counter();
3660                self.local.generation_counter += 1;
3661                let generation = self.local.generation_counter;
3662
3663                self.local.sequences.insert(
3664                    create_seq.id.clone(),
3665                    SequenceOverlay::Present(SequenceState {
3666                        id: create_seq.id.clone(),
3667                        owner: ObjectId::new("", self.local.current_role.clone()),
3668                        owned_by: create_seq.owned_by.clone(),
3669                        kind: if create_seq.owned_by.is_some() {
3670                            SequenceKind::Owned
3671                        } else {
3672                            SequenceKind::Standalone
3673                        },
3674                        generation,
3675                    }),
3676                );
3677
3678                if let Some((table_id, col)) = &create_seq.owned_by {
3679                    self.snapshot_graph();
3680                    self.local.graph.edges.push(DependencyEdge::new(
3681                        create_seq.id.clone(),
3682                        table_id.clone(),
3683                        DependencyKind::SequenceOwnedBy {
3684                            column: col.clone(),
3685                        },
3686                    ));
3687                }
3688                MutationResult::Applied
3689            }
3690            Mutation::AlterSequence(alter_seq) => {
3691                if !self.sequence_is_present(&alter_seq.id) {
3692                    if alter_seq.if_exists {
3693                        return MutationResult::Skipped;
3694                    }
3695                    if self.baseline_covers_object(&alter_seq.id) && self.baseline_available {
3696                        return MutationResult::Conflict {
3697                            reason: format!("sequence '{}' does not exist", alter_seq.id),
3698                        };
3699                    }
3700                    self.snapshot_confidence();
3701                    self.local.confidence = Confidence::Tainted;
3702                    return MutationResult::Skipped;
3703                }
3704                let current = match self.local.sequences.get(&alter_seq.id) {
3705                    Some(SequenceOverlay::Present(sequence)) => sequence.clone(),
3706                    _ => unreachable!("presence checked above"),
3707                };
3708                match &alter_seq.action {
3709                    crate::analysis::mutations::AlterSequenceActionMutation::OwnedBy(owned_by) => {
3710                        if current.kind == SequenceKind::Identity {
3711                            return MutationResult::Conflict {
3712                                reason: "cannot change ownership of an identity sequence"
3713                                    .to_string(),
3714                            };
3715                        }
3716                        if let Some((table_id, column)) = owned_by {
3717                            if table_id.schema != alter_seq.id.schema {
3718                                return MutationResult::Conflict {
3719                                    reason:
3720                                        "sequence must be in the same schema as its owning table"
3721                                            .to_string(),
3722                                };
3723                            }
3724                            let Some(RelationOverlay::Present(table)) =
3725                                self.local.relations.get(table_id)
3726                            else {
3727                                return MutationResult::Conflict {
3728                                    reason: format!("relation '{}' does not exist", table_id),
3729                                };
3730                            };
3731                            if !table.has_column(column) {
3732                                return MutationResult::Conflict {
3733                                    reason: format!(
3734                                        "column '{}.{}' does not exist",
3735                                        table_id, column
3736                                    ),
3737                                };
3738                            }
3739                            if table.owner != current.owner {
3740                                return MutationResult::Conflict {
3741                                    reason: "sequence and table must have the same owner"
3742                                        .to_string(),
3743                                };
3744                            }
3745                        }
3746                        self.snapshot_sequence(&alter_seq.id);
3747                        self.snapshot_graph();
3748                        self.local.graph.edges.retain(|edge| {
3749                            !(matches!(edge.kind, DependencyKind::SequenceOwnedBy { .. })
3750                                && edge.dependent == alter_seq.id)
3751                        });
3752                        if let Some(SequenceOverlay::Present(sequence)) =
3753                            self.local.sequences.get_mut(&alter_seq.id)
3754                        {
3755                            sequence.owned_by = owned_by.clone();
3756                            sequence.kind = if owned_by.is_some() {
3757                                SequenceKind::Owned
3758                            } else {
3759                                SequenceKind::Standalone
3760                            };
3761                        }
3762                        if let Some((table_id, column)) = owned_by {
3763                            self.local.graph.edges.push(DependencyEdge::new(
3764                                alter_seq.id.clone(),
3765                                table_id.clone(),
3766                                DependencyKind::SequenceOwnedBy {
3767                                    column: column.clone(),
3768                                },
3769                            ));
3770                        }
3771                        MutationResult::Applied
3772                    }
3773                    crate::analysis::mutations::AlterSequenceActionMutation::OwnerTo(owner) => {
3774                        if current.kind == SequenceKind::Identity {
3775                            return MutationResult::Conflict {
3776                                reason: "cannot alter an identity sequence independently"
3777                                    .to_string(),
3778                            };
3779                        }
3780                        let Some((owner_name, known)) = self.role_fact_identity(owner) else {
3781                            self.snapshot_confidence();
3782                            self.local.confidence = Confidence::Tainted;
3783                            return MutationResult::Skipped;
3784                        };
3785                        if known
3786                            && self.local.roles_known
3787                            && self.present_role(&owner_name).is_none()
3788                        {
3789                            return MutationResult::Conflict {
3790                                reason: format!("role '{}' does not exist", owner_name),
3791                            };
3792                        }
3793                        if let Some((table_id, _)) = &current.owned_by
3794                            && let Some(RelationOverlay::Present(table)) =
3795                                self.local.relations.get(table_id)
3796                            && table.owner.name != owner_name
3797                        {
3798                            return MutationResult::Conflict {
3799                                reason: "sequence and table must have the same owner".to_string(),
3800                            };
3801                        }
3802                        self.snapshot_sequence(&alter_seq.id);
3803                        if let Some(SequenceOverlay::Present(sequence)) =
3804                            self.local.sequences.get_mut(&alter_seq.id)
3805                        {
3806                            sequence.owner = ObjectId::new("", owner_name);
3807                        }
3808                        MutationResult::Applied
3809                    }
3810                    crate::analysis::mutations::AlterSequenceActionMutation::RenameTo(new_id)
3811                    | crate::analysis::mutations::AlterSequenceActionMutation::SetSchema(new_id) => {
3812                        if current.kind == SequenceKind::Identity {
3813                            return MutationResult::Conflict {
3814                                reason: "cannot alter an identity sequence independently"
3815                                    .to_string(),
3816                            };
3817                        }
3818                        if self.relation_namespace_is_taken(new_id) {
3819                            return MutationResult::Conflict {
3820                                reason: format!("relation '{}' already exists", new_id),
3821                            };
3822                        }
3823                        if let Some((table_id, _)) = &current.owned_by
3824                            && table_id.schema != new_id.schema
3825                        {
3826                            return MutationResult::Conflict {
3827                                reason: "sequence must be in the same schema as its owning table"
3828                                    .to_string(),
3829                            };
3830                        }
3831                        self.snapshot_namespace();
3832                        let mut moved = current;
3833                        moved.id = new_id.clone();
3834                        self.local.sequences.remove(&alter_seq.id);
3835                        self.local
3836                            .sequences
3837                            .insert(new_id.clone(), SequenceOverlay::Present(moved));
3838                        self.local.graph.propagate_rename(&alter_seq.id, new_id);
3839                        self.local.graph.edges.push(DependencyEdge::new(
3840                            alter_seq.id.clone(),
3841                            new_id.clone(),
3842                            DependencyKind::RenameTo,
3843                        ));
3844                        if self.baseline_sequences.remove(&alter_seq.id) {
3845                            self.baseline_sequences.insert(new_id.clone());
3846                        }
3847                        MutationResult::Applied
3848                    }
3849                    crate::analysis::mutations::AlterSequenceActionMutation::Other => {
3850                        MutationResult::Applied
3851                    }
3852                }
3853            }
3854            Mutation::DropSequence(drop_seq) => {
3855                if !drop_seq.if_exists {
3856                    let missing: Vec<ObjectId> = drop_seq
3857                        .ids
3858                        .iter()
3859                        .filter(|id| !self.sequence_is_present(id))
3860                        .cloned()
3861                        .collect();
3862                    for id in &missing {
3863                        if self.baseline_covers_object(id) && self.baseline_available {
3864                            return MutationResult::Conflict {
3865                                reason: format!("sequence '{}' does not exist", id),
3866                            };
3867                        }
3868                        self.snapshot_confidence();
3869                        self.local.confidence = Confidence::Tainted;
3870                    }
3871                }
3872                let present: Vec<ObjectId> = drop_seq
3873                    .ids
3874                    .iter()
3875                    .filter(|id| self.sequence_is_present(id))
3876                    .cloned()
3877                    .collect();
3878                if present.is_empty() {
3879                    return MutationResult::Skipped;
3880                }
3881                for id in &present {
3882                    let Some(SequenceOverlay::Present(sequence)) = self.local.sequences.get(id)
3883                    else {
3884                        continue;
3885                    };
3886                    if sequence.kind == SequenceKind::Identity {
3887                        return MutationResult::Conflict {
3888                            reason: format!("cannot drop identity sequence '{}' independently", id),
3889                        };
3890                    }
3891                    if sequence.kind == SequenceKind::SerialLike && !drop_seq.cascade {
3892                        return MutationResult::Conflict {
3893                            reason: format!("sequence '{}' still has dependent defaults", id),
3894                        };
3895                    }
3896                }
3897                if drop_seq.cascade {
3898                    let serial_owners: Vec<(ObjectId, String)> = present
3899                        .iter()
3900                        .filter_map(|id| match self.local.sequences.get(id) {
3901                            Some(SequenceOverlay::Present(sequence))
3902                                if sequence.kind == SequenceKind::SerialLike =>
3903                            {
3904                                sequence.owned_by.clone()
3905                            }
3906                            _ => None,
3907                        })
3908                        .collect();
3909                    for (table_id, column) in serial_owners {
3910                        self.snapshot_relation(&table_id);
3911                        if let Some(RelationOverlay::Present(table)) =
3912                            self.local.relations.get_mut(&table_id)
3913                            && let Some(column) =
3914                                table.columns.iter_mut().find(|item| item.name == column)
3915                        {
3916                            column.default = None;
3917                            column.default_expr_text = None;
3918                        }
3919                    }
3920                }
3921                for id in &present {
3922                    self.snapshot_sequence(id);
3923                    self.local
3924                        .sequences
3925                        .insert(id.clone(), SequenceOverlay::Dropped);
3926                }
3927                self.snapshot_graph_full();
3928                self.local.graph.edges.retain(|e| {
3929                    !(matches!(e.kind, DependencyKind::SequenceOwnedBy { .. })
3930                        && present.contains(&e.dependent))
3931                });
3932                MutationResult::Applied
3933            }
3934            Mutation::Rename(rename) => {
3935                let renames_relation = self.relation_is_present(&rename.old_id);
3936                let renames_index = self.index_is_present(&rename.old_id);
3937                if !renames_relation && !renames_index {
3938                    if self.baseline_covers_object(&rename.old_id) {
3939                        return MutationResult::Conflict {
3940                            reason: format!("relation '{}' does not exist", rename.old_id),
3941                        };
3942                    }
3943                    self.snapshot_confidence();
3944                    self.local.confidence = Confidence::Tainted;
3945                    return MutationResult::Skipped;
3946                }
3947                if rename.old_id != rename.new_id
3948                    && self.relation_namespace_is_taken(&rename.new_id)
3949                {
3950                    return MutationResult::Conflict {
3951                        reason: format!("relation '{}' already exists", rename.new_id),
3952                    };
3953                }
3954                if rename.old_id.schema != rename.new_id.schema
3955                    && !self.schema_is_present(&rename.new_id.schema)
3956                {
3957                    if self.schema_absence_is_authoritative(&rename.new_id.schema) {
3958                        return MutationResult::Conflict {
3959                            reason: format!("schema '{}' does not exist", rename.new_id.schema),
3960                        };
3961                    }
3962                    self.snapshot_confidence();
3963                    self.local.confidence = Confidence::Tainted;
3964                    return MutationResult::Skipped;
3965                }
3966
3967                self.snapshot_namespace();
3968                if let Some(RelationOverlay::Present(mut state)) =
3969                    self.local.relations.remove(&rename.old_id)
3970                {
3971                    state.id = rename.new_id.clone();
3972                    self.local
3973                        .relations
3974                        .insert(rename.new_id.clone(), RelationOverlay::Present(state));
3975                }
3976                let owned_sequence_ids: Vec<ObjectId> = self
3977                    .local
3978                    .sequences
3979                    .iter()
3980                    .filter_map(|(id, overlay)| match overlay {
3981                        SequenceOverlay::Present(sequence)
3982                            if sequence
3983                                .owned_by
3984                                .as_ref()
3985                                .is_some_and(|(table, _)| table == &rename.old_id) =>
3986                        {
3987                            Some(id.clone())
3988                        }
3989                        _ => None,
3990                    })
3991                    .collect();
3992                for sequence_id in owned_sequence_ids {
3993                    self.snapshot_sequence(&sequence_id);
3994                    if let Some(SequenceOverlay::Present(sequence)) =
3995                        self.local.sequences.get_mut(&sequence_id)
3996                        && let Some((table, _)) = &mut sequence.owned_by
3997                    {
3998                        *table = rename.new_id.clone();
3999                    }
4000                }
4001                let triggers_to_move: Vec<(ObjectId, crate::model::trigger::TriggerState)> = self
4002                    .local
4003                    .triggers
4004                    .iter()
4005                    .filter_map(|(id, overlay)| match overlay {
4006                        TriggerOverlay::Present(trigger) if trigger.table_id == rename.old_id => {
4007                            Some((id.clone(), trigger.clone()))
4008                        }
4009                        _ => None,
4010                    })
4011                    .collect();
4012                for (old_trigger_id, mut trigger) in triggers_to_move {
4013                    let new_trigger_id = Self::trigger_key(&rename.new_id, &trigger.name);
4014                    self.local.triggers.remove(&old_trigger_id);
4015                    trigger.id = new_trigger_id.clone();
4016                    trigger.table_id = rename.new_id.clone();
4017                    self.local
4018                        .triggers
4019                        .insert(new_trigger_id.clone(), TriggerOverlay::Present(trigger));
4020                    self.local
4021                        .graph
4022                        .propagate_rename(&old_trigger_id, &new_trigger_id);
4023                    self.local.graph.edges.push(DependencyEdge::new(
4024                        old_trigger_id,
4025                        new_trigger_id,
4026                        DependencyKind::RenameTo,
4027                    ));
4028                }
4029                let constraints_to_move: Vec<(String, ConstraintState)> = self
4030                    .local
4031                    .constraints
4032                    .iter()
4033                    .filter(|((table_id, _), _)| table_id == &rename.old_id)
4034                    .map(|((_, name), constraint)| (name.clone(), constraint.clone()))
4035                    .collect();
4036                for (name, mut constraint) in constraints_to_move {
4037                    self.snapshot_constraint(&rename.old_id, &name);
4038                    self.snapshot_constraint(&rename.new_id, &name);
4039                    self.local
4040                        .constraints
4041                        .remove(&(rename.old_id.clone(), name.clone()));
4042                    constraint.table_id = rename.new_id.clone();
4043                    self.local
4044                        .constraints
4045                        .insert((rename.new_id.clone(), name), constraint);
4046                }
4047                self.local.pending_validation = std::mem::take(&mut self.local.pending_validation)
4048                    .into_iter()
4049                    .map(|(table, name)| {
4050                        if table == rename.old_id {
4051                            (rename.new_id.clone(), name)
4052                        } else {
4053                            (table, name)
4054                        }
4055                    })
4056                    .collect();
4057                self.local.graph.edges.push(DependencyEdge::new(
4058                    rename.old_id.clone(),
4059                    rename.new_id.clone(),
4060                    DependencyKind::RenameTo,
4061                ));
4062                self.local
4063                    .graph
4064                    .propagate_rename(&rename.old_id, &rename.new_id);
4065
4066                if renames_relation {
4067                    if self.baseline_relations.remove(&rename.old_id) {
4068                        self.baseline_relations.insert(rename.new_id.clone());
4069                    }
4070                    if self.baseline_fk_dependencies.remove(&rename.old_id) {
4071                        self.baseline_fk_dependencies.insert(rename.new_id.clone());
4072                    }
4073                    self.baseline_foreign_keys = std::mem::take(&mut self.baseline_foreign_keys)
4074                        .into_iter()
4075                        .map(|(table, name)| {
4076                            if table == rename.old_id {
4077                                (rename.new_id.clone(), name)
4078                            } else {
4079                                (table, name)
4080                            }
4081                        })
4082                        .collect();
4083                }
4084                if renames_index && self.baseline_indexes.remove(&rename.old_id) {
4085                    self.baseline_indexes.insert(rename.new_id.clone());
4086                }
4087
4088                MutationResult::Applied
4089            }
4090            Mutation::DropView(drop_view) => {
4091                let mut present = Vec::new();
4092                for id in &drop_view.ids {
4093                    match self.local.relations.get(id) {
4094                        Some(RelationOverlay::Present(relation))
4095                            if relation.kind == RelationKind::View =>
4096                        {
4097                            present.push(id.clone());
4098                        }
4099                        Some(RelationOverlay::Present(_)) => {
4100                            return MutationResult::Conflict {
4101                                reason: format!("'{}' is not a view", id),
4102                            };
4103                        }
4104                        _ if drop_view.if_exists => {}
4105                        _ if self.baseline_available && self.baseline_covers_object(id) => {
4106                            return MutationResult::Conflict {
4107                                reason: format!("view '{}' does not exist", id),
4108                            };
4109                        }
4110                        _ => {
4111                            self.snapshot_confidence();
4112                            self.local.confidence = Confidence::Tainted;
4113                            continue;
4114                        }
4115                    }
4116                }
4117                if present.is_empty() {
4118                    return MutationResult::Skipped;
4119                }
4120                for id in &present {
4121                    self.snapshot_relation(id);
4122                    self.local
4123                        .relations
4124                        .insert(id.clone(), RelationOverlay::Dropped);
4125                }
4126                self.snapshot_graph_full();
4127                self.local.graph.edges.retain(|e| {
4128                    !(matches!(e.kind, DependencyKind::ViewDependency { .. })
4129                        && present.contains(&e.dependent))
4130                });
4131                MutationResult::Applied
4132            }
4133            Mutation::DropMaterializedView(drop_mv) => {
4134                let mut present = Vec::new();
4135                for id in &drop_mv.ids {
4136                    match self.local.relations.get(id) {
4137                        Some(RelationOverlay::Present(relation))
4138                            if relation.kind == RelationKind::MaterializedView =>
4139                        {
4140                            present.push(id.clone());
4141                        }
4142                        Some(RelationOverlay::Present(_)) => {
4143                            return MutationResult::Conflict {
4144                                reason: format!("'{}' is not a materialized view", id),
4145                            };
4146                        }
4147                        _ if drop_mv.if_exists => {}
4148                        _ if self.baseline_available && self.baseline_covers_object(id) => {
4149                            return MutationResult::Conflict {
4150                                reason: format!("materialized view '{}' does not exist", id),
4151                            };
4152                        }
4153                        _ => {
4154                            self.snapshot_confidence();
4155                            self.local.confidence = Confidence::Tainted;
4156                            continue;
4157                        }
4158                    }
4159                }
4160                if present.is_empty() {
4161                    return MutationResult::Skipped;
4162                }
4163                for id in &present {
4164                    self.snapshot_relation(id);
4165                    self.local
4166                        .relations
4167                        .insert(id.clone(), RelationOverlay::Dropped);
4168                }
4169                self.snapshot_graph_full();
4170                self.local.graph.edges.retain(|e| {
4171                    !((matches!(e.kind, DependencyKind::ViewDependency { .. })
4172                        && present.contains(&e.dependent))
4173                        || (matches!(e.kind, DependencyKind::IndexOnRelation { .. })
4174                            && present.contains(&e.referenced)))
4175                });
4176                MutationResult::Applied
4177            }
4178            Mutation::DropIndex(drop_idx) => {
4179                let present = self.local.graph.edges.iter().any(|edge| {
4180                    matches!(edge.kind, DependencyKind::IndexOnRelation { .. })
4181                        && edge.dependent == drop_idx.id
4182                });
4183                if !present {
4184                    if drop_idx.if_exists {
4185                        return MutationResult::Skipped;
4186                    }
4187                    if self.baseline_available && self.baseline_covers_object(&drop_idx.id) {
4188                        return MutationResult::Conflict {
4189                            reason: format!("index '{}' does not exist", drop_idx.id),
4190                        };
4191                    }
4192                    self.snapshot_confidence();
4193                    self.local.confidence = Confidence::Tainted;
4194                    return MutationResult::Skipped;
4195                }
4196                self.snapshot_graph();
4197                self.local.graph.edges.retain(|e| {
4198                    !(matches!(e.kind, DependencyKind::IndexOnRelation { .. })
4199                        && e.dependent == drop_idx.id)
4200                });
4201                MutationResult::Applied
4202            }
4203            Mutation::ChangeRelationOwner { id, new_owner } => {
4204                let Some((owner, known)) = self.role_fact_identity(new_owner) else {
4205                    self.snapshot_confidence();
4206                    self.local.confidence = Confidence::Tainted;
4207                    return MutationResult::Skipped;
4208                };
4209                if !known {
4210                    self.snapshot_confidence();
4211                    self.local.confidence = Confidence::Tainted;
4212                }
4213                self.snapshot_relation(id);
4214                if let Some(RelationOverlay::Present(relation)) = self.local.relations.get_mut(id) {
4215                    relation.owner = ObjectId::new("", owner);
4216                    MutationResult::Applied
4217                } else {
4218                    MutationResult::Conflict {
4219                        reason: format!("relation '{}' does not exist", id),
4220                    }
4221                }
4222            }
4223            Mutation::SearchPath(sp) => {
4224                if sp.local && self.local.transactions.is_empty() {
4225                    // PostgreSQL warns and leaves SET LOCAL unchanged outside
4226                    // an explicit transaction block.
4227                    return MutationResult::Skipped;
4228                }
4229                self.snapshot_search_path();
4230                self.snapshot_confidence();
4231                let template = match &sp.target {
4232                    SearchPathTarget::Default => self.local.default_search_path_template.clone(),
4233                    SearchPathTarget::Schemas(schemas) => schemas.clone(),
4234                };
4235                self.local.search_path_template = template.clone();
4236                if !sp.local {
4237                    self.local.session_search_path_template = template;
4238                }
4239                self.refresh_role_sensitive_search_path();
4240                MutationResult::Applied
4241            }
4242            Mutation::TimeoutSetting(change) => {
4243                if change.local && self.local.transactions.is_empty() {
4244                    return MutationResult::Skipped;
4245                }
4246                let next = match &change.value {
4247                    TimeoutSettingValue::Default => match change.setting {
4248                        TimeoutSetting::Lock => self.local.lock_timeout.default,
4249                        TimeoutSetting::Statement => self.local.statement_timeout.default,
4250                    },
4251                    TimeoutSettingValue::Milliseconds(milliseconds) => Some(*milliseconds),
4252                    TimeoutSettingValue::Current => match change.setting {
4253                        TimeoutSetting::Lock => self.local.lock_timeout.effective,
4254                        TimeoutSetting::Statement => self.local.statement_timeout.effective,
4255                    },
4256                    TimeoutSettingValue::Invalid(reason) => {
4257                        return MutationResult::Conflict {
4258                            reason: reason.clone(),
4259                        };
4260                    }
4261                };
4262                self.snapshot_timeout_settings();
4263                let setting = match change.setting {
4264                    TimeoutSetting::Lock => &mut self.local.lock_timeout,
4265                    TimeoutSetting::Statement => &mut self.local.statement_timeout,
4266                };
4267                setting.effective = next;
4268                if !change.local {
4269                    setting.session = next;
4270                }
4271                MutationResult::Applied
4272            }
4273            Mutation::ResetSettings(target) => {
4274                if matches!(
4275                    target,
4276                    ResetSettingTarget::All | ResetSettingTarget::SearchPath
4277                ) {
4278                    self.snapshot_search_path();
4279                    self.snapshot_confidence();
4280                    let template = self.local.default_search_path_template.clone();
4281                    self.local.session_search_path_template = template.clone();
4282                    self.local.search_path_template = template;
4283                    self.refresh_role_sensitive_search_path();
4284                }
4285                if matches!(
4286                    target,
4287                    ResetSettingTarget::All
4288                        | ResetSettingTarget::LockTimeout
4289                        | ResetSettingTarget::StatementTimeout
4290                ) {
4291                    self.snapshot_timeout_settings();
4292                    if matches!(
4293                        target,
4294                        ResetSettingTarget::All | ResetSettingTarget::LockTimeout
4295                    ) {
4296                        self.local.lock_timeout.session = self.local.lock_timeout.default;
4297                        self.local.lock_timeout.effective = self.local.lock_timeout.default;
4298                    }
4299                    if matches!(
4300                        target,
4301                        ResetSettingTarget::All | ResetSettingTarget::StatementTimeout
4302                    ) {
4303                        self.local.statement_timeout.session = self.local.statement_timeout.default;
4304                        self.local.statement_timeout.effective =
4305                            self.local.statement_timeout.default;
4306                    }
4307                }
4308                MutationResult::Applied
4309            }
4310            Mutation::CheckTimeouts => MutationResult::Applied,
4311            Mutation::SwitchRole {
4312                role,
4313                local,
4314                is_session_auth,
4315            } => {
4316                if *local && self.local.transactions.is_empty() {
4317                    // PostgreSQL warns and leaves the setting unchanged.
4318                    return MutationResult::Skipped;
4319                }
4320
4321                let target = if let Some(role) = role {
4322                    let Some(identity) = self.role_fact_identity(role) else {
4323                        self.snapshot_confidence();
4324                        self.local.confidence = Confidence::Tainted;
4325                        return MutationResult::Skipped;
4326                    };
4327                    Some(identity)
4328                } else if *is_session_auth {
4329                    Some((
4330                        self.local.authenticated_role.clone(),
4331                        self.local.authenticated_role_known,
4332                    ))
4333                } else {
4334                    Some((
4335                        self.local.session_role.clone(),
4336                        self.local.session_role_known,
4337                    ))
4338                };
4339                let (target_name, target_known) = target.expect("role reset always has a target");
4340                let persistent_role_reset_target = if role.is_none() && !*is_session_auth {
4341                    Some((
4342                        self.local.persistent_session_role.clone(),
4343                        self.local.persistent_session_role_known,
4344                    ))
4345                } else {
4346                    None
4347                };
4348
4349                let authorized = if role.is_none() {
4350                    Some(true)
4351                } else if *is_session_auth {
4352                    self.can_set_session_authorization_to(&target_name)
4353                } else {
4354                    self.can_set_role_to(&target_name)
4355                };
4356                match authorized {
4357                    Some(false) => {
4358                        return MutationResult::Conflict {
4359                            reason: if self.present_role(&target_name).is_none() {
4360                                format!("role '{}' does not exist", target_name)
4361                            } else {
4362                                format!("permission denied to set role '{}'", target_name)
4363                            },
4364                        };
4365                    }
4366                    None => {
4367                        self.snapshot_confidence();
4368                        self.local.confidence = Confidence::Tainted;
4369                    }
4370                    Some(true) => {}
4371                }
4372
4373                self.snapshot_role_context();
4374                self.snapshot_search_path();
4375                self.snapshot_confidence();
4376                if *is_session_auth {
4377                    self.local.session_role = target_name.clone();
4378                    self.local.session_role_known = target_known;
4379                    self.local.current_role = target_name.clone();
4380                    self.local.current_role_known = target_known;
4381                    if !local {
4382                        self.local.persistent_session_role = target_name.clone();
4383                        self.local.persistent_session_role_known = target_known;
4384                        self.local.persistent_current_role = target_name;
4385                        self.local.persistent_current_role_known = target_known;
4386                    }
4387                } else {
4388                    self.local.current_role = target_name.clone();
4389                    self.local.current_role_known = target_known;
4390                    if !local {
4391                        let (persistent_name, persistent_known) =
4392                            persistent_role_reset_target.unwrap_or((target_name, target_known));
4393                        self.local.persistent_current_role = persistent_name;
4394                        self.local.persistent_current_role_known = persistent_known;
4395                    }
4396                }
4397                self.refresh_role_sensitive_search_path();
4398                MutationResult::Applied
4399            }
4400            Mutation::BeginTransaction => {
4401                if self.local.transactions.is_empty() {
4402                    self.local.transactions.push(TransactionFrame::root());
4403                    MutationResult::Applied
4404                } else {
4405                    // PostgreSQL emits a warning and leaves the current
4406                    // transaction active for a nested BEGIN.
4407                    MutationResult::Skipped
4408                }
4409            }
4410            Mutation::CommitTransaction => {
4411                if self.local.transaction_aborted {
4412                    while let Some(frame) = self.local.transactions.pop() {
4413                        self.rollback_frame(frame);
4414                    }
4415                } else {
4416                    while self.local.transactions.pop().is_some() {}
4417                    self.restore_persistent_role_context();
4418                }
4419                self.local.transaction_aborted = false;
4420                MutationResult::Applied
4421            }
4422            Mutation::CommitAndChain => {
4423                if self.local.transactions.is_empty() {
4424                    self.local.confidence = Confidence::Tainted;
4425                    return MutationResult::Conflict {
4426                        reason: "COMMIT AND CHAIN can only be used in transaction blocks"
4427                            .to_string(),
4428                    };
4429                }
4430                if self.local.transaction_aborted {
4431                    while let Some(frame) = self.local.transactions.pop() {
4432                        self.rollback_frame(frame);
4433                    }
4434                } else {
4435                    while self.local.transactions.pop().is_some() {}
4436                    self.restore_persistent_role_context();
4437                }
4438                self.local.transaction_aborted = false;
4439                self.local.transactions.push(TransactionFrame::root());
4440                MutationResult::Applied
4441            }
4442            Mutation::RollbackTransaction => {
4443                while let Some(frame) = self.local.transactions.pop() {
4444                    self.rollback_frame(frame);
4445                }
4446                self.local.transaction_aborted = false;
4447                MutationResult::Applied
4448            }
4449            Mutation::RollbackAndChain => {
4450                if self.local.transactions.is_empty() {
4451                    self.local.confidence = Confidence::Tainted;
4452                    return MutationResult::Conflict {
4453                        reason: "ROLLBACK AND CHAIN can only be used in transaction blocks"
4454                            .to_string(),
4455                    };
4456                }
4457                while let Some(frame) = self.local.transactions.pop() {
4458                    self.rollback_frame(frame);
4459                }
4460                self.local.transaction_aborted = false;
4461                self.local.transactions.push(TransactionFrame::root());
4462                MutationResult::Applied
4463            }
4464            Mutation::RollbackToSavepoint(rts) => {
4465                let Some(position) = self
4466                    .local
4467                    .transactions
4468                    .iter()
4469                    .rposition(|frame| frame.is_named_savepoint(&rts.name))
4470                else {
4471                    self.local.confidence = Confidence::Tainted;
4472                    if !self.local.transactions.is_empty() {
4473                        self.local.transaction_aborted = true;
4474                    }
4475                    return MutationResult::Conflict {
4476                        reason: format!("savepoint '{}' does not exist", rts.name),
4477                    };
4478                };
4479                let rolled_back = self.local.transactions.split_off(position + 1);
4480                // Frames are popped newest-first. Restore them in that same
4481                // order before restoring changes made after the target
4482                // savepoint itself; undo logs are chronological.
4483                for frame in rolled_back.into_iter().rev() {
4484                    self.rollback_frame(frame);
4485                }
4486                let undo_log = std::mem::take(&mut self.local.transactions[position].undo_log);
4487                self.rollback_undo_log(undo_log);
4488                self.local.transaction_aborted = false;
4489                MutationResult::Applied
4490            }
4491            Mutation::Savepoint(sp) => {
4492                if self.local.transactions.is_empty() {
4493                    self.local.confidence = Confidence::Tainted;
4494                    return MutationResult::Conflict {
4495                        reason: "SAVEPOINT can only be used in transaction blocks".to_string(),
4496                    };
4497                }
4498                self.local
4499                    .transactions
4500                    .push(TransactionFrame::savepoint(sp.name.clone()));
4501                MutationResult::Applied
4502            }
4503            Mutation::ReleaseSavepoint(rsp) => {
4504                let Some(position) = self
4505                    .local
4506                    .transactions
4507                    .iter()
4508                    .rposition(|frame| frame.is_named_savepoint(&rsp.name))
4509                else {
4510                    self.local.confidence = Confidence::Tainted;
4511                    if !self.local.transactions.is_empty() {
4512                        self.local.transaction_aborted = true;
4513                    }
4514                    return MutationResult::Conflict {
4515                        reason: format!("savepoint '{}' does not exist", rsp.name),
4516                    };
4517                };
4518                if position == 0 {
4519                    self.local.confidence = Confidence::Tainted;
4520                    return MutationResult::Conflict {
4521                        reason: format!("savepoint '{}' is not inside a transaction", rsp.name),
4522                    };
4523                }
4524
4525                let released = self.local.transactions.split_off(position);
4526                let outer = self
4527                    .local
4528                    .transactions
4529                    .last_mut()
4530                    .expect("a released savepoint always has an outer transaction frame");
4531                for frame in released {
4532                    outer.undo_log.extend(frame.undo_log);
4533                }
4534                MutationResult::Applied
4535            }
4536            Mutation::Opaque(_) => {
4537                self.snapshot_confidence();
4538                self.local.confidence = Confidence::Tainted;
4539                MutationResult::Applied
4540            }
4541            Mutation::CreateFunction(f) => {
4542                let routine_kind =
4543                    if f.options.iter().any(|option| {
4544                        matches!(option, crate::analysis::facts::FuncOptionFact::Window)
4545                    }) {
4546                        crate::model::function::RoutineKind::Window
4547                    } else {
4548                        crate::model::function::RoutineKind::Function
4549                    };
4550                match self.local.functions.get(&f.id) {
4551                    Some(crate::model::function::FunctionOverlay::Present(existing))
4552                        if existing.routine_kind != routine_kind || !f.or_replace =>
4553                    {
4554                        return MutationResult::Conflict {
4555                            reason: format!("routine '{}' already exists", f.id),
4556                        };
4557                    }
4558                    None if !self.baseline_available || !self.baseline_covers_object(&f.id) => {
4559                        self.snapshot_confidence();
4560                        self.local.confidence = Confidence::Tainted;
4561                    }
4562                    _ => {}
4563                }
4564                self.snapshot_function(&f.id);
4565                self.snapshot_generation_counter();
4566                self.local.generation_counter += 1;
4567                let _generation = self.local.generation_counter;
4568
4569                let volatility = f
4570                    .options
4571                    .iter()
4572                    .find_map(|opt| {
4573                        if let crate::analysis::facts::FuncOptionFact::Volatility(v) = opt {
4574                            Some(match v {
4575                                crate::analysis::facts::VolatilityKind::Volatile => {
4576                                    crate::model::function::Volatility::Volatile
4577                                }
4578                                crate::analysis::facts::VolatilityKind::Stable => {
4579                                    crate::model::function::Volatility::Stable
4580                                }
4581                                crate::analysis::facts::VolatilityKind::Immutable => {
4582                                    crate::model::function::Volatility::Immutable
4583                                }
4584                            })
4585                        } else {
4586                            None
4587                        }
4588                    })
4589                    .unwrap_or(crate::model::function::Volatility::Volatile);
4590
4591                let security = f
4592                    .options
4593                    .iter()
4594                    .find_map(|opt| {
4595                        if let crate::analysis::facts::FuncOptionFact::Security(s) = opt {
4596                            Some(match s {
4597                                crate::analysis::facts::SecurityKind::Invoker => {
4598                                    crate::model::function::SecurityMode::Invoker
4599                                }
4600                                crate::analysis::facts::SecurityKind::Definer => {
4601                                    crate::model::function::SecurityMode::Definer
4602                                }
4603                            })
4604                        } else {
4605                            None
4606                        }
4607                    })
4608                    .unwrap_or(crate::model::function::SecurityMode::Invoker);
4609
4610                let language = f
4611                    .options
4612                    .iter()
4613                    .find_map(|opt| {
4614                        if let crate::analysis::facts::FuncOptionFact::Language(l) = opt {
4615                            Some(l.clone())
4616                        } else {
4617                            None
4618                        }
4619                    })
4620                    .unwrap_or_else(|| "sql".to_string());
4621
4622                self.local.functions.insert(
4623                    f.id.clone(),
4624                    crate::model::function::FunctionOverlay::Present(
4625                        crate::model::function::FunctionState {
4626                            id: f.id.clone(),
4627                            routine_kind,
4628                            arg_types: f
4629                                .params
4630                                .iter()
4631                                .filter(|p| {
4632                                    !matches!(&p.mode, crate::analysis::facts::ParamModeFact::Out)
4633                                })
4634                                .map(|p| p.ty.clone())
4635                                .collect(),
4636                            arg_type_ids: f
4637                                .params
4638                                .iter()
4639                                .filter(|p| {
4640                                    !matches!(&p.mode, crate::analysis::facts::ParamModeFact::Out)
4641                                })
4642                                .map(|parameter| self.resolve_type_reference(&parameter.ty))
4643                                .collect(),
4644                            return_type: f
4645                                .return_type
4646                                .as_ref()
4647                                .map(|rt| match rt {
4648                                    crate::analysis::facts::RetTypeFact::Scalar(ty) => ty.clone(),
4649                                    crate::analysis::facts::RetTypeFact::Table(columns) => columns
4650                                        .iter()
4651                                        .map(|column| {
4652                                            format!(
4653                                                "{} {}",
4654                                                column.name,
4655                                                column.ty.as_deref().unwrap_or("unknown")
4656                                            )
4657                                        })
4658                                        .collect::<Vec<_>>()
4659                                        .join(", "),
4660                                })
4661                                .unwrap_or_default(),
4662                            return_type_id: f.return_type.as_ref().and_then(|return_type| {
4663                                match return_type {
4664                                    crate::analysis::facts::RetTypeFact::Scalar(ty) => {
4665                                        self.resolve_type_reference(ty)
4666                                    }
4667                                    crate::analysis::facts::RetTypeFact::Table(_) => None,
4668                                }
4669                            }),
4670                            volatility,
4671                            language,
4672                            security,
4673                        },
4674                    ),
4675                );
4676                MutationResult::Applied
4677            }
4678            Mutation::AlterFunction(f) => {
4679                use crate::analysis::facts::{AlterFunctionAction, FuncOptionFact};
4680                use crate::model::function::{
4681                    FunctionOverlay, RoutineKind, SecurityMode, Volatility,
4682                };
4683
4684                match self.local.functions.get(&f.id) {
4685                    Some(FunctionOverlay::Present(function))
4686                        if matches!(
4687                            function.routine_kind,
4688                            RoutineKind::Function | RoutineKind::Window
4689                        ) => {}
4690                    Some(FunctionOverlay::Present(_)) => {
4691                        return MutationResult::Conflict {
4692                            reason: format!("'{}' is not a function", f.id),
4693                        };
4694                    }
4695                    Some(FunctionOverlay::Dropped) => {
4696                        return MutationResult::Conflict {
4697                            reason: format!("function '{}' does not exist", f.id),
4698                        };
4699                    }
4700                    _ if self.baseline_available && self.baseline_covers_object(&f.id) => {
4701                        return MutationResult::Conflict {
4702                            reason: format!("function '{}' does not exist", f.id),
4703                        };
4704                    }
4705                    _ => {
4706                        self.snapshot_confidence();
4707                        self.local.confidence = Confidence::Tainted;
4708                        return MutationResult::Skipped;
4709                    }
4710                }
4711
4712                match &f.action {
4713                    AlterFunctionAction::OptionsChange(options) => {
4714                        self.snapshot_function(&f.id);
4715                        if let Some(FunctionOverlay::Present(function)) =
4716                            self.local.functions.get_mut(&f.id)
4717                        {
4718                            for option in options {
4719                                match option {
4720                                    FuncOptionFact::Volatility(volatility) => {
4721                                        function.volatility = match volatility {
4722                                            crate::analysis::facts::VolatilityKind::Volatile => {
4723                                                Volatility::Volatile
4724                                            }
4725                                            crate::analysis::facts::VolatilityKind::Stable => {
4726                                                Volatility::Stable
4727                                            }
4728                                            crate::analysis::facts::VolatilityKind::Immutable => {
4729                                                Volatility::Immutable
4730                                            }
4731                                        };
4732                                    }
4733                                    FuncOptionFact::Security(security) => {
4734                                        function.security = match security {
4735                                            crate::analysis::facts::SecurityKind::Invoker => {
4736                                                SecurityMode::Invoker
4737                                            }
4738                                            crate::analysis::facts::SecurityKind::Definer => {
4739                                                SecurityMode::Definer
4740                                            }
4741                                        };
4742                                    }
4743                                    FuncOptionFact::Language(language) => {
4744                                        function.language = language.clone();
4745                                    }
4746                                    _ => {}
4747                                }
4748                            }
4749                        }
4750                    }
4751                    AlterFunctionAction::Rename { to, .. } => {
4752                        let signature =
4753                            f.id.name
4754                                .find('(')
4755                                .map(|index| &f.id.name[index..])
4756                                .unwrap_or("");
4757                        let new_id = ObjectId::new(f.id.schema.clone(), format!("{to}{signature}"));
4758                        self.move_function(&f.id, &new_id);
4759                    }
4760                    AlterFunctionAction::SchemaChange { new_schema } => {
4761                        let new_id = ObjectId::new(new_schema.clone(), f.id.name.clone());
4762                        self.move_function(&f.id, &new_id);
4763                    }
4764                    AlterFunctionAction::OwnerChange(_)
4765                    | AlterFunctionAction::DependsOnExtension { .. }
4766                    | AlterFunctionAction::NoDependsOnExtension { .. } => {
4767                        self.snapshot_function(&f.id);
4768                    }
4769                }
4770                MutationResult::Applied
4771            }
4772            Mutation::DropFunction(f) => {
4773                let mut any_applied = false;
4774                for sig in &f.signatures {
4775                    let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
4776                    let schema = self.resolve_function_schema(&sig.name, &sig_str);
4777                    let id = ObjectId::new(schema, sig_str);
4778                    let is_function = matches!(
4779                        self.local.functions.get(&id),
4780                        Some(crate::model::function::FunctionOverlay::Present(function))
4781                            if matches!(
4782                                function.routine_kind,
4783                                crate::model::function::RoutineKind::Function
4784                                    | crate::model::function::RoutineKind::Window
4785                            )
4786                    );
4787                    if !is_function {
4788                        let routine_exists = self.local.functions.contains_key(&id);
4789                        let absence_is_exact =
4790                            self.baseline_available && self.baseline_covers_object(&id);
4791                        if routine_exists || (!f.if_exists && absence_is_exact) {
4792                            return MutationResult::Conflict {
4793                                reason: format!("function '{}' does not exist", id),
4794                            };
4795                        } else if !f.if_exists && !absence_is_exact {
4796                            self.snapshot_confidence();
4797                            self.local.confidence = Confidence::Tainted;
4798                        }
4799                    } else {
4800                        let dependent_triggers: Vec<(ObjectId, ObjectId)> = self
4801                            .local
4802                            .graph
4803                            .edges
4804                            .iter()
4805                            .filter_map(|edge| {
4806                                let DependencyKind::TriggerOnTable { function_id, .. } = &edge.kind
4807                                else {
4808                                    return None;
4809                                };
4810                                (function_id == &id)
4811                                    .then(|| (edge.dependent.clone(), edge.referenced.clone()))
4812                            })
4813                            .collect();
4814                        if !dependent_triggers.is_empty() && !f.cascade {
4815                            return MutationResult::Conflict {
4816                                reason: format!(
4817                                    "function '{}' still has dependent triggers; use CASCADE",
4818                                    id
4819                                ),
4820                            };
4821                        }
4822
4823                        any_applied = true;
4824                        self.snapshot_function(&id);
4825                        self.local
4826                            .functions
4827                            .insert(id.clone(), crate::model::function::FunctionOverlay::Dropped);
4828
4829                        if f.cascade {
4830                            for (trigger_id, table_id) in &dependent_triggers {
4831                                let trigger_name =
4832                                    self.local.triggers.get(trigger_id).and_then(|overlay| {
4833                                        match overlay {
4834                                            TriggerOverlay::Present(trigger) => {
4835                                                Some(trigger.name.clone())
4836                                            }
4837                                            TriggerOverlay::Dropped => None,
4838                                        }
4839                                    });
4840                                self.snapshot_trigger(trigger_id);
4841                                self.local
4842                                    .triggers
4843                                    .insert(trigger_id.clone(), TriggerOverlay::Dropped);
4844                                self.snapshot_relation(table_id);
4845                                if let Some(RelationOverlay::Present(relation)) =
4846                                    self.local.relations.get_mut(table_id)
4847                                    && let Some(trigger_name) = trigger_name
4848                                {
4849                                    relation.triggers.remove(&trigger_name);
4850                                }
4851                            }
4852                            if !dependent_triggers.is_empty() {
4853                                self.snapshot_graph_full();
4854                                self.local.graph.edges.retain(|edge| {
4855                                    !dependent_triggers
4856                                        .iter()
4857                                        .any(|(trigger_id, _)| edge.dependent == *trigger_id)
4858                                });
4859                            }
4860                        }
4861                    }
4862                }
4863                if any_applied {
4864                    MutationResult::Applied
4865                } else {
4866                    MutationResult::Skipped
4867                }
4868            }
4869            Mutation::CreateProcedure(p) => {
4870                match self.local.functions.get(&p.id) {
4871                    Some(crate::model::function::FunctionOverlay::Present(existing))
4872                        if existing.routine_kind
4873                            == crate::model::function::RoutineKind::Procedure
4874                            && p.or_replace => {}
4875                    Some(crate::model::function::FunctionOverlay::Present(_)) => {
4876                        return MutationResult::Conflict {
4877                            reason: format!("routine '{}' already exists", p.id),
4878                        };
4879                    }
4880                    None if !self.baseline_available || !self.baseline_covers_object(&p.id) => {
4881                        self.snapshot_confidence();
4882                        self.local.confidence = Confidence::Tainted;
4883                    }
4884                    None => {}
4885                    Some(crate::model::function::FunctionOverlay::Dropped) => {}
4886                }
4887                self.snapshot_function(&p.id);
4888                self.snapshot_generation_counter();
4889                self.local.generation_counter += 1;
4890                let _generation = self.local.generation_counter;
4891
4892                self.local.functions.insert(
4893                    p.id.clone(),
4894                    crate::model::function::FunctionOverlay::Present(
4895                        crate::model::function::FunctionState {
4896                            id: p.id.clone(),
4897                            routine_kind: crate::model::function::RoutineKind::Procedure,
4898                            arg_types: p
4899                                .params
4900                                .iter()
4901                                .filter(|p| {
4902                                    !matches!(&p.mode, crate::analysis::facts::ParamModeFact::Out)
4903                                })
4904                                .map(|p| p.ty.clone())
4905                                .collect(),
4906                            arg_type_ids: p
4907                                .params
4908                                .iter()
4909                                .filter(|p| {
4910                                    !matches!(&p.mode, crate::analysis::facts::ParamModeFact::Out)
4911                                })
4912                                .map(|parameter| self.resolve_type_reference(&parameter.ty))
4913                                .collect(),
4914                            return_type: "void".to_string(),
4915                            return_type_id: None,
4916                            volatility: crate::model::function::Volatility::Volatile,
4917                            language: "sql".to_string(),
4918                            security: crate::model::function::SecurityMode::Invoker,
4919                        },
4920                    ),
4921                );
4922                MutationResult::Applied
4923            }
4924            Mutation::AlterProcedure(p) => {
4925                use crate::analysis::facts::AlterFunctionAction;
4926                use crate::model::function::{FunctionOverlay, RoutineKind};
4927
4928                match self.local.functions.get(&p.id) {
4929                    Some(FunctionOverlay::Present(function))
4930                        if function.routine_kind == RoutineKind::Procedure => {}
4931                    Some(FunctionOverlay::Present(_)) => {
4932                        return MutationResult::Conflict {
4933                            reason: format!("'{}' is not a procedure", p.id),
4934                        };
4935                    }
4936                    Some(FunctionOverlay::Dropped) => {
4937                        return MutationResult::Conflict {
4938                            reason: format!("procedure '{}' does not exist", p.id),
4939                        };
4940                    }
4941                    None if self.baseline_available && self.baseline_covers_object(&p.id) => {
4942                        return MutationResult::Conflict {
4943                            reason: format!("procedure '{}' does not exist", p.id),
4944                        };
4945                    }
4946                    None => {
4947                        self.snapshot_confidence();
4948                        self.local.confidence = Confidence::Tainted;
4949                        return MutationResult::Skipped;
4950                    }
4951                }
4952
4953                match &p.action {
4954                    AlterFunctionAction::Rename { to, .. } => {
4955                        let signature =
4956                            p.id.name
4957                                .find('(')
4958                                .map(|index| &p.id.name[index..])
4959                                .unwrap_or("");
4960                        let new_id = ObjectId::new(p.id.schema.clone(), format!("{to}{signature}"));
4961                        self.move_function(&p.id, &new_id);
4962                    }
4963                    AlterFunctionAction::SchemaChange { new_schema } => {
4964                        let new_id = ObjectId::new(new_schema.clone(), p.id.name.clone());
4965                        self.move_function(&p.id, &new_id);
4966                    }
4967                    _ => self.snapshot_function(&p.id),
4968                }
4969                MutationResult::Applied
4970            }
4971            Mutation::DropProcedure(p) => {
4972                let mut any_applied = false;
4973                for sig in &p.signatures {
4974                    let sig_str = format!("{}({})", sig.name.name.resolve(), sig.params.join(","));
4975                    let schema = self.resolve_function_schema(&sig.name, &sig_str);
4976                    let id = ObjectId::new(schema, sig_str);
4977                    let is_procedure = matches!(
4978                        self.local.functions.get(&id),
4979                        Some(crate::model::function::FunctionOverlay::Present(function))
4980                            if function.routine_kind
4981                                == crate::model::function::RoutineKind::Procedure
4982                    );
4983                    if is_procedure {
4984                        any_applied = true;
4985                        self.snapshot_function(&id);
4986                        self.local
4987                            .functions
4988                            .insert(id, crate::model::function::FunctionOverlay::Dropped);
4989                    } else if self.local.functions.contains_key(&id)
4990                        || !p.if_exists
4991                            && self.baseline_available
4992                            && self.baseline_covers_object(&id)
4993                    {
4994                        return MutationResult::Conflict {
4995                            reason: format!("procedure '{}' does not exist", id),
4996                        };
4997                    } else if !p.if_exists {
4998                        self.snapshot_confidence();
4999                        self.local.confidence = Confidence::Tainted;
5000                        return MutationResult::Skipped;
5001                    }
5002                }
5003                if any_applied {
5004                    MutationResult::Applied
5005                } else {
5006                    MutationResult::Skipped
5007                }
5008            }
5009            Mutation::CreateAggregate(a) => {
5010                match self.local.functions.get(&a.id) {
5011                    Some(crate::model::function::FunctionOverlay::Present(existing))
5012                        if existing.routine_kind
5013                            == crate::model::function::RoutineKind::Aggregate
5014                            && a.or_replace => {}
5015                    Some(crate::model::function::FunctionOverlay::Present(_)) => {
5016                        return MutationResult::Conflict {
5017                            reason: format!("routine '{}' already exists", a.id),
5018                        };
5019                    }
5020                    None if !self.baseline_available || !self.baseline_covers_object(&a.id) => {
5021                        self.snapshot_confidence();
5022                        self.local.confidence = Confidence::Tainted;
5023                    }
5024                    None | Some(crate::model::function::FunctionOverlay::Dropped) => {}
5025                }
5026
5027                self.snapshot_function(&a.id);
5028                self.local.functions.insert(
5029                    a.id.clone(),
5030                    crate::model::function::FunctionOverlay::Present(
5031                        crate::model::function::FunctionState {
5032                            id: a.id.clone(),
5033                            routine_kind: crate::model::function::RoutineKind::Aggregate,
5034                            arg_types: a
5035                                .params
5036                                .iter()
5037                                .filter(|parameter| {
5038                                    !matches!(
5039                                        parameter.mode,
5040                                        crate::analysis::facts::ParamModeFact::Out
5041                                    )
5042                                })
5043                                .map(|parameter| parameter.ty.clone())
5044                                .collect(),
5045                            arg_type_ids: a
5046                                .params
5047                                .iter()
5048                                .filter(|parameter| {
5049                                    !matches!(
5050                                        parameter.mode,
5051                                        crate::analysis::facts::ParamModeFact::Out
5052                                    )
5053                                })
5054                                .map(|parameter| self.resolve_type_reference(&parameter.ty))
5055                                .collect(),
5056                            return_type: String::new(),
5057                            return_type_id: None,
5058                            volatility: crate::model::function::Volatility::Volatile,
5059                            language: "internal".to_string(),
5060                            security: crate::model::function::SecurityMode::Invoker,
5061                        },
5062                    ),
5063                );
5064                MutationResult::Applied
5065            }
5066            Mutation::AlterAggregate(a) => {
5067                use crate::analysis::facts::AlterFunctionAction;
5068                use crate::model::function::{FunctionOverlay, RoutineKind};
5069
5070                match self.local.functions.get(&a.id) {
5071                    Some(FunctionOverlay::Present(aggregate))
5072                        if aggregate.routine_kind == RoutineKind::Aggregate => {}
5073                    Some(FunctionOverlay::Present(_)) => {
5074                        return MutationResult::Conflict {
5075                            reason: format!("'{}' is not an aggregate", a.id),
5076                        };
5077                    }
5078                    Some(FunctionOverlay::Dropped) => {
5079                        return MutationResult::Conflict {
5080                            reason: format!("aggregate '{}' does not exist", a.id),
5081                        };
5082                    }
5083                    None if self.baseline_available && self.baseline_covers_object(&a.id) => {
5084                        return MutationResult::Conflict {
5085                            reason: format!("aggregate '{}' does not exist", a.id),
5086                        };
5087                    }
5088                    None => {
5089                        self.snapshot_confidence();
5090                        self.local.confidence = Confidence::Tainted;
5091                        return MutationResult::Skipped;
5092                    }
5093                }
5094
5095                match &a.action {
5096                    AlterFunctionAction::Rename { to, .. } => {
5097                        let signature =
5098                            a.id.name
5099                                .find('(')
5100                                .map(|index| &a.id.name[index..])
5101                                .unwrap_or("");
5102                        let new_id = ObjectId::new(a.id.schema.clone(), format!("{to}{signature}"));
5103                        self.move_function(&a.id, &new_id);
5104                    }
5105                    AlterFunctionAction::SchemaChange { new_schema } => {
5106                        let new_id = ObjectId::new(new_schema.clone(), a.id.name.clone());
5107                        self.move_function(&a.id, &new_id);
5108                    }
5109                    AlterFunctionAction::OwnerChange(_) => self.snapshot_function(&a.id),
5110                    _ => unreachable!("aggregate extraction only emits rename, owner, or schema"),
5111                }
5112                MutationResult::Applied
5113            }
5114            Mutation::DropAggregate(a) => {
5115                let mut any_applied = false;
5116                for signature in &a.signatures {
5117                    let signature_name = format!(
5118                        "{}({})",
5119                        signature.name.name.resolve(),
5120                        signature.params.join(",")
5121                    );
5122                    let schema = self.resolve_function_schema(&signature.name, &signature_name);
5123                    let id = ObjectId::new(schema, signature_name);
5124                    let is_aggregate = matches!(
5125                        self.local.functions.get(&id),
5126                        Some(crate::model::function::FunctionOverlay::Present(routine))
5127                            if routine.routine_kind
5128                                == crate::model::function::RoutineKind::Aggregate
5129                    );
5130                    if is_aggregate {
5131                        any_applied = true;
5132                        self.snapshot_function(&id);
5133                        self.local
5134                            .functions
5135                            .insert(id, crate::model::function::FunctionOverlay::Dropped);
5136                    } else if self.local.functions.contains_key(&id)
5137                        || self.baseline_available && self.baseline_covers_object(&id)
5138                    {
5139                        if a.if_exists {
5140                            continue;
5141                        }
5142                        return MutationResult::Conflict {
5143                            reason: format!("aggregate '{}' does not exist", id),
5144                        };
5145                    } else {
5146                        self.snapshot_confidence();
5147                        self.local.confidence = Confidence::Tainted;
5148                        if !a.if_exists {
5149                            return MutationResult::Skipped;
5150                        }
5151                    }
5152                }
5153                if a.cascade && any_applied {
5154                    self.snapshot_confidence();
5155                    self.local.confidence = Confidence::Tainted;
5156                }
5157                if any_applied {
5158                    MutationResult::Applied
5159                } else {
5160                    MutationResult::Skipped
5161                }
5162            }
5163            Mutation::CreatePublication(p) => {
5164                match self.local.publications.get(&p.name) {
5165                    Some(crate::model::replication::PublicationOverlay::Present(_)) => {
5166                        return MutationResult::Conflict {
5167                            reason: format!("publication '{}' already exists", p.name),
5168                        };
5169                    }
5170                    None => {
5171                        if !self.baseline_available {
5172                            self.snapshot_confidence();
5173                            self.local.confidence = Confidence::Tainted;
5174                        }
5175                    }
5176                    Some(crate::model::replication::PublicationOverlay::Dropped) => {}
5177                }
5178                if let Err(reason) = self.validate_publication_scope(&p.scope) {
5179                    return MutationResult::Conflict { reason };
5180                }
5181                self.taint_inheritance_sensitive_publication_scope(&p.scope);
5182                self.snapshot_publication(&p.name);
5183                self.snapshot_generation_counter();
5184                self.local.generation_counter += 1;
5185                let generation = self.local.generation_counter;
5186
5187                let owner = self
5188                    .local
5189                    .current_role_known
5190                    .then(|| self.local.current_role.clone());
5191                self.local.publications.insert(
5192                    p.name.clone(),
5193                    crate::model::replication::PublicationOverlay::Present(
5194                        crate::model::replication::PublicationState {
5195                            name: p.name.clone(),
5196                            owner,
5197                            scope: p.scope.clone(),
5198                            params: p.params.clone(),
5199                            generation,
5200                        },
5201                    ),
5202                );
5203
5204                if let crate::analysis::facts::PublicationScope::Explicit(objects) = &p.scope {
5205                    self.snapshot_graph_full();
5206                    for obj in objects {
5207                        if let crate::analysis::facts::PublicationObjectFact::Table {
5208                            name, ..
5209                        } = obj
5210                        {
5211                            let table_id = self.resolve_relation_id(name);
5212                            self.local.graph.edges.push(DependencyEdge::new(
5213                                table_id,
5214                                ObjectId::new("public", &p.name),
5215                                DependencyKind::PublicationIncludes {
5216                                    publication_name: p.name.clone(),
5217                                },
5218                            ));
5219                        }
5220                    }
5221                }
5222                MutationResult::Applied
5223            }
5224            Mutation::AlterPublication(p) => {
5225                match self.local.publications.get(&p.name) {
5226                    Some(crate::model::replication::PublicationOverlay::Present(_)) => {}
5227                    Some(crate::model::replication::PublicationOverlay::Dropped) => {
5228                        return MutationResult::Conflict {
5229                            reason: format!("publication '{}' does not exist", p.name),
5230                        };
5231                    }
5232                    None => {
5233                        if self.baseline_available {
5234                            return MutationResult::Conflict {
5235                                reason: format!("publication '{}' does not exist", p.name),
5236                            };
5237                        }
5238                        self.snapshot_confidence();
5239                        self.local.confidence = Confidence::Tainted;
5240                        return MutationResult::Skipped;
5241                    }
5242                }
5243                self.snapshot_publication(&p.name);
5244                self.snapshot_generation_counter();
5245                self.local.generation_counter += 1;
5246                let new_gen = self.local.generation_counter;
5247                use crate::analysis::facts::AlterPublicationActionFact;
5248
5249                let current_scope = match self.local.publications.get(&p.name) {
5250                    Some(crate::model::replication::PublicationOverlay::Present(publication)) => {
5251                        publication.scope.clone()
5252                    }
5253                    _ => unreachable!("publication existence checked above"),
5254                };
5255                let mut replacement_scope = None;
5256                let mut rename_to = None;
5257                match &p.action {
5258                    AlterPublicationActionFact::AddObjects(additions) => {
5259                        let additions_scope =
5260                            crate::analysis::facts::PublicationScope::Explicit(additions.clone());
5261                        if let Err(reason) = self.validate_publication_scope(&additions_scope) {
5262                            return MutationResult::Conflict { reason };
5263                        }
5264                        self.taint_inheritance_sensitive_publication_scope(&additions_scope);
5265                        let crate::analysis::facts::PublicationScope::Explicit(mut objects) =
5266                            current_scope
5267                        else {
5268                            return MutationResult::Conflict {
5269                                reason: format!(
5270                                    "publication '{}' already includes all tables",
5271                                    p.name
5272                                ),
5273                            };
5274                        };
5275                        let mut keys: HashSet<String> = objects
5276                            .iter()
5277                            .map(|object| self.publication_object_key(object))
5278                            .collect();
5279                        for addition in additions {
5280                            let key = self.publication_object_key(addition);
5281                            if !keys.insert(key) {
5282                                return MutationResult::Conflict {
5283                                    reason: format!(
5284                                        "publication '{}' already contains the requested object",
5285                                        p.name
5286                                    ),
5287                                };
5288                            }
5289                            objects.push(addition.clone());
5290                        }
5291                        replacement_scope =
5292                            Some(crate::analysis::facts::PublicationScope::Explicit(objects));
5293                    }
5294                    AlterPublicationActionFact::SetObjects(scope) => {
5295                        if let Err(reason) = self.validate_publication_scope(scope) {
5296                            return MutationResult::Conflict { reason };
5297                        }
5298                        self.taint_inheritance_sensitive_publication_scope(scope);
5299                        replacement_scope = Some(scope.clone());
5300                    }
5301                    AlterPublicationActionFact::DropObjects(removals) => {
5302                        self.taint_inheritance_sensitive_publication_scope(
5303                            &crate::analysis::facts::PublicationScope::Explicit(removals.clone()),
5304                        );
5305                        let crate::analysis::facts::PublicationScope::Explicit(mut objects) =
5306                            current_scope
5307                        else {
5308                            return MutationResult::Conflict {
5309                                reason: format!("publication '{}' includes all tables", p.name),
5310                            };
5311                        };
5312                        for removal in removals {
5313                            let key = self.publication_object_key(removal);
5314                            let Some(position) = objects
5315                                .iter()
5316                                .position(|object| self.publication_object_key(object) == key)
5317                            else {
5318                                return MutationResult::Conflict {
5319                                    reason: format!(
5320                                        "publication '{}' does not contain the requested object",
5321                                        p.name
5322                                    ),
5323                                };
5324                            };
5325                            objects.remove(position);
5326                        }
5327                        replacement_scope =
5328                            Some(crate::analysis::facts::PublicationScope::Explicit(objects));
5329                    }
5330                    AlterPublicationActionFact::SetOptions(options) => {
5331                        if let Some(crate::model::replication::PublicationOverlay::Present(
5332                            publication,
5333                        )) = self.local.publications.get_mut(&p.name)
5334                        {
5335                            for option in options {
5336                                publication
5337                                    .params
5338                                    .retain(|existing| existing.name != option.name);
5339                                publication.params.push(option.clone());
5340                            }
5341                        }
5342                    }
5343                    AlterPublicationActionFact::OwnerChange(role) => {
5344                        if let Some((owner, known)) = self.role_fact_identity(role) {
5345                            if known {
5346                                if let Some(
5347                                    crate::model::replication::PublicationOverlay::Present(
5348                                        publication,
5349                                    ),
5350                                ) = self.local.publications.get_mut(&p.name)
5351                                {
5352                                    publication.owner = Some(owner);
5353                                }
5354                            } else {
5355                                self.snapshot_confidence();
5356                                self.local.confidence = Confidence::Tainted;
5357                                if let Some(
5358                                    crate::model::replication::PublicationOverlay::Present(
5359                                        publication,
5360                                    ),
5361                                ) = self.local.publications.get_mut(&p.name)
5362                                {
5363                                    publication.owner = None;
5364                                }
5365                            }
5366                        } else {
5367                            self.snapshot_confidence();
5368                            self.local.confidence = Confidence::Tainted;
5369                            if let Some(crate::model::replication::PublicationOverlay::Present(
5370                                publication,
5371                            )) = self.local.publications.get_mut(&p.name)
5372                            {
5373                                publication.owner = None;
5374                            }
5375                        }
5376                    }
5377                    AlterPublicationActionFact::Rename { to } => {
5378                        if matches!(
5379                            self.local.publications.get(to),
5380                            Some(crate::model::replication::PublicationOverlay::Present(_))
5381                        ) {
5382                            return MutationResult::Conflict {
5383                                reason: format!("publication '{}' already exists", to),
5384                            };
5385                        }
5386                        rename_to = Some(to.clone());
5387                    }
5388                }
5389
5390                if let Some(scope) = replacement_scope {
5391                    if let Some(crate::model::replication::PublicationOverlay::Present(
5392                        publication,
5393                    )) = self.local.publications.get_mut(&p.name)
5394                    {
5395                        publication.scope = scope.clone();
5396                    }
5397                    self.replace_publication_edges(&p.name, &scope);
5398                }
5399                if let Some(crate::model::replication::PublicationOverlay::Present(publication)) =
5400                    self.local.publications.get_mut(&p.name)
5401                {
5402                    publication.generation = new_gen;
5403                }
5404                if let Some(to) = rename_to {
5405                    self.snapshot_publication(&to);
5406                    let Some(crate::model::replication::PublicationOverlay::Present(
5407                        mut publication,
5408                    )) = self.local.publications.remove(&p.name)
5409                    else {
5410                        unreachable!("publication existence checked above");
5411                    };
5412                    publication.name = to.clone();
5413                    self.local.publications.insert(
5414                        to.clone(),
5415                        crate::model::replication::PublicationOverlay::Present(publication),
5416                    );
5417                    self.snapshot_graph_full();
5418                    for edge in &mut self.local.graph.edges {
5419                        if let DependencyKind::PublicationIncludes { publication_name } =
5420                            &mut edge.kind
5421                            && publication_name == &p.name
5422                        {
5423                            *publication_name = to.clone();
5424                            edge.referenced = ObjectId::new("public", &to);
5425                        }
5426                    }
5427                }
5428                MutationResult::Applied
5429            }
5430            Mutation::DropPublication(p) => {
5431                let mut present_names = Vec::new();
5432                for name in &p.names {
5433                    match self.local.publications.get(name) {
5434                        Some(crate::model::replication::PublicationOverlay::Present(_)) => {
5435                            present_names.push(name.clone());
5436                        }
5437                        Some(crate::model::replication::PublicationOverlay::Dropped) => {
5438                            if !p.if_exists {
5439                                return MutationResult::Conflict {
5440                                    reason: format!("publication '{}' does not exist", name),
5441                                };
5442                            }
5443                        }
5444                        None if p.if_exists && self.baseline_available => {}
5445                        None if p.if_exists => {
5446                            self.snapshot_confidence();
5447                            self.local.confidence = Confidence::Tainted;
5448                        }
5449                        None => {
5450                            if self.baseline_available {
5451                                return MutationResult::Conflict {
5452                                    reason: format!("publication '{}' does not exist", name),
5453                                };
5454                            }
5455                            self.snapshot_confidence();
5456                            self.local.confidence = Confidence::Tainted;
5457                            return MutationResult::Skipped;
5458                        }
5459                    }
5460                }
5461                for name in &present_names {
5462                    self.snapshot_publication(name);
5463                    self.local.publications.insert(
5464                        name.clone(),
5465                        crate::model::replication::PublicationOverlay::Dropped,
5466                    );
5467                }
5468                self.snapshot_graph_full();
5469                self.local.graph.edges.retain(|e| {
5470                    !(matches!(e.kind, DependencyKind::PublicationIncludes { .. })
5471                        && present_names.contains(&e.referenced.name))
5472                });
5473                if present_names.is_empty() {
5474                    MutationResult::Skipped
5475                } else {
5476                    MutationResult::Applied
5477                }
5478            }
5479            Mutation::CreateSubscription(s) => {
5480                let name = s.name.clone().unwrap_or_else(|| "unnamed_sub".into());
5481                match self.local.subscriptions.get(&name) {
5482                    Some(crate::model::replication::SubscriptionOverlay::Present(_)) => {
5483                        return MutationResult::Conflict {
5484                            reason: format!("subscription '{}' already exists", name),
5485                        };
5486                    }
5487                    None => {
5488                        if !self.baseline_available {
5489                            self.snapshot_confidence();
5490                            self.local.confidence = Confidence::Tainted;
5491                        }
5492                    }
5493                    Some(crate::model::replication::SubscriptionOverlay::Dropped) => {}
5494                }
5495
5496                let params = s.params.as_deref();
5497                if let Err(reason) = Self::validate_subscription_boolean_options(
5498                    params,
5499                    &[
5500                        "connect",
5501                        "create_slot",
5502                        "enabled",
5503                        "copy_data",
5504                        "binary",
5505                        "disable_on_error",
5506                        "password_required",
5507                        "run_as_owner",
5508                        "failover",
5509                        "two_phase",
5510                    ],
5511                ) {
5512                    return MutationResult::Conflict { reason };
5513                }
5514                let connects_to_publisher =
5515                    Self::subscription_boolean_option(params, "connect") != Some(false);
5516                if !connects_to_publisher
5517                    && ["create_slot", "enabled", "copy_data"]
5518                        .iter()
5519                        .any(|name| Self::subscription_boolean_option(params, name) == Some(true))
5520                {
5521                    return MutationResult::Conflict {
5522                        reason: format!(
5523                            "subscription '{}' cannot enable connection-dependent options when connect is false",
5524                            name
5525                        ),
5526                    };
5527                }
5528                let creates_slot = connects_to_publisher
5529                    && Self::subscription_boolean_option(params, "create_slot") != Some(false);
5530                if !self.local.transactions.is_empty() && connects_to_publisher && creates_slot {
5531                    return MutationResult::Conflict {
5532                        reason: format!(
5533                            "subscription '{}' cannot create a replication slot inside a transaction",
5534                            name
5535                        ),
5536                    };
5537                }
5538                self.snapshot_subscription(&name);
5539                self.snapshot_generation_counter();
5540                self.local.generation_counter += 1;
5541                let generation = self.local.generation_counter;
5542
5543                if connects_to_publisher {
5544                    self.snapshot_confidence();
5545                    self.local.confidence = Confidence::Tainted;
5546                }
5547
5548                let enabled = connects_to_publisher
5549                    && Self::subscription_boolean_option(params, "enabled") != Some(false);
5550                let slot_name = match Self::subscription_option(params, "slot_name") {
5551                    Some(value) if value.eq_ignore_ascii_case("none") => None,
5552                    Some(value) => Some(value.to_string()),
5553                    None => Some(name.clone()),
5554                };
5555                if slot_name.is_none() && (enabled || creates_slot) {
5556                    return MutationResult::Conflict {
5557                        reason: format!(
5558                            "subscription '{}' with slot_name NONE must disable enabled and create_slot",
5559                            name
5560                        ),
5561                    };
5562                }
5563                let mut unique_publications = HashSet::new();
5564                if !s
5565                    .publications
5566                    .iter()
5567                    .all(|publication| unique_publications.insert(publication))
5568                {
5569                    return MutationResult::Conflict {
5570                        reason: format!(
5571                            "subscription '{}' lists the same publication more than once",
5572                            name
5573                        ),
5574                    };
5575                }
5576
5577                let owner = self
5578                    .local
5579                    .current_role_known
5580                    .then(|| self.local.current_role.clone());
5581                self.local.subscriptions.insert(
5582                    name.clone(),
5583                    crate::model::replication::SubscriptionOverlay::Present(
5584                        crate::model::replication::SubscriptionState {
5585                            name,
5586                            owner,
5587                            connection: s.connection.clone(),
5588                            publications: s.publications.clone(),
5589                            params: s.params.clone(),
5590                            enabled,
5591                            slot_name,
5592                            generation,
5593                        },
5594                    ),
5595                );
5596                MutationResult::Applied
5597            }
5598            Mutation::AlterSubscription(s) => {
5599                match self.local.subscriptions.get(&s.name) {
5600                    Some(crate::model::replication::SubscriptionOverlay::Present(_)) => {}
5601                    Some(crate::model::replication::SubscriptionOverlay::Dropped) => {
5602                        return MutationResult::Conflict {
5603                            reason: format!("subscription '{}' does not exist", s.name),
5604                        };
5605                    }
5606                    None => {
5607                        if self.baseline_available {
5608                            return MutationResult::Conflict {
5609                                reason: format!("subscription '{}' does not exist", s.name),
5610                            };
5611                        }
5612                        self.snapshot_confidence();
5613                        self.local.confidence = Confidence::Tainted;
5614                        return MutationResult::Skipped;
5615                    }
5616                }
5617                let existing = match self.local.subscriptions.get(&s.name) {
5618                    Some(crate::model::replication::SubscriptionOverlay::Present(subscription)) => {
5619                        subscription
5620                    }
5621                    _ => unreachable!("subscription existence checked above"),
5622                };
5623                let in_transaction = !self.local.transactions.is_empty();
5624                match &s.action {
5625                    crate::analysis::facts::AlterSubscriptionActionFact::Publications {
5626                        mode,
5627                        publications,
5628                        params,
5629                    } => {
5630                        if let Err(reason) = Self::validate_subscription_boolean_options(
5631                            Some(params),
5632                            &["refresh", "copy_data"],
5633                        ) {
5634                            return MutationResult::Conflict { reason };
5635                        }
5636                        if in_transaction
5637                            && Self::subscription_boolean_option(Some(params), "refresh")
5638                                != Some(false)
5639                        {
5640                            return MutationResult::Conflict {
5641                                reason: format!(
5642                                    "subscription '{}' cannot refresh publications inside a transaction",
5643                                    s.name
5644                                ),
5645                            };
5646                        }
5647
5648                        let mut unique = HashSet::new();
5649                        match mode {
5650                            crate::analysis::facts::SubscriptionPublicationMode::Set => {
5651                                if !publications
5652                                    .iter()
5653                                    .all(|publication| unique.insert(publication))
5654                                {
5655                                    return MutationResult::Conflict {
5656                                        reason: format!(
5657                                            "subscription '{}' lists the same publication more than once",
5658                                            s.name
5659                                        ),
5660                                    };
5661                                }
5662                            }
5663                            crate::analysis::facts::SubscriptionPublicationMode::Add => {
5664                                for publication in publications {
5665                                    if !unique.insert(publication)
5666                                        || existing.publications.contains(publication)
5667                                    {
5668                                        return MutationResult::Conflict {
5669                                            reason: format!(
5670                                                "subscription '{}' already includes publication '{}'",
5671                                                s.name, publication
5672                                            ),
5673                                        };
5674                                    }
5675                                }
5676                            }
5677                            crate::analysis::facts::SubscriptionPublicationMode::Drop => {
5678                                for publication in publications {
5679                                    if !unique.insert(publication)
5680                                        || !existing.publications.contains(publication)
5681                                    {
5682                                        return MutationResult::Conflict {
5683                                            reason: format!(
5684                                                "subscription '{}' does not include publication '{}'",
5685                                                s.name, publication
5686                                            ),
5687                                        };
5688                                    }
5689                                }
5690                            }
5691                        }
5692                    }
5693                    crate::analysis::facts::AlterSubscriptionActionFact::RefreshPublication(_)
5694                        if in_transaction =>
5695                    {
5696                        return MutationResult::Conflict {
5697                            reason: format!(
5698                                "subscription '{}' cannot refresh publications inside a transaction",
5699                                s.name
5700                            ),
5701                        };
5702                    }
5703                    crate::analysis::facts::AlterSubscriptionActionFact::SetOptions(options) => {
5704                        if let Err(reason) = Self::validate_subscription_boolean_options(
5705                            Some(options),
5706                            &[
5707                                "binary",
5708                                "disable_on_error",
5709                                "password_required",
5710                                "run_as_owner",
5711                                "failover",
5712                                "two_phase",
5713                            ],
5714                        ) {
5715                            return MutationResult::Conflict { reason };
5716                        }
5717                        if existing.enabled
5718                            && options
5719                                .iter()
5720                                .any(|option| option.name.eq_ignore_ascii_case("slot_name"))
5721                        {
5722                            return MutationResult::Conflict {
5723                                reason: format!(
5724                                    "subscription '{}' must be disabled before changing slot_name",
5725                                    s.name
5726                                ),
5727                            };
5728                        }
5729                        let changes_failover_or_two_phase = options.iter().any(|option| {
5730                            option.name.eq_ignore_ascii_case("failover")
5731                                || option.name.eq_ignore_ascii_case("two_phase")
5732                        });
5733                        if changes_failover_or_two_phase && existing.enabled {
5734                            return MutationResult::Conflict {
5735                                reason: format!(
5736                                    "subscription '{}' must be disabled before changing failover or two_phase",
5737                                    s.name
5738                                ),
5739                            };
5740                        }
5741                        let forbidden_in_transaction = options.iter().any(|option| {
5742                            option.name.eq_ignore_ascii_case("failover")
5743                                || (option.name.eq_ignore_ascii_case("two_phase")
5744                                    && Self::postgres_boolean(&option.value) == Some(false))
5745                        });
5746                        if in_transaction && forbidden_in_transaction {
5747                            return MutationResult::Conflict {
5748                                reason: format!(
5749                                    "subscription '{}' cannot change this setting inside a transaction",
5750                                    s.name
5751                                ),
5752                            };
5753                        }
5754                    }
5755                    crate::analysis::facts::AlterSubscriptionActionFact::SetEnabled(true)
5756                        if existing.slot_name.is_none() =>
5757                    {
5758                        return MutationResult::Conflict {
5759                            reason: format!(
5760                                "subscription '{}' cannot be enabled without a slot_name",
5761                                s.name
5762                            ),
5763                        };
5764                    }
5765                    _ => {}
5766                }
5767                self.snapshot_subscription(&s.name);
5768                self.snapshot_generation_counter();
5769                self.local.generation_counter += 1;
5770                let new_gen = self.local.generation_counter;
5771                use crate::analysis::facts::{
5772                    AlterSubscriptionActionFact, SubscriptionPublicationMode,
5773                };
5774                let mut rename_to = None;
5775                match &s.action {
5776                    AlterSubscriptionActionFact::SetConnection(connection) => {
5777                        if let Some(crate::model::replication::SubscriptionOverlay::Present(
5778                            subscription,
5779                        )) = self.local.subscriptions.get_mut(&s.name)
5780                        {
5781                            subscription.connection = connection.clone();
5782                        }
5783                    }
5784                    AlterSubscriptionActionFact::SetServer(server) => {
5785                        if let Some(crate::model::replication::SubscriptionOverlay::Present(
5786                            subscription,
5787                        )) = self.local.subscriptions.get_mut(&s.name)
5788                        {
5789                            subscription.connection =
5790                                crate::analysis::facts::ConnectionTarget::Server(server.clone());
5791                        }
5792                        self.snapshot_confidence();
5793                        self.local.confidence = Confidence::Tainted;
5794                    }
5795                    AlterSubscriptionActionFact::Publications {
5796                        mode,
5797                        publications,
5798                        params,
5799                    } => {
5800                        let refreshes = Self::subscription_boolean_option(Some(params), "refresh")
5801                            != Some(false);
5802                        if refreshes {
5803                            self.snapshot_confidence();
5804                            self.local.confidence = Confidence::Tainted;
5805                        }
5806                        if let Some(crate::model::replication::SubscriptionOverlay::Present(
5807                            subscription,
5808                        )) = self.local.subscriptions.get_mut(&s.name)
5809                        {
5810                            match mode {
5811                                SubscriptionPublicationMode::Set => {
5812                                    subscription.publications = publications.clone();
5813                                }
5814                                SubscriptionPublicationMode::Add => {
5815                                    subscription
5816                                        .publications
5817                                        .extend(publications.iter().cloned());
5818                                }
5819                                SubscriptionPublicationMode::Drop => {
5820                                    subscription
5821                                        .publications
5822                                        .retain(|existing| !publications.contains(existing));
5823                                }
5824                            }
5825                        }
5826                    }
5827                    AlterSubscriptionActionFact::RefreshPublication(_) => {
5828                        self.snapshot_confidence();
5829                        self.local.confidence = Confidence::Tainted;
5830                    }
5831                    AlterSubscriptionActionFact::SetEnabled(enabled) => {
5832                        if let Some(crate::model::replication::SubscriptionOverlay::Present(
5833                            subscription,
5834                        )) = self.local.subscriptions.get_mut(&s.name)
5835                        {
5836                            subscription.enabled = *enabled;
5837                        }
5838                    }
5839                    AlterSubscriptionActionFact::SetOptions(options) => {
5840                        if let Some(crate::model::replication::SubscriptionOverlay::Present(
5841                            subscription,
5842                        )) = self.local.subscriptions.get_mut(&s.name)
5843                        {
5844                            for option in options {
5845                                Self::set_subscription_option(subscription, option);
5846                                if option.name.eq_ignore_ascii_case("slot_name") {
5847                                    subscription.slot_name =
5848                                        (!option.value.eq_ignore_ascii_case("none"))
5849                                            .then(|| option.value.clone());
5850                                }
5851                            }
5852                        }
5853                    }
5854                    AlterSubscriptionActionFact::Skip(options) => {
5855                        if let Some(crate::model::replication::SubscriptionOverlay::Present(
5856                            subscription,
5857                        )) = self.local.subscriptions.get_mut(&s.name)
5858                        {
5859                            for option in options {
5860                                let mut normalized = option.clone();
5861                                normalized.name = "skip_lsn".to_string();
5862                                Self::set_subscription_option(subscription, &normalized);
5863                            }
5864                        }
5865                    }
5866                    AlterSubscriptionActionFact::OwnerChange(role) => {
5867                        if let Some((owner, known)) = self.role_fact_identity(role) {
5868                            if known {
5869                                if let Some(
5870                                    crate::model::replication::SubscriptionOverlay::Present(
5871                                        subscription,
5872                                    ),
5873                                ) = self.local.subscriptions.get_mut(&s.name)
5874                                {
5875                                    subscription.owner = Some(owner);
5876                                }
5877                            } else {
5878                                self.snapshot_confidence();
5879                                self.local.confidence = Confidence::Tainted;
5880                                if let Some(
5881                                    crate::model::replication::SubscriptionOverlay::Present(
5882                                        subscription,
5883                                    ),
5884                                ) = self.local.subscriptions.get_mut(&s.name)
5885                                {
5886                                    subscription.owner = None;
5887                                }
5888                            }
5889                        } else {
5890                            self.snapshot_confidence();
5891                            self.local.confidence = Confidence::Tainted;
5892                            if let Some(crate::model::replication::SubscriptionOverlay::Present(
5893                                subscription,
5894                            )) = self.local.subscriptions.get_mut(&s.name)
5895                            {
5896                                subscription.owner = None;
5897                            }
5898                        }
5899                    }
5900                    AlterSubscriptionActionFact::Rename { to } => {
5901                        if matches!(
5902                            self.local.subscriptions.get(to),
5903                            Some(crate::model::replication::SubscriptionOverlay::Present(_))
5904                        ) {
5905                            return MutationResult::Conflict {
5906                                reason: format!("subscription '{}' already exists", to),
5907                            };
5908                        }
5909                        rename_to = Some(to.clone());
5910                    }
5911                }
5912
5913                if let Some(crate::model::replication::SubscriptionOverlay::Present(subscription)) =
5914                    self.local.subscriptions.get_mut(&s.name)
5915                {
5916                    subscription.generation = new_gen;
5917                }
5918                if let Some(to) = rename_to {
5919                    self.snapshot_subscription(&to);
5920                    let Some(crate::model::replication::SubscriptionOverlay::Present(
5921                        mut subscription,
5922                    )) = self.local.subscriptions.remove(&s.name)
5923                    else {
5924                        unreachable!("subscription existence checked above");
5925                    };
5926                    subscription.name = to.clone();
5927                    self.local.subscriptions.insert(
5928                        to,
5929                        crate::model::replication::SubscriptionOverlay::Present(subscription),
5930                    );
5931                }
5932                MutationResult::Applied
5933            }
5934            Mutation::DropSubscription(s) => {
5935                let has_slot = match self.local.subscriptions.get(&s.name) {
5936                    Some(crate::model::replication::SubscriptionOverlay::Present(subscription)) => {
5937                        subscription.slot_name.is_some()
5938                    }
5939                    Some(crate::model::replication::SubscriptionOverlay::Dropped) => {
5940                        if !s.if_exists {
5941                            return MutationResult::Conflict {
5942                                reason: format!("subscription '{}' does not exist", s.name),
5943                            };
5944                        }
5945                        return MutationResult::Skipped;
5946                    }
5947                    None if s.if_exists && self.baseline_available => {
5948                        return MutationResult::Skipped;
5949                    }
5950                    None if s.if_exists => {
5951                        self.snapshot_confidence();
5952                        self.local.confidence = Confidence::Tainted;
5953                        return MutationResult::Skipped;
5954                    }
5955                    None => {
5956                        if self.baseline_available {
5957                            return MutationResult::Conflict {
5958                                reason: format!("subscription '{}' does not exist", s.name),
5959                            };
5960                        }
5961                        self.snapshot_confidence();
5962                        self.local.confidence = Confidence::Tainted;
5963                        return MutationResult::Skipped;
5964                    }
5965                };
5966                if has_slot && !self.local.transactions.is_empty() {
5967                    return MutationResult::Conflict {
5968                        reason: format!(
5969                            "subscription '{}' has a replication slot and cannot be dropped inside a transaction",
5970                            s.name
5971                        ),
5972                    };
5973                }
5974                self.snapshot_confidence();
5975                self.local.confidence = Confidence::Tainted;
5976                self.snapshot_subscription(&s.name);
5977                self.local.subscriptions.insert(
5978                    s.name.clone(),
5979                    crate::model::replication::SubscriptionOverlay::Dropped,
5980                );
5981                MutationResult::Applied
5982            }
5983            Mutation::CreateRole(r) => {
5984                let role_id = ObjectId::new("", &r.name);
5985                if matches!(
5986                    self.local.roles.get(&role_id),
5987                    Some(crate::model::role::RoleOverlay::Present(_))
5988                ) {
5989                    return MutationResult::Conflict {
5990                        reason: format!("role '{}' already exists", r.name),
5991                    };
5992                }
5993                self.snapshot_role(&role_id);
5994                self.snapshot_generation_counter();
5995                self.local.generation_counter += 1;
5996                let _generation = self.local.generation_counter;
5997
5998                self.local.roles.insert(
5999                    role_id.clone(),
6000                    crate::model::role::RoleOverlay::Present(crate::model::role::RoleState {
6001                        id: role_id,
6002                        can_login: r.can_login,
6003                        is_superuser: false,
6004                        member_of: Vec::new(),
6005                        can_set_role_to: Vec::new(),
6006                        granted_privileges: Vec::new(),
6007                    }),
6008                );
6009                MutationResult::Applied
6010            }
6011            Mutation::AlterRole(r) => {
6012                if let Some(role_id) = Self::resolve_role_name(
6013                    &r.name,
6014                    &self.local.current_role,
6015                    &self.local.session_role,
6016                ) {
6017                    self.snapshot_role(&role_id);
6018                    if !self.local.roles.contains_key(&role_id) {
6019                        self.local.confidence = Confidence::Tainted;
6020                        return MutationResult::Skipped;
6021                    }
6022                    self.snapshot_generation_counter();
6023                    self.local.generation_counter += 1;
6024                    let _new_gen = self.local.generation_counter;
6025
6026                    MutationResult::Applied
6027                } else {
6028                    MutationResult::Skipped
6029                }
6030            }
6031            Mutation::DropRole(r) => {
6032                for name in &r.names {
6033                    if let Some(role_id) = Self::resolve_role_name(
6034                        &crate::analysis::facts::RoleFact::Named {
6035                            name: name.clone(),
6036                            via_legacy_group_syntax: false,
6037                        },
6038                        &self.local.current_role,
6039                        &self.local.session_role,
6040                    ) {
6041                        self.snapshot_role(&role_id);
6042                        if !r.if_exists
6043                            && !matches!(
6044                                self.local.roles.get(&role_id),
6045                                Some(crate::model::role::RoleOverlay::Present(_))
6046                            )
6047                        {
6048                            return MutationResult::Conflict {
6049                                reason: format!("role '{}' does not exist", name),
6050                            };
6051                        }
6052                        self.local
6053                            .roles
6054                            .insert(role_id, crate::model::role::RoleOverlay::Dropped);
6055                    }
6056                }
6057                MutationResult::Applied
6058            }
6059            Mutation::Grant(grant) => {
6060                let privileges = Self::resolve_grant_privileges(&grant.privileges);
6061                let grantees = &grant.grantees;
6062                match &grant.target {
6063                    crate::analysis::mutations::ResolvedGrantTarget::Tables(ids) => {
6064                        for id in ids {
6065                            self.apply_grant_to_relation(id, &privileges, grantees);
6066                        }
6067                    }
6068                    crate::analysis::mutations::ResolvedGrantTarget::AllTablesInSchema(schemas) => {
6069                        let target_ids: Vec<ObjectId> = self
6070                            .local
6071                            .relations
6072                            .keys()
6073                            .filter(|id| schemas.contains(&id.schema))
6074                            .cloned()
6075                            .collect();
6076                        for id in &target_ids {
6077                            self.apply_grant_to_relation(id, &privileges, grantees);
6078                        }
6079                    }
6080                }
6081                MutationResult::Applied
6082            }
6083            Mutation::Revoke(revoke) => {
6084                let privileges = Self::resolve_grant_privileges(&revoke.privileges);
6085                let revokees = &revoke.revokees;
6086                match &revoke.target {
6087                    crate::analysis::mutations::ResolvedGrantTarget::Tables(ids) => {
6088                        for id in ids {
6089                            self.apply_revoke_to_relation(id, &privileges, revokees);
6090                        }
6091                    }
6092                    crate::analysis::mutations::ResolvedGrantTarget::AllTablesInSchema(schemas) => {
6093                        let target_ids: Vec<ObjectId> = self
6094                            .local
6095                            .relations
6096                            .keys()
6097                            .filter(|id| schemas.contains(&id.schema))
6098                            .cloned()
6099                            .collect();
6100                        for id in &target_ids {
6101                            self.apply_revoke_to_relation(id, &privileges, revokees);
6102                        }
6103                    }
6104                }
6105                MutationResult::Applied
6106            }
6107            Mutation::CreateDatabase(_) => MutationResult::Applied,
6108            Mutation::AlterDatabase(_) => MutationResult::Applied,
6109            Mutation::DropDatabase(_) => MutationResult::Applied,
6110            Mutation::Vacuum { .. } => MutationResult::Applied,
6111        }
6112    }
6113
6114    fn snapshot_relation(&mut self, id: &ObjectId) {
6115        if let Some(frame) = self.local.transactions.last_mut() {
6116            let previous = self.local.relations.get(id).cloned();
6117            frame.undo_log.push(StateChange::RelationSnapshot {
6118                id: id.clone(),
6119                previous: Box::new(previous),
6120            });
6121        }
6122    }
6123
6124    fn snapshot_schema(&mut self, name: &str) {
6125        if let Some(frame) = self.local.transactions.last_mut() {
6126            frame.undo_log.push(StateChange::SchemaSnapshot {
6127                name: name.to_string(),
6128                previous: self.local.schemas.get(name).cloned(),
6129            });
6130        }
6131    }
6132
6133    fn snapshot_namespace(&mut self) {
6134        if let Some(frame) = self.local.transactions.last_mut() {
6135            frame.undo_log.push(StateChange::NamespaceSnapshot(Box::new(
6136                NamespaceSnapshot {
6137                    schemas: self.local.schemas.clone(),
6138                    relations: self.local.relations.clone(),
6139                    types: self.local.types.clone(),
6140                    functions: self.local.functions.clone(),
6141                    sequences: self.local.sequences.clone(),
6142                    publications: self.local.publications.clone(),
6143                    triggers: self.local.triggers.clone(),
6144                    constraints: self.local.constraints.clone(),
6145                    graph: self.local.graph.edges.clone(),
6146                    pending_validation: self.local.pending_validation.clone(),
6147                    baseline_relations: self.baseline_relations.clone(),
6148                    baseline_indexes: self.baseline_indexes.clone(),
6149                    baseline_foreign_keys: self.baseline_foreign_keys.clone(),
6150                    baseline_fk_dependencies: self.baseline_fk_dependencies.clone(),
6151                    baseline_sequences: self.baseline_sequences.clone(),
6152                },
6153            )));
6154        }
6155    }
6156
6157    fn snapshot_type(&mut self, id: &ObjectId) {
6158        if let Some(frame) = self.local.transactions.last_mut() {
6159            let previous = self.local.types.get(id).cloned();
6160            frame.undo_log.push(StateChange::TypeSnapshot {
6161                id: id.clone(),
6162                previous,
6163            });
6164        }
6165    }
6166
6167    fn snapshot_sequence(&mut self, id: &ObjectId) {
6168        if let Some(frame) = self.local.transactions.last_mut() {
6169            let previous = self.local.sequences.get(id).cloned();
6170            frame.undo_log.push(StateChange::SequenceSnapshot {
6171                id: id.clone(),
6172                previous,
6173            });
6174        }
6175    }
6176
6177    fn move_function(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
6178        self.snapshot_function(old_id);
6179        self.snapshot_function(new_id);
6180        if let Some(crate::model::function::FunctionOverlay::Present(mut function)) =
6181            self.local.functions.remove(old_id)
6182        {
6183            function.id = new_id.clone();
6184            self.local.functions.insert(
6185                new_id.clone(),
6186                crate::model::function::FunctionOverlay::Present(function),
6187            );
6188        }
6189
6190        self.snapshot_graph_full();
6191        self.local.graph.propagate_rename(old_id, new_id);
6192        self.local.graph.edges.push(DependencyEdge::new(
6193            old_id.clone(),
6194            new_id.clone(),
6195            DependencyKind::RenameTo,
6196        ));
6197    }
6198
6199    fn snapshot_function(&mut self, id: &ObjectId) {
6200        if let Some(frame) = self.local.transactions.last_mut() {
6201            let previous = self.local.functions.get(id).cloned();
6202            frame.undo_log.push(StateChange::FunctionSnapshot {
6203                id: id.clone(),
6204                previous,
6205            });
6206        }
6207    }
6208
6209    fn snapshot_publication(&mut self, name: &str) {
6210        if let Some(frame) = self.local.transactions.last_mut() {
6211            let previous = self.local.publications.get(name).cloned();
6212            frame.undo_log.push(StateChange::PublicationSnapshot {
6213                id: ObjectId::new("", name),
6214                previous,
6215            });
6216        }
6217    }
6218
6219    fn snapshot_subscription(&mut self, name: &str) {
6220        if let Some(frame) = self.local.transactions.last_mut() {
6221            let previous = self.local.subscriptions.get(name).cloned();
6222            frame.undo_log.push(StateChange::SubscriptionSnapshot {
6223                id: ObjectId::new("", name),
6224                previous,
6225            });
6226        }
6227    }
6228
6229    fn snapshot_role(&mut self, id: &ObjectId) {
6230        if let Some(frame) = self.local.transactions.last_mut() {
6231            let previous = self.local.roles.get(id).cloned();
6232            frame.undo_log.push(StateChange::RoleSnapshot {
6233                id: id.clone(),
6234                previous,
6235            });
6236        }
6237    }
6238
6239    fn snapshot_trigger(&mut self, id: &ObjectId) {
6240        if let Some(frame) = self.local.transactions.last_mut() {
6241            let previous = self.local.triggers.get(id).cloned();
6242            frame.undo_log.push(StateChange::TriggerSnapshot {
6243                id: id.clone(),
6244                previous,
6245            });
6246        }
6247    }
6248
6249    fn snapshot_constraint(&mut self, table_id: &ObjectId, name: &str) {
6250        if let Some(frame) = self.local.transactions.last_mut() {
6251            let key = (table_id.clone(), name.to_string());
6252            let previous = self.local.constraints.get(&key).cloned();
6253            frame.undo_log.push(StateChange::ConstraintSnapshot {
6254                table_id: table_id.clone(),
6255                name: name.to_string(),
6256                previous,
6257            });
6258        }
6259    }
6260
6261    fn snapshot_role_context(&mut self) {
6262        if let Some(frame) = self.local.transactions.last_mut() {
6263            frame.undo_log.push(StateChange::RoleContextSnapshot {
6264                current_role: self.local.current_role.clone(),
6265                current_role_known: self.local.current_role_known,
6266                persistent_current_role: self.local.persistent_current_role.clone(),
6267                persistent_current_role_known: self.local.persistent_current_role_known,
6268                session_role: self.local.session_role.clone(),
6269                session_role_known: self.local.session_role_known,
6270                persistent_session_role: self.local.persistent_session_role.clone(),
6271                persistent_session_role_known: self.local.persistent_session_role_known,
6272            });
6273        }
6274    }
6275
6276    fn snapshot_search_path(&mut self) {
6277        if let Some(frame) = self.local.transactions.last_mut() {
6278            frame.undo_log.push(StateChange::SearchPathSnapshot {
6279                previous: self.local.search_path.clone(),
6280                previous_template: self.local.search_path_template.clone(),
6281                previous_session_template: self.local.session_search_path_template.clone(),
6282            });
6283        }
6284    }
6285
6286    fn snapshot_timeout_settings(&mut self) {
6287        if let Some(frame) = self.local.transactions.last_mut() {
6288            frame.undo_log.push(StateChange::TimeoutSettingsSnapshot {
6289                lock_timeout: self.local.lock_timeout.clone(),
6290                statement_timeout: self.local.statement_timeout.clone(),
6291            });
6292        }
6293    }
6294
6295    fn snapshot_generation_counter(&mut self) {
6296        if let Some(frame) = self.local.transactions.last_mut() {
6297            frame.undo_log.push(StateChange::GenerationCounterSnapshot {
6298                previous: self.local.generation_counter,
6299            });
6300        }
6301    }
6302
6303    #[allow(dead_code)]
6304    fn snapshot_pending_validation(&mut self) {
6305        if let Some(frame) = self.local.transactions.last_mut() {
6306            frame.undo_log.push(StateChange::PendingValidationSnapshot {
6307                previous: self.local.pending_validation.clone(),
6308            });
6309        }
6310    }
6311
6312    fn snapshot_confidence(&mut self) {
6313        if let Some(frame) = self.local.transactions.last_mut() {
6314            frame.undo_log.push(StateChange::ConfidenceSnapshot {
6315                previous: self.local.confidence.clone(),
6316            });
6317        }
6318    }
6319
6320    fn snapshot_graph(&mut self) {
6321        if let Some(frame) = self.local.transactions.last_mut() {
6322            frame.undo_log.push(StateChange::GraphLengthMarker {
6323                len: self.local.graph.edges.len(),
6324            });
6325        }
6326    }
6327
6328    fn snapshot_graph_full(&mut self) {
6329        if let Some(frame) = self.local.transactions.last_mut() {
6330            frame.undo_log.push(StateChange::GraphSnapshot {
6331                previous: self.local.graph.edges.clone(),
6332            });
6333        }
6334    }
6335
6336    fn rollback_frame(&mut self, mut frame: TransactionFrame) {
6337        self.rollback_undo_log(std::mem::take(&mut frame.undo_log));
6338    }
6339
6340    fn rollback_undo_log(&mut self, mut undo_log: Vec<StateChange>) {
6341        while let Some(change) = undo_log.pop() {
6342            match change {
6343                StateChange::SchemaSnapshot { name, previous } => match previous {
6344                    Some(overlay) => {
6345                        self.local.schemas.insert(name, overlay);
6346                    }
6347                    None => {
6348                        self.local.schemas.remove(&name);
6349                    }
6350                },
6351                StateChange::NamespaceSnapshot(snapshot) => {
6352                    self.local.schemas = snapshot.schemas;
6353                    self.local.relations = snapshot.relations;
6354                    self.local.types = snapshot.types;
6355                    self.local.functions = snapshot.functions;
6356                    self.local.sequences = snapshot.sequences;
6357                    self.local.publications = snapshot.publications;
6358                    self.local.triggers = snapshot.triggers;
6359                    self.local.constraints = snapshot.constraints;
6360                    self.local.graph.edges = snapshot.graph;
6361                    self.local.pending_validation = snapshot.pending_validation;
6362                    self.baseline_relations = snapshot.baseline_relations;
6363                    self.baseline_indexes = snapshot.baseline_indexes;
6364                    self.baseline_foreign_keys = snapshot.baseline_foreign_keys;
6365                    self.baseline_fk_dependencies = snapshot.baseline_fk_dependencies;
6366                    self.baseline_sequences = snapshot.baseline_sequences;
6367                }
6368                StateChange::RelationSnapshot { id, previous } => {
6369                    if let Some(prev) = *previous {
6370                        self.local.relations.insert(id, prev);
6371                    } else {
6372                        self.local.relations.remove(&id);
6373                    }
6374                }
6375                StateChange::TypeSnapshot { id, previous } => {
6376                    if let Some(prev) = previous {
6377                        self.local.types.insert(id, prev);
6378                    } else {
6379                        self.local.types.remove(&id);
6380                    }
6381                }
6382                StateChange::SequenceSnapshot { id, previous } => {
6383                    if let Some(prev) = previous {
6384                        self.local.sequences.insert(id, prev);
6385                    } else {
6386                        self.local.sequences.remove(&id);
6387                    }
6388                }
6389                StateChange::FunctionSnapshot { id, previous } => {
6390                    if let Some(prev) = previous {
6391                        self.local.functions.insert(id, prev);
6392                    } else {
6393                        self.local.functions.remove(&id);
6394                    }
6395                }
6396                StateChange::PublicationSnapshot { id, previous } => {
6397                    if let Some(prev) = previous {
6398                        self.local.publications.insert(id.name, prev);
6399                    } else {
6400                        self.local.publications.remove(&id.name);
6401                    }
6402                }
6403                StateChange::SubscriptionSnapshot { id, previous } => {
6404                    if let Some(prev) = previous {
6405                        self.local.subscriptions.insert(id.name, prev);
6406                    } else {
6407                        self.local.subscriptions.remove(&id.name);
6408                    }
6409                }
6410                StateChange::RoleSnapshot { id, previous } => {
6411                    if let Some(prev) = previous {
6412                        self.local.roles.insert(id, prev);
6413                    } else {
6414                        self.local.roles.remove(&id);
6415                    }
6416                }
6417                StateChange::TriggerSnapshot { id, previous } => {
6418                    if let Some(prev) = previous {
6419                        self.local.triggers.insert(id, prev);
6420                    } else {
6421                        self.local.triggers.remove(&id);
6422                    }
6423                }
6424                StateChange::ConstraintSnapshot {
6425                    table_id,
6426                    name,
6427                    previous,
6428                } => {
6429                    let key = (table_id, name);
6430                    if let Some(previous) = previous {
6431                        self.local.constraints.insert(key, previous);
6432                    } else {
6433                        self.local.constraints.remove(&key);
6434                    }
6435                }
6436                StateChange::GraphLengthMarker { len } => {
6437                    self.local.graph.edges.truncate(len);
6438                }
6439                StateChange::GraphSnapshot { previous } => {
6440                    self.local.graph.edges = previous;
6441                }
6442                StateChange::RoleContextSnapshot {
6443                    current_role,
6444                    current_role_known,
6445                    persistent_current_role,
6446                    persistent_current_role_known,
6447                    session_role,
6448                    session_role_known,
6449                    persistent_session_role,
6450                    persistent_session_role_known,
6451                } => {
6452                    self.local.current_role = current_role;
6453                    self.local.current_role_known = current_role_known;
6454                    self.local.persistent_current_role = persistent_current_role;
6455                    self.local.persistent_current_role_known = persistent_current_role_known;
6456                    self.local.session_role = session_role;
6457                    self.local.session_role_known = session_role_known;
6458                    self.local.persistent_session_role = persistent_session_role;
6459                    self.local.persistent_session_role_known = persistent_session_role_known;
6460                }
6461                StateChange::SearchPathSnapshot {
6462                    previous,
6463                    previous_template,
6464                    previous_session_template,
6465                } => {
6466                    self.local.search_path = previous;
6467                    self.local.search_path_template = previous_template;
6468                    self.local.session_search_path_template = previous_session_template;
6469                }
6470                StateChange::TimeoutSettingsSnapshot {
6471                    lock_timeout,
6472                    statement_timeout,
6473                } => {
6474                    self.local.lock_timeout = lock_timeout;
6475                    self.local.statement_timeout = statement_timeout;
6476                }
6477                StateChange::GenerationCounterSnapshot { previous } => {
6478                    self.local.generation_counter = previous;
6479                }
6480                StateChange::PendingValidationSnapshot { previous } => {
6481                    self.local.pending_validation = previous;
6482                }
6483                StateChange::ConfidenceSnapshot { previous } => {
6484                    self.local.confidence = previous;
6485                }
6486            }
6487        }
6488    }
6489}