Skip to main content

safe_migrate/analysis/
state.rs

1use crate::analysis::graph::{DependencyEdge, DependencyGraph, DependencyKind};
2use crate::analysis::mutations::Mutation;
3use crate::analysis::settings::ScopedSetting;
4use crate::analysis::transaction::{NamespaceSnapshot, StateChange, TransactionFrame};
5use crate::ast::identifiers::ObjectId;
6use crate::db::cache::DbCache;
7use crate::model::constraint::ConstraintState;
8use crate::model::function::FunctionOverlay;
9pub use crate::model::relation::RelationOverlay;
10use crate::model::relation::{Persistence, Privilege, RelationKind};
11use crate::model::schema::SchemaOverlay;
12use crate::model::sequence::SequenceOverlay;
13use crate::model::trigger::TriggerOverlay;
14use crate::model::types::{TypeKind, TypeOverlay, TypeState};
15use std::collections::{HashMap, HashSet};
16use std::hash::Hash;
17
18mod apply_misc;
19mod apply_policy_trigger;
20mod apply_relation;
21mod apply_replication;
22mod apply_role;
23mod apply_routine;
24mod apply_schema;
25mod apply_sequence;
26mod apply_settings;
27mod apply_transaction;
28mod apply_type;
29mod apply_view_index;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum Confidence {
33    Exact,
34    Tainted,
35}
36
37#[derive(Debug, PartialEq, Eq)]
38pub enum MutationResult {
39    Applied,
40    Skipped,
41    /// PostgreSQL did not execute this statement because an earlier statement
42    /// aborted the active transaction.
43    NotExecuted,
44    Conflict {
45        reason: String,
46    },
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50enum ObjectLookup {
51    Present,
52    WrongKind,
53    Tombstone,
54    AuthoritativelyAbsent,
55    Unknown,
56}
57
58fn sync_present_map<K, V, O, F>(target: &mut HashMap<K, V>, source: &HashMap<K, O>, present: F)
59where
60    K: Clone + Eq + Hash,
61    V: Clone + PartialEq,
62    F: for<'a> Fn(&'a O) -> Option<&'a V> + Copy,
63{
64    target.retain(|key, value| {
65        let Some(current) = source.get(key).and_then(present) else {
66            return false;
67        };
68        if value != current {
69            value.clone_from(current);
70        }
71        true
72    });
73    target.reserve(source.len().saturating_sub(target.len()));
74    for (key, overlay) in source {
75        if !target.contains_key(key)
76            && let Some(value) = present(overlay)
77        {
78            target.insert(key.clone(), value.clone());
79        }
80    }
81}
82
83#[derive(Debug, Default, Clone)]
84pub struct CascadeResult {
85    pub dropped_relations: HashSet<ObjectId>,
86    pub dropped_indexes: HashSet<ObjectId>,
87    pub dropped_constraints: HashSet<(ObjectId, String)>,
88}
89
90#[derive(Clone)]
91pub struct LocalState {
92    pub schemas: HashMap<String, SchemaOverlay>,
93    pub relations: HashMap<ObjectId, RelationOverlay>,
94    pub types: HashMap<ObjectId, TypeOverlay>,
95    pub functions: HashMap<ObjectId, crate::model::function::FunctionOverlay>,
96    pub sequences: HashMap<ObjectId, SequenceOverlay>,
97    pub publications: HashMap<String, crate::model::replication::PublicationOverlay>,
98    pub subscriptions: HashMap<String, crate::model::replication::SubscriptionOverlay>,
99    pub roles: HashMap<ObjectId, crate::model::role::RoleOverlay>,
100    pub triggers: HashMap<ObjectId, TriggerOverlay>,
101    pub constraints: HashMap<(ObjectId, String), ConstraintState>,
102    pub graph: DependencyGraph,
103    pub search_path: Vec<String>,
104    pub default_search_path: Vec<String>,
105    pub search_path_template: Vec<String>,
106    pub session_search_path_template: Vec<String>,
107    pub default_search_path_template: Vec<String>,
108    pub lock_timeout: ScopedSetting<Option<u64>>,
109    pub statement_timeout: ScopedSetting<Option<u64>>,
110    /// Role currently active for this session context (updated by SET ROLE /
111    /// SET SESSION AUTHORIZATION). Begins equal to `session_role`.
112    pub current_role: String,
113    /// Whether `current_role` is statically known. False when no synchronized cache was
114    /// loaded and no SET ROLE statement has been processed yet.
115    pub current_role_known: bool,
116    /// Effective role setting that survives transaction commit. A LOCAL role
117    /// change updates `current_role` without changing this value.
118    pub persistent_current_role: String,
119    pub persistent_current_role_known: bool,
120    /// The session-level role as captured from the cache's `source_role`. This
121    /// is the value `SET ROLE NONE` / `SET SESSION AUTHORIZATION DEFAULT`
122    /// reverts to.
123    pub session_role: String,
124    /// Whether `session_role` is statically known (mirrors `current_role_known`
125    /// at baseline; a SET SESSION AUTHORIZATION updates this too).
126    pub session_role_known: bool,
127    /// Session authorization that survives transaction commit.
128    pub persistent_session_role: String,
129    pub persistent_session_role_known: bool,
130    /// Login identity restored by `SET SESSION AUTHORIZATION DEFAULT`.
131    pub authenticated_role: String,
132    pub authenticated_role_known: bool,
133    /// Whether the cache contains a complete PostgreSQL role catalog.
134    pub roles_known: bool,
135    pub confidence: Confidence,
136    pub transactions: Vec<TransactionFrame>,
137    pub transaction_aborted: bool,
138    pub pending_validation: HashSet<(ObjectId, String)>,
139    pub generation_counter: u64,
140}
141
142#[derive(Clone, Debug, Default, PartialEq)]
143pub struct PreState {
144    pub relations: HashMap<ObjectId, crate::model::relation::RelationState>,
145    pub functions: HashMap<ObjectId, crate::model::function::FunctionState>,
146    pub roles: HashMap<ObjectId, crate::model::role::RoleState>,
147    pub publications: HashMap<String, crate::model::replication::PublicationState>,
148    pub subscriptions: HashMap<String, crate::model::replication::SubscriptionState>,
149    pub sequences: HashMap<ObjectId, crate::model::sequence::SequenceState>,
150    pub types: HashMap<ObjectId, crate::model::types::TypeState>,
151    pub indexes: Vec<crate::analysis::graph::DependencyEdge>,
152    pub baseline_foreign_keys: HashSet<(ObjectId, String)>,
153}
154
155#[cfg(test)]
156mod pre_state_tests {
157    use super::*;
158
159    #[test]
160    fn incremental_capture_matches_a_fresh_public_pre_state() {
161        let mut state = AnalysisState::new(DbCache::new());
162        let table_id = ObjectId::new("public", "capture_table");
163        state.local.relations.insert(
164            table_id.clone(),
165            RelationOverlay::Present(crate::model::relation::RelationState::new(
166                table_id.clone(),
167                ObjectId::new("", "postgres"),
168                1,
169                Some(1),
170                RelationKind::Table,
171                Persistence::Permanent,
172                0,
173            )),
174        );
175
176        let mut reused = PreState::default();
177        state.capture_pre_state_into(&mut reused);
178        assert_eq!(reused, state.capture_pre_state());
179
180        let Some(RelationOverlay::Present(relation)) = state.local.relations.get_mut(&table_id)
181        else {
182            panic!("test relation must remain present");
183        };
184        relation.estimated_rows = Some(2);
185        let index_id = ObjectId::new("public", "capture_table_idx");
186        state.local.graph.add_edge(DependencyEdge::new(
187            index_id,
188            table_id.clone(),
189            DependencyKind::IndexOnRelation {
190                using_method: Some("btree".into()),
191                has_predicate: false,
192                is_concurrent: false,
193                is_unique: false,
194                eligibility_known: true,
195            },
196        ));
197        state.capture_pre_state_into(&mut reused);
198        assert_eq!(reused, state.capture_pre_state());
199        assert_eq!(reused.relations[&table_id].estimated_rows, Some(2));
200        assert_eq!(reused.indexes.len(), 1);
201
202        state
203            .local
204            .relations
205            .insert(table_id.clone(), RelationOverlay::Dropped);
206        state.local.graph.replace_edges(Vec::new());
207        state.capture_pre_state_into(&mut reused);
208        assert_eq!(reused, state.capture_pre_state());
209        assert!(!reused.relations.contains_key(&table_id));
210        assert!(reused.indexes.is_empty());
211    }
212}
213
214#[derive(Clone)]
215pub struct AnalysisState {
216    pub pg_version_num: Option<u32>,
217    /// Whether the initial cache was loaded from a real cache file. An empty
218    /// cache can be a valid baseline for an empty database, so availability
219    /// must not be inferred from the number of modeled objects.
220    pub baseline_available: bool,
221    /// `None` means the cache covered all non-system schemas. A populated set
222    /// records an explicitly scoped sync, for which objects outside the set
223    /// are unknown rather than known absent.
224    pub baseline_schemas: Option<HashSet<String>>,
225    pub baseline_relations: HashSet<ObjectId>,
226    pub baseline_indexes: HashSet<ObjectId>,
227    pub baseline_foreign_keys: HashSet<(ObjectId, String)>,
228    pub baseline_fk_dependencies: HashSet<ObjectId>,
229    pub baseline_sequences: HashSet<ObjectId>,
230    pub local: LocalState,
231}
232
233impl AnalysisState {
234    fn trigger_key(table_id: &ObjectId, name: &str) -> ObjectId {
235        // PostgreSQL identifiers cannot contain NUL, so this is an unambiguous
236        // internal composite key while keeping the public cache representation
237        // as the trigger's actual name.
238        ObjectId::new(&table_id.schema, format!("{}\0{name}", table_id.name))
239    }
240
241    fn publication_object_key(
242        &self,
243        object: &crate::analysis::facts::PublicationObjectFact,
244    ) -> String {
245        match object {
246            crate::analysis::facts::PublicationObjectFact::Table { name, .. } => {
247                format!("table\0{}", self.resolve_relation_id(name))
248            }
249            crate::analysis::facts::PublicationObjectFact::SchemaTables { schema, .. } => {
250                format!("schema\0{schema}")
251            }
252            crate::analysis::facts::PublicationObjectFact::CurrentSchemaShorthand => {
253                format!(
254                    "schema\0{}",
255                    self.local
256                        .search_path
257                        .first()
258                        .map(String::as_str)
259                        .unwrap_or("public")
260                )
261            }
262            crate::analysis::facts::PublicationObjectFact::Unknown => "unknown".to_string(),
263        }
264    }
265
266    fn replace_publication_edges(
267        &mut self,
268        publication_name: &str,
269        scope: &crate::analysis::facts::PublicationScope,
270    ) {
271        self.snapshot_graph_full();
272        self.local.graph.retain_edges(|edge| {
273            !matches!(
274                &edge.kind,
275                DependencyKind::PublicationIncludes { publication_name: name }
276                    if name == publication_name
277            )
278        });
279        if let crate::analysis::facts::PublicationScope::Explicit(objects) = scope {
280            for object in objects {
281                if let crate::analysis::facts::PublicationObjectFact::Table { name, .. } = object {
282                    self.local.graph.add_edge(DependencyEdge::new(
283                        self.resolve_relation_id(name),
284                        ObjectId::new("public", publication_name),
285                        DependencyKind::PublicationIncludes {
286                            publication_name: publication_name.to_string(),
287                        },
288                    ));
289                }
290            }
291        }
292    }
293
294    fn validate_publication_scope(
295        &mut self,
296        scope: &crate::analysis::facts::PublicationScope,
297    ) -> Result<(), String> {
298        let crate::analysis::facts::PublicationScope::Explicit(objects) = scope else {
299            return Ok(());
300        };
301        let mut object_keys = HashSet::new();
302        for object in objects {
303            if !object_keys.insert(self.publication_object_key(object)) {
304                return Err("publication contains the same object more than once".to_string());
305            }
306            match object {
307                crate::analysis::facts::PublicationObjectFact::Table { name, columns, .. } => {
308                    let id = self.resolve_relation_id(name);
309                    match self.local.relations.get(&id) {
310                        Some(RelationOverlay::Present(relation))
311                            if relation.kind == RelationKind::Table
312                                && relation.persistence == Persistence::Permanent =>
313                        {
314                            if let Some(columns) = columns {
315                                let mut seen = HashSet::new();
316                                for column in columns {
317                                    if !seen.insert(column) {
318                                        return Err(format!(
319                                            "publication lists column '{}' more than once for '{}'",
320                                            column, id
321                                        ));
322                                    }
323                                    if !relation.has_column(column) {
324                                        return Err(format!(
325                                            "publication column '{}.{}' does not exist",
326                                            id, column
327                                        ));
328                                    }
329                                }
330                            }
331                        }
332                        Some(RelationOverlay::Present(_)) => {
333                            return Err(format!(
334                                "publication target '{}' is not a permanent table",
335                                id
336                            ));
337                        }
338                        Some(RelationOverlay::Dropped) => {
339                            return Err(format!("publication table '{}' does not exist", id));
340                        }
341                        None if self.baseline_available && self.baseline_covers_object(&id) => {
342                            return Err(format!("publication table '{}' does not exist", id));
343                        }
344                        None => {
345                            self.snapshot_confidence();
346                            self.local.confidence = Confidence::Tainted;
347                        }
348                    }
349                }
350                crate::analysis::facts::PublicationObjectFact::SchemaTables { schema, .. } => {
351                    if !self.schema_is_present(schema) {
352                        if self.schema_absence_is_authoritative(schema) {
353                            return Err(format!("publication schema '{}' does not exist", schema));
354                        }
355                        self.snapshot_confidence();
356                        self.local.confidence = Confidence::Tainted;
357                    }
358                }
359                crate::analysis::facts::PublicationObjectFact::CurrentSchemaShorthand
360                | crate::analysis::facts::PublicationObjectFact::Unknown => {
361                    self.snapshot_confidence();
362                    self.local.confidence = Confidence::Tainted;
363                }
364            }
365        }
366        Ok(())
367    }
368
369    fn publication_scope_needs_inheritance_knowledge(
370        &self,
371        scope: &crate::analysis::facts::PublicationScope,
372    ) -> bool {
373        match scope {
374            // FOR ALL TABLES necessarily depends on the complete catalog and
375            // future table/inheritance state, neither of which Cache V6 stores.
376            crate::analysis::facts::PublicationScope::AllTables { .. } => true,
377            crate::analysis::facts::PublicationScope::Explicit(objects) => {
378                objects.iter().any(|object| match object {
379                    crate::analysis::facts::PublicationObjectFact::Table {
380                        name,
381                        only,
382                        include_partitions,
383                        ..
384                    } if !only || *include_partitions => {
385                        let id = self.resolve_relation_id(name);
386                        !matches!(
387                            self.local.relations.get(&id),
388                            Some(RelationOverlay::Present(relation)) if relation.generation > 0
389                        ) || self.local.graph.edges().iter().any(|edge| {
390                            matches!(edge.kind, DependencyKind::PartitionOf)
391                                && self.local.graph.resolve_rename(&edge.referenced)
392                                    == self.local.graph.resolve_rename(&id)
393                        })
394                    }
395                    // Schema-wide and current-schema shorthand scopes also
396                    // include inherited/partitioned descendants.
397                    crate::analysis::facts::PublicationObjectFact::SchemaTables { .. }
398                    | crate::analysis::facts::PublicationObjectFact::CurrentSchemaShorthand => true,
399                    crate::analysis::facts::PublicationObjectFact::Unknown => false,
400                    _ => false,
401                })
402            }
403        }
404    }
405
406    fn taint_inheritance_sensitive_publication_scope(
407        &mut self,
408        scope: &crate::analysis::facts::PublicationScope,
409    ) {
410        if self.publication_scope_needs_inheritance_knowledge(scope) {
411            self.snapshot_confidence();
412            self.local.confidence = Confidence::Tainted;
413        }
414    }
415
416    fn subscription_option<'a>(
417        params: Option<&'a [crate::analysis::facts::AttributeFact]>,
418        name: &str,
419    ) -> Option<&'a str> {
420        params?
421            .iter()
422            .rev()
423            .find(|param| param.name.eq_ignore_ascii_case(name))
424            .map(|param| param.value.as_str())
425    }
426
427    fn postgres_boolean(value: &str) -> Option<bool> {
428        let value = value.trim().to_ascii_lowercase();
429        match value.as_str() {
430            "1" => return Some(true),
431            "0" => return Some(false),
432            "" => return None,
433            _ => {}
434        }
435
436        let mut matched = None;
437        for (spelling, parsed) in [
438            ("true", true),
439            ("yes", true),
440            ("on", true),
441            ("false", false),
442            ("no", false),
443            ("off", false),
444        ] {
445            if spelling.starts_with(&value) {
446                if matched.is_some() {
447                    return None;
448                }
449                matched = Some(parsed);
450            }
451        }
452        matched
453    }
454
455    fn subscription_boolean_option(
456        params: Option<&[crate::analysis::facts::AttributeFact]>,
457        name: &str,
458    ) -> Option<bool> {
459        Self::subscription_option(params, name).and_then(Self::postgres_boolean)
460    }
461
462    fn validate_subscription_boolean_options(
463        params: Option<&[crate::analysis::facts::AttributeFact]>,
464        names: &[&str],
465    ) -> Result<(), String> {
466        let Some(params) = params else {
467            return Ok(());
468        };
469        for option in params {
470            if names
471                .iter()
472                .any(|name| option.name.eq_ignore_ascii_case(name))
473                && Self::postgres_boolean(&option.value).is_none()
474            {
475                return Err(format!(
476                    "subscription option '{}' requires a PostgreSQL boolean value",
477                    option.name
478                ));
479            }
480        }
481        Ok(())
482    }
483
484    fn set_subscription_option(
485        subscription: &mut crate::model::replication::SubscriptionState,
486        option: &crate::analysis::facts::AttributeFact,
487    ) {
488        let params = subscription.params.get_or_insert_with(Vec::new);
489        params.retain(|existing| !existing.name.eq_ignore_ascii_case(&option.name));
490        params.push(option.clone());
491    }
492
493    pub fn new(cache: DbCache) -> Self {
494        Self::with_baseline(cache, true)
495    }
496
497    pub fn with_baseline(cache: DbCache, baseline_available: bool) -> Self {
498        let source_lock_timeout =
499            baseline_available.then_some(cache.metadata.source_lock_timeout_ms);
500        let source_statement_timeout =
501            baseline_available.then_some(cache.metadata.source_statement_timeout_ms);
502        let default_search_path = cache.search_path.clone();
503        let default_search_path_template = if cache.metadata.schemas.is_none() {
504            cache
505                .metadata
506                .source_search_path
507                .clone()
508                .unwrap_or_else(|| default_search_path.clone())
509        } else {
510            default_search_path.clone()
511        };
512        let current_role_known = cache.metadata.source_role.is_some();
513        let current_role = cache
514            .metadata
515            .source_role
516            .clone()
517            .unwrap_or_else(|| "postgres".to_string());
518        let session_role_known = cache.metadata.source_session_role.is_some();
519        let session_role = cache
520            .metadata
521            .source_session_role
522            .clone()
523            .unwrap_or_else(|| current_role.clone());
524        let authenticated_role = session_role.clone();
525        let authenticated_role_known = session_role_known;
526        let persistent_current_role = current_role.clone();
527        let persistent_current_role_known = current_role_known;
528        let persistent_session_role = session_role.clone();
529        let persistent_session_role_known = session_role_known;
530        let roles_known = cache.metadata.source_session_role.is_some();
531        let baseline_schemas: Option<HashSet<String>> = cache
532            .metadata
533            .schemas
534            .as_ref()
535            .map(|schemas| schemas.iter().cloned().collect());
536        let mut relations: HashMap<ObjectId, RelationOverlay> = HashMap::new();
537        let mut baseline_relations = HashSet::new();
538        let mut baseline_indexes = HashSet::new();
539        let mut baseline_foreign_keys = HashSet::new();
540        let mut baseline_fk_dependencies = HashSet::new();
541        let mut triggers = HashMap::new();
542        let mut constraints = HashMap::new();
543        let mut types = HashMap::new();
544        let mut graph = DependencyGraph::new();
545
546        let mut schemas: HashMap<String, SchemaOverlay> = cache
547            .schemas
548            .iter()
549            .map(|(name, schema)| (name.clone(), SchemaOverlay::Present(schema.clone())))
550            .collect();
551        // Effective cached search-path entries and modeled objects are direct
552        // evidence that their namespaces existed at synchronization time.
553        // This also keeps programmatically assembled caches internally
554        // consistent without treating unrelated out-of-scope schemas as
555        // authoritative catalogs.
556        let inferred_schema_owner = ObjectId::new(
557            "",
558            cache.metadata.source_role.as_deref().unwrap_or("postgres"),
559        );
560        for name in cache
561            .relations
562            .keys()
563            .map(|id| &id.schema)
564            .chain(cache.types.keys().map(|id| &id.schema))
565            .chain(cache.functions.keys().map(|id| &id.schema))
566            .chain(cache.sequences.keys().map(|id| &id.schema))
567        {
568            schemas.entry(name.clone()).or_insert_with(|| {
569                SchemaOverlay::Present(crate::model::schema::SchemaState {
570                    name: name.clone(),
571                    owner: inferred_schema_owner.clone(),
572                    generation: 0,
573                })
574            });
575        }
576        if cache.schemas.is_empty() && cache.metadata.schemas.is_none() {
577            for name in &cache.search_path {
578                schemas.entry(name.clone()).or_insert_with(|| {
579                    SchemaOverlay::Present(crate::model::schema::SchemaState {
580                        name: name.clone(),
581                        owner: inferred_schema_owner.clone(),
582                        generation: 0,
583                    })
584                });
585            }
586        }
587
588        let sequences = cache
589            .sequences
590            .iter()
591            .map(|(id, sequence)| (id.clone(), SequenceOverlay::Present(sequence.clone())))
592            .collect();
593        let baseline_sequences = cache.sequences.keys().cloned().collect();
594        for sequence in cache.sequences.values() {
595            if let Some((table, column)) = &sequence.owned_by {
596                graph.add_edge(DependencyEdge::new(
597                    sequence.id.clone(),
598                    table.clone(),
599                    DependencyKind::SequenceOwnedBy {
600                        column: column.clone(),
601                    },
602                ));
603            }
604        }
605
606        for (id, rel_state) in cache.baseline_relations() {
607            if rel_state.is_fk_dependency {
608                baseline_fk_dependencies.insert(id.clone());
609            }
610            relations.insert(id.clone(), RelationOverlay::Present(rel_state.clone()));
611            baseline_relations.insert(id.clone());
612        }
613
614        for (id, type_state) in &cache.types {
615            types.insert(id.clone(), TypeOverlay::Present(type_state.clone()));
616        }
617        let type_catalog = types.clone();
618        for overlay in relations.values_mut() {
619            if let RelationOverlay::Present(relation) = overlay {
620                for column in &mut relation.columns {
621                    column.type_id = column.data_type.as_deref().and_then(|raw| {
622                        Self::resolve_type_reference_from_catalog(
623                            raw,
624                            &type_catalog,
625                            &default_search_path,
626                        )
627                    });
628                }
629            }
630        }
631        for overlay in types.values_mut() {
632            if let TypeOverlay::Present(TypeState {
633                kind:
634                    TypeKind::Domain {
635                        base_type,
636                        base_type_id,
637                    },
638                ..
639            }) = overlay
640            {
641                *base_type_id = Self::resolve_type_reference_from_catalog(
642                    base_type,
643                    &type_catalog,
644                    &default_search_path,
645                );
646            }
647        }
648        for fk in cache.foreign_keys {
649            baseline_foreign_keys.insert((fk.from_table.clone(), fk.constraint_name.clone()));
650            graph.add_edge(DependencyEdge::new(
651                fk.from_table,
652                fk.to_table,
653                DependencyKind::ForeignKey {
654                    constraint_name: Some(fk.constraint_name),
655                    from_columns: Vec::new(),
656                    to_columns: Vec::new(),
657                    from_generation: 0,
658                },
659            ));
660        }
661
662        for idx in cache.indexes {
663            // Index identities are tracked separately from relation identities.
664            baseline_indexes.insert(idx.index_id.clone());
665            graph.add_edge(DependencyEdge::new(
666                idx.index_id,
667                idx.table_id,
668                DependencyKind::IndexOnRelation {
669                    using_method: None,
670                    has_predicate: false,
671                    is_concurrent: false,
672                    is_unique: false,
673                    eligibility_known: false,
674                },
675            ));
676        }
677
678        for dependency in cache.dependencies {
679            if dependency.deptype != "view" {
680                continue;
681            }
682            let (Some(obj_schema), Some(obj_name), Some(ref_schema), Some(ref_name)) = (
683                dependency.obj_schema,
684                dependency.obj_name,
685                dependency.ref_schema,
686                dependency.ref_name,
687            ) else {
688                continue;
689            };
690            let dependent = ObjectId::new(obj_schema, obj_name);
691            let referenced = ObjectId::new(ref_schema, ref_name);
692            // Older caches created on PostgreSQL 14/15 can contain an
693            // internal pg_rewrite self-edge for a view. Ignore it while
694            // loading so upgrading safe-migrate does not require a re-sync to
695            // restore a meaningful dependency graph.
696            if dependent == referenced {
697                continue;
698            }
699            let is_view = relations.get(&dependent).is_some_and(|relation| {
700                matches!(
701                    relation,
702                    RelationOverlay::Present(state)
703                        if matches!(
704                            state.kind,
705                            crate::model::relation::RelationKind::View
706                                | crate::model::relation::RelationKind::MaterializedView
707                    )
708                )
709            });
710            let dependent_schema_is_omitted = baseline_schemas
711                .as_ref()
712                .is_some_and(|schemas| !schemas.contains(&dependent.schema));
713            if is_view || dependent_schema_is_omitted {
714                // Scoped caches may intentionally omit the referenced schema.
715                // The dependency query can also return a view outside the
716                // selected scope when it depends on an in-scope relation.
717                // Preserve either direction so a later migration cannot
718                // mistake an omitted dependent or referenced object for a
719                // safe drop target.
720                graph.add_edge(DependencyEdge::new(
721                    dependent,
722                    referenced,
723                    DependencyKind::ViewDependency { view_generation: 0 },
724                ));
725            }
726        }
727
728        for constraint in cache.constraints {
729            constraints.insert(
730                (constraint.table_id.clone(), constraint.name.clone()),
731                constraint,
732            );
733        }
734
735        for t in cache.triggers {
736            let trigger_key = Self::trigger_key(&t.table_id, &t.trigger_id.name);
737            triggers.insert(
738                trigger_key.clone(),
739                TriggerOverlay::Present(crate::model::trigger::TriggerState {
740                    name: t.trigger_id.name.clone(),
741                    id: trigger_key.clone(),
742                    table_id: t.table_id.clone(),
743                    enabled_mode: t.enabled_mode,
744                    generation: 0,
745                }),
746            );
747            graph.add_edge(DependencyEdge::new(
748                trigger_key.clone(),
749                t.table_id,
750                DependencyKind::TriggerOnTable {
751                    trigger_id: trigger_key,
752                    function_id: t.function_id,
753                },
754            ));
755        }
756
757        let mut functions: HashMap<ObjectId, crate::model::function::FunctionOverlay> =
758            HashMap::new();
759        for (id, func_state) in &cache.functions {
760            functions.insert(
761                id.clone(),
762                crate::model::function::FunctionOverlay::Present(func_state.clone()),
763            );
764        }
765        for overlay in functions.values_mut() {
766            if let crate::model::function::FunctionOverlay::Present(function) = overlay {
767                function.arg_type_ids = function
768                    .arg_types
769                    .iter()
770                    .map(|raw| {
771                        Self::resolve_type_reference_from_catalog(
772                            raw,
773                            &type_catalog,
774                            &default_search_path,
775                        )
776                    })
777                    .collect();
778                function.return_type_id = Self::resolve_type_reference_from_catalog(
779                    &function.return_type,
780                    &type_catalog,
781                    &default_search_path,
782                );
783            }
784        }
785
786        let publications = cache
787            .publications
788            .into_iter()
789            .map(|(name, publication)| {
790                if let crate::analysis::facts::PublicationScope::Explicit(objects) =
791                    &publication.scope
792                {
793                    for object in objects {
794                        if let crate::analysis::facts::PublicationObjectFact::Table {
795                            name: relation,
796                            ..
797                        } = object
798                        {
799                            let table_id = ObjectId::new(
800                                relation
801                                    .schema
802                                    .as_ref()
803                                    .map(|schema| schema.resolve())
804                                    .unwrap_or_else(|| "public".to_string()),
805                                relation.name.resolve(),
806                            );
807                            graph.add_edge(DependencyEdge::new(
808                                table_id,
809                                ObjectId::new("public", &name),
810                                DependencyKind::PublicationIncludes {
811                                    publication_name: name.clone(),
812                                },
813                            ));
814                        }
815                    }
816                }
817                (
818                    name,
819                    crate::model::replication::PublicationOverlay::Present(publication),
820                )
821            })
822            .collect();
823        let subscriptions = cache
824            .subscriptions
825            .into_iter()
826            .map(|(name, subscription)| {
827                (
828                    name,
829                    crate::model::replication::SubscriptionOverlay::Present(subscription),
830                )
831            })
832            .collect();
833
834        let mut state = Self {
835            pg_version_num: cache.pg_version_num,
836            baseline_available,
837            baseline_schemas,
838            baseline_relations,
839            baseline_indexes,
840            baseline_foreign_keys,
841            baseline_fk_dependencies,
842            baseline_sequences,
843            local: LocalState {
844                schemas,
845                relations,
846                types,
847                functions,
848                sequences,
849                publications,
850                subscriptions,
851                roles: cache
852                    .roles
853                    .into_iter()
854                    .map(|(id, role)| (id, crate::model::role::RoleOverlay::Present(role)))
855                    .collect(),
856                triggers,
857                constraints,
858                graph,
859                search_path: default_search_path.clone(),
860                default_search_path,
861                search_path_template: default_search_path_template.clone(),
862                session_search_path_template: default_search_path_template.clone(),
863                default_search_path_template,
864                lock_timeout: ScopedSetting::new(source_lock_timeout),
865                statement_timeout: ScopedSetting::new(source_statement_timeout),
866                current_role,
867                current_role_known,
868                persistent_current_role,
869                persistent_current_role_known,
870                session_role,
871                session_role_known,
872                persistent_session_role,
873                persistent_session_role_known,
874                authenticated_role,
875                authenticated_role_known,
876                roles_known,
877                confidence: Confidence::Exact,
878                transactions: Vec::new(),
879                transaction_aborted: false,
880                pending_validation: HashSet::new(),
881                generation_counter: 0,
882            },
883        };
884        state.refresh_role_sensitive_search_path();
885        state.local.default_search_path = state.local.search_path.clone();
886        state
887    }
888
889    pub fn get_relation(&self, id: &ObjectId) -> Option<&RelationOverlay> {
890        self.local.relations.get(id)
891    }
892
893    pub fn resolve_function_schema(
894        &self,
895        name: &crate::ast::identifiers::QualifiedName,
896        sig_str: &str,
897    ) -> String {
898        if let Some(schema) = &name.schema {
899            return schema.resolve();
900        }
901        for schema in &self.local.search_path {
902            let candidate = ObjectId::new(schema.clone(), sig_str.to_string());
903            if self.routine_is_present(&candidate) {
904                return schema.clone();
905            }
906        }
907        self.local
908            .search_path
909            .first()
910            .cloned()
911            .unwrap_or_else(|| "public".to_string())
912    }
913
914    pub fn resolve_relation_id(&self, name: &crate::ast::identifiers::QualifiedName) -> ObjectId {
915        if let Some(schema) = &name.schema {
916            return ObjectId::new(schema.resolve(), name.name.resolve());
917        }
918        let resolved_name = name.name.resolve();
919        for schema in &self.local.search_path {
920            let mut candidate = ObjectId::new(schema.clone(), resolved_name.clone());
921            if self.relation_namespace_object_is_present(&candidate) {
922                candidate.inferred_schema = true;
923                return candidate;
924            }
925        }
926        let schema = self
927            .local
928            .search_path
929            .first()
930            .cloned()
931            .unwrap_or_else(|| "public".to_string());
932        let mut id = ObjectId::new(schema, resolved_name);
933        id.inferred_schema = true;
934        id
935    }
936
937    pub fn relation_is_present(&self, id: &ObjectId) -> bool {
938        matches!(
939            self.local.relations.get(id),
940            Some(RelationOverlay::Present(_))
941        )
942    }
943
944    /// Returns whether a cache-backed absence is authoritative for an object.
945    /// A scoped cache only establishes absence in the schemas it actually
946    /// synchronized.
947    pub fn baseline_covers_object(&self, id: &ObjectId) -> bool {
948        self.baseline_schemas
949            .as_ref()
950            .is_none_or(|schemas| schemas.contains(&id.schema))
951    }
952
953    fn relation_lookup(
954        &self,
955        id: &ObjectId,
956        expected: impl FnOnce(&RelationKind) -> bool,
957    ) -> ObjectLookup {
958        match self.local.relations.get(id) {
959            Some(RelationOverlay::Present(relation)) if expected(&relation.kind) => {
960                ObjectLookup::Present
961            }
962            Some(RelationOverlay::Present(_)) => ObjectLookup::WrongKind,
963            Some(RelationOverlay::Dropped) => ObjectLookup::Tombstone,
964            None if self.sequence_is_present(id) || self.index_is_present(id) => {
965                ObjectLookup::WrongKind
966            }
967            None if self.baseline_available && self.baseline_covers_object(id) => {
968                ObjectLookup::AuthoritativelyAbsent
969            }
970            None => ObjectLookup::Unknown,
971        }
972    }
973
974    /// Validate a relation reference before a mutation creates a dependent
975    /// object.  A scoped baseline cannot prove anything about an omitted
976    /// schema, so leave the state conservative instead of inventing an edge.
977    pub(super) fn ensure_relation_target<F>(
978        &mut self,
979        id: &ObjectId,
980        expected: F,
981        missing_reason: String,
982        wrong_kind_reason: String,
983    ) -> Result<(), MutationResult>
984    where
985        F: FnOnce(&RelationKind) -> bool,
986    {
987        match self.relation_lookup(id, expected) {
988            ObjectLookup::Present => Ok(()),
989            ObjectLookup::WrongKind => Err(MutationResult::Conflict {
990                reason: wrong_kind_reason,
991            }),
992            ObjectLookup::AuthoritativelyAbsent | ObjectLookup::Tombstone => {
993                Err(MutationResult::Conflict {
994                    reason: missing_reason,
995                })
996            }
997            ObjectLookup::Unknown => {
998                self.snapshot_confidence();
999                self.local.confidence = Confidence::Tainted;
1000                Err(MutationResult::Skipped)
1001            }
1002        }
1003    }
1004
1005    fn type_lookup(&self, id: &ObjectId, expected: impl FnOnce(&TypeKind) -> bool) -> ObjectLookup {
1006        match self.local.types.get(id) {
1007            Some(TypeOverlay::Present(state)) if expected(&state.kind) => ObjectLookup::Present,
1008            Some(TypeOverlay::Present(_)) => ObjectLookup::WrongKind,
1009            Some(TypeOverlay::Dropped) => ObjectLookup::Tombstone,
1010            None if self.baseline_available && self.baseline_covers_object(id) => {
1011                ObjectLookup::AuthoritativelyAbsent
1012            }
1013            None => ObjectLookup::Unknown,
1014        }
1015    }
1016
1017    pub(super) fn ensure_routine_target(
1018        &mut self,
1019        id: &ObjectId,
1020        expected: crate::model::function::RoutineKind,
1021        missing_reason: String,
1022        wrong_kind_reason: String,
1023    ) -> Result<(), MutationResult> {
1024        match self.local.functions.get(id) {
1025            Some(FunctionOverlay::Present(function)) if function.routine_kind == expected => Ok(()),
1026            Some(FunctionOverlay::Present(_)) => Err(MutationResult::Conflict {
1027                reason: wrong_kind_reason,
1028            }),
1029            Some(FunctionOverlay::Dropped) => Err(MutationResult::Conflict {
1030                reason: missing_reason,
1031            }),
1032            None if self.baseline_available && self.baseline_covers_object(id) => {
1033                Err(MutationResult::Conflict {
1034                    reason: missing_reason,
1035                })
1036            }
1037            None => {
1038                self.snapshot_confidence();
1039                self.local.confidence = Confidence::Tainted;
1040                Err(MutationResult::Skipped)
1041            }
1042        }
1043    }
1044
1045    /// Validate the namespace in which a newly-created object will live.
1046    ///
1047    /// A full baseline (or an explicit schema creation earlier in the chain)
1048    /// can prove that a schema exists.  An omitted scoped schema is unknown;
1049    /// do not manufacture an object there while claiming an exact result.
1050    pub(super) fn ensure_schema_target(&mut self, schema: &str) -> Result<(), MutationResult> {
1051        match self.schema_lookup(schema) {
1052            ObjectLookup::Present => Ok(()),
1053            ObjectLookup::Tombstone | ObjectLookup::AuthoritativelyAbsent => {
1054                Err(MutationResult::Conflict {
1055                    reason: format!("schema '{}' does not exist", schema),
1056                })
1057            }
1058            ObjectLookup::Unknown => {
1059                self.snapshot_confidence();
1060                self.local.confidence = Confidence::Tainted;
1061                Err(MutationResult::Skipped)
1062            }
1063            ObjectLookup::WrongKind => {
1064                unreachable!("schemas do not share an overlay with other object kinds")
1065            }
1066        }
1067    }
1068
1069    fn snapshot_baseline_foreign_keys(&mut self) {
1070        if let Some(frame) = self.local.transactions.last_mut() {
1071            frame
1072                .undo_log
1073                .push(StateChange::BaselineForeignKeysSnapshot {
1074                    previous: self.baseline_foreign_keys.clone(),
1075                });
1076        }
1077    }
1078
1079    /// Remove constraint metadata and baseline FK identities for objects that
1080    /// PostgreSQL drops as part of a relation dependency operation.
1081    pub(super) fn remove_dropped_constraints(
1082        &mut self,
1083        dropped_relations: &HashSet<ObjectId>,
1084        dropped_constraints: &HashSet<(ObjectId, String)>,
1085    ) {
1086        let resolution_graph = self.local.graph.clone();
1087        let should_remove = |table_id: &ObjectId, name: &str| {
1088            let resolved_table = resolution_graph.resolve_rename(table_id);
1089            dropped_relations.contains(resolved_table)
1090                || dropped_constraints.contains(&(resolved_table.clone(), name.to_string()))
1091        };
1092
1093        let constraint_keys: Vec<(ObjectId, String)> = self
1094            .local
1095            .constraints
1096            .keys()
1097            .filter(|(table_id, name)| should_remove(table_id, name))
1098            .cloned()
1099            .collect();
1100        for (table_id, name) in constraint_keys {
1101            self.snapshot_constraint(&table_id, &name);
1102            self.local.constraints.remove(&(table_id, name));
1103        }
1104
1105        let pending_changed = self
1106            .local
1107            .pending_validation
1108            .iter()
1109            .any(|(table_id, name)| should_remove(table_id, name));
1110        if pending_changed {
1111            self.snapshot_pending_validation();
1112            self.local
1113                .pending_validation
1114                .retain(|(table_id, name)| !should_remove(table_id, name));
1115        }
1116
1117        if self
1118            .baseline_foreign_keys
1119            .iter()
1120            .any(|(table_id, name)| should_remove(table_id, name))
1121        {
1122            self.snapshot_baseline_foreign_keys();
1123            self.baseline_foreign_keys
1124                .retain(|(table_id, name)| !should_remove(table_id, name));
1125        }
1126    }
1127
1128    pub fn baseline_scope_omits_displayed_object<'a>(
1129        &self,
1130        object_name: &'a str,
1131    ) -> Option<&'a str> {
1132        let schemas = self.baseline_schemas.as_ref()?;
1133        let (schema, _) = object_name.split_once('.')?;
1134        (!schemas.contains(schema)).then_some(schema)
1135    }
1136
1137    pub(crate) fn sequence_is_present(&self, id: &ObjectId) -> bool {
1138        matches!(
1139            self.local.sequences.get(id),
1140            Some(SequenceOverlay::Present(_))
1141        )
1142    }
1143
1144    pub(crate) fn type_is_present(&self, id: &ObjectId) -> bool {
1145        matches!(self.local.types.get(id), Some(TypeOverlay::Present(_)))
1146    }
1147
1148    pub(crate) fn routine_is_present(&self, id: &ObjectId) -> bool {
1149        matches!(
1150            self.local.functions.get(id),
1151            Some(FunctionOverlay::Present(_))
1152        )
1153    }
1154
1155    fn resolve_type_reference_from_catalog(
1156        raw: &str,
1157        types: &HashMap<ObjectId, TypeOverlay>,
1158        search_path: &[String],
1159    ) -> Option<ObjectId> {
1160        let (schema, name) = Self::parse_type_reference(raw)?;
1161        if let Some(schema) = schema {
1162            let candidate = ObjectId::new(schema, name);
1163            return matches!(types.get(&candidate), Some(TypeOverlay::Present(_)))
1164                .then_some(candidate);
1165        }
1166        search_path.iter().find_map(|schema| {
1167            let candidate = ObjectId::new(schema, &name);
1168            matches!(types.get(&candidate), Some(TypeOverlay::Present(_))).then_some(candidate)
1169        })
1170    }
1171
1172    /// Parse a SQL type name exactly enough to resolve modeled named types.
1173    /// Quoted identifiers retain their case, unquoted identifiers fold to
1174    /// lowercase, and any number of array suffixes are ignored for lookup.
1175    fn parse_type_reference(raw: &str) -> Option<(Option<String>, String)> {
1176        let mut token = raw.trim();
1177        while let Some(without_array) = token.strip_suffix("[]") {
1178            token = without_array.trim_end();
1179        }
1180
1181        let mut parts = Vec::new();
1182        let mut current = String::new();
1183        let mut quoted = false;
1184        let mut part_is_quoted = false;
1185        let mut chars = token.chars().peekable();
1186        while let Some(character) = chars.next() {
1187            match character {
1188                '"' if quoted && chars.peek() == Some(&'"') => {
1189                    current.push('"');
1190                    chars.next();
1191                }
1192                '"' => {
1193                    quoted = !quoted;
1194                    part_is_quoted = true;
1195                }
1196                '.' if !quoted => {
1197                    parts.push(Self::resolve_type_identifier(&current, part_is_quoted)?);
1198                    current.clear();
1199                    part_is_quoted = false;
1200                }
1201                character if !quoted && character.is_whitespace() => {}
1202                character => current.push(character),
1203            }
1204        }
1205        if quoted {
1206            return None;
1207        }
1208        parts.push(Self::resolve_type_identifier(&current, part_is_quoted)?);
1209        match parts.as_slice() {
1210            [name] => Some((None, name.clone())),
1211            [schema, name] => Some((Some(schema.clone()), name.clone())),
1212            _ => None,
1213        }
1214    }
1215
1216    fn resolve_type_identifier(identifier: &str, quoted: bool) -> Option<String> {
1217        (!identifier.is_empty()).then(|| {
1218            if quoted {
1219                identifier.to_string()
1220            } else {
1221                identifier.to_lowercase()
1222            }
1223        })
1224    }
1225
1226    fn resolve_type_reference(&self, raw: &str) -> Option<ObjectId> {
1227        Self::resolve_type_reference_from_catalog(raw, &self.local.types, &self.local.search_path)
1228    }
1229
1230    fn type_reference_name(id: &ObjectId, qualified: bool) -> String {
1231        let quote = |identifier: &str| {
1232            let unquoted = identifier
1233                .chars()
1234                .enumerate()
1235                .all(|(index, character)| match index {
1236                    0 => character.is_ascii_lowercase() || character == '_',
1237                    _ => {
1238                        character.is_ascii_lowercase()
1239                            || character.is_ascii_digit()
1240                            || character == '_'
1241                            || character == '$'
1242                    }
1243                });
1244            if unquoted {
1245                identifier.to_string()
1246            } else {
1247                format!("\"{}\"", identifier.replace('"', "\"\""))
1248            }
1249        };
1250
1251        if qualified {
1252            format!("{}.{}", quote(&id.schema), quote(&id.name))
1253        } else {
1254            quote(&id.name)
1255        }
1256    }
1257
1258    fn remapped_type_display(raw: &str, new_id: &ObjectId, schema_changed: bool) -> String {
1259        let suffix = raw.find('[').map(|index| &raw[index..]).unwrap_or("");
1260        format!(
1261            "{}{}",
1262            Self::type_reference_name(new_id, schema_changed),
1263            suffix
1264        )
1265    }
1266
1267    pub(crate) fn index_is_present(&self, id: &ObjectId) -> bool {
1268        self.local.graph.edges().iter().any(|edge| {
1269            matches!(edge.kind, DependencyKind::IndexOnRelation { .. }) && edge.dependent == *id
1270        })
1271    }
1272
1273    pub(crate) fn relation_namespace_object_is_present(&self, id: &ObjectId) -> bool {
1274        self.relation_is_present(id) || self.sequence_is_present(id) || self.index_is_present(id)
1275    }
1276
1277    /// Pick a generated name while also avoiding names reserved by other
1278    /// constraints in the same CREATE TABLE statement. The ordinary helper
1279    /// only sees already-applied state, which is not enough for a batch of
1280    /// inline constraints that becomes visible at the end of the statement.
1281    pub(super) fn next_generated_constraint_name_avoiding(
1282        &self,
1283        table: &ObjectId,
1284        name1: &str,
1285        name2: Option<&str>,
1286        label: &str,
1287        reserved: &HashSet<String>,
1288    ) -> String {
1289        (0..)
1290            .map(|suffix| {
1291                let label = if suffix == 0 {
1292                    label.to_string()
1293                } else {
1294                    format!("{label}{suffix}")
1295                };
1296                Self::postgres_object_name(name1, name2, &label)
1297            })
1298            .find(|candidate| {
1299                !reserved.contains(candidate)
1300                    && !self
1301                        .local
1302                        .constraints
1303                        .contains_key(&(table.clone(), candidate.clone()))
1304            })
1305            .expect("constraint suffix space is unbounded")
1306    }
1307
1308    fn postgres_object_name(name1: &str, name2: Option<&str>, label: &str) -> String {
1309        const MAX_IDENTIFIER_BYTES: usize = 63;
1310
1311        fn truncate(value: &str, max_bytes: usize) -> &str {
1312            let mut end = max_bytes.min(value.len());
1313            while !value.is_char_boundary(end) {
1314                end -= 1;
1315            }
1316            &value[..end]
1317        }
1318
1319        let separators = usize::from(name2.is_some()) + 1;
1320        let available = MAX_IDENTIFIER_BYTES.saturating_sub(label.len() + separators);
1321        let mut name1_bytes = name1.len();
1322        let mut name2_bytes = name2.map_or(0, str::len);
1323        while name1_bytes + name2_bytes > available {
1324            if name1_bytes > name2_bytes {
1325                name1_bytes -= 1;
1326            } else {
1327                name2_bytes -= 1;
1328            }
1329        }
1330
1331        let name1 = truncate(name1, name1_bytes);
1332        match name2 {
1333            Some(name2) => format!("{name1}_{}_{}", truncate(name2, name2_bytes), label),
1334            None => format!("{name1}_{label}"),
1335        }
1336    }
1337
1338    fn relation_namespace_is_taken(&self, id: &ObjectId) -> bool {
1339        self.relation_namespace_object_is_present(id) || self.type_is_present(id)
1340    }
1341
1342    fn next_implicit_sequence_id(
1343        &self,
1344        table: &ObjectId,
1345        column: &str,
1346        reserved: &HashSet<ObjectId>,
1347    ) -> ObjectId {
1348        (0..)
1349            .map(|suffix| {
1350                let label = if suffix == 0 {
1351                    "seq".to_string()
1352                } else {
1353                    format!("seq{suffix}")
1354                };
1355                ObjectId::new(
1356                    &table.schema,
1357                    Self::postgres_object_name(&table.name, Some(column), &label),
1358                )
1359            })
1360            .find(|candidate| {
1361                !reserved.contains(candidate) && !self.relation_namespace_is_taken(candidate)
1362            })
1363            .expect("implicit sequence suffix space is unbounded")
1364    }
1365
1366    fn sequence_nextval_default(id: &ObjectId) -> crate::analysis::expr_ir::ExprIr {
1367        crate::analysis::expr_ir::ExprIr::FunctionCall {
1368            name: "nextval".to_string(),
1369            args: vec![crate::analysis::expr_ir::ExprIr::Literal(format!(
1370                "{}.{}",
1371                id.schema, id.name
1372            ))],
1373        }
1374    }
1375
1376    pub fn column_was_added_in_transaction(&self, table_id: &ObjectId, column: &str) -> bool {
1377        if self.local.transactions.is_empty() {
1378            return false;
1379        }
1380
1381        // Search from the oldest transaction frame to the newest
1382        for frame in &self.local.transactions {
1383            for change in &frame.undo_log {
1384                if let StateChange::RelationSnapshot { id, previous } = change
1385                    && id == table_id
1386                {
1387                    match previous.as_ref() {
1388                        None | Some(RelationOverlay::Dropped) => {
1389                            return true;
1390                        }
1391                        Some(RelationOverlay::Present(r)) => {
1392                            let col_existed = r.columns.iter().any(|c| c.name == column);
1393                            return !col_existed;
1394                        }
1395                    }
1396                }
1397            }
1398        }
1399        false
1400    }
1401
1402    pub fn capture_pre_state(&self) -> PreState {
1403        let mut pre_state = PreState::default();
1404        self.capture_pre_state_into(&mut pre_state);
1405        pre_state
1406    }
1407
1408    pub(crate) fn capture_pre_state_into(&self, pre_state: &mut PreState) {
1409        let PreState {
1410            relations,
1411            functions,
1412            roles,
1413            publications,
1414            subscriptions,
1415            sequences,
1416            types,
1417            indexes,
1418            baseline_foreign_keys,
1419        } = pre_state;
1420
1421        sync_present_map(relations, &self.local.relations, |overlay| match overlay {
1422            RelationOverlay::Present(state) => Some(state),
1423            RelationOverlay::Dropped => None,
1424        });
1425        sync_present_map(functions, &self.local.functions, |overlay| match overlay {
1426            crate::model::function::FunctionOverlay::Present(state) => Some(state),
1427            crate::model::function::FunctionOverlay::Dropped => None,
1428        });
1429        sync_present_map(roles, &self.local.roles, |overlay| match overlay {
1430            crate::model::role::RoleOverlay::Present(state) => Some(state),
1431            crate::model::role::RoleOverlay::Dropped => None,
1432        });
1433        sync_present_map(
1434            publications,
1435            &self.local.publications,
1436            |overlay| match overlay {
1437                crate::model::replication::PublicationOverlay::Present(state) => Some(state),
1438                crate::model::replication::PublicationOverlay::Dropped => None,
1439            },
1440        );
1441        sync_present_map(
1442            subscriptions,
1443            &self.local.subscriptions,
1444            |overlay| match overlay {
1445                crate::model::replication::SubscriptionOverlay::Present(state) => Some(state),
1446                crate::model::replication::SubscriptionOverlay::Dropped => None,
1447            },
1448        );
1449        sync_present_map(sequences, &self.local.sequences, |overlay| match overlay {
1450            SequenceOverlay::Present(state) => Some(state),
1451            SequenceOverlay::Dropped => None,
1452        });
1453        sync_present_map(types, &self.local.types, |overlay| match overlay {
1454            TypeOverlay::Present(state) => Some(state),
1455            TypeOverlay::Dropped => None,
1456        });
1457
1458        let mut index = 0;
1459        for edge in self
1460            .local
1461            .graph
1462            .edges()
1463            .iter()
1464            .filter(|edge| matches!(edge.kind, DependencyKind::IndexOnRelation { .. }))
1465        {
1466            if let Some(existing) = indexes.get_mut(index) {
1467                if existing != edge {
1468                    existing.clone_from(edge);
1469                }
1470            } else {
1471                indexes.push(edge.clone());
1472            }
1473            index += 1;
1474        }
1475        indexes.truncate(index);
1476        baseline_foreign_keys.clone_from(&self.baseline_foreign_keys);
1477    }
1478
1479    pub fn get_cascade_closure(&self, target_oid: &ObjectId) -> CascadeResult {
1480        let mut result = CascadeResult::default();
1481        let mut visited = HashSet::new();
1482        self.walk_cascade(target_oid, &mut visited, &mut result);
1483        result
1484    }
1485
1486    fn walk_cascade(
1487        &self,
1488        current: &ObjectId,
1489        visited: &mut HashSet<ObjectId>,
1490        result: &mut CascadeResult,
1491    ) {
1492        let resolved_current = self.local.graph.resolve_rename(current).clone();
1493
1494        if !visited.insert(resolved_current.clone()) {
1495            return;
1496        }
1497
1498        result.dropped_relations.insert(resolved_current.clone());
1499
1500        if self.local.graph.cascade_index_is_worthwhile() {
1501            for edge in self.local.graph.cascade_edges(&resolved_current) {
1502                self.walk_cascade_edge(edge, &resolved_current, visited, result);
1503            }
1504        } else {
1505            for edge in self.local.graph.edges() {
1506                self.walk_cascade_edge(edge, &resolved_current, visited, result);
1507            }
1508        }
1509    }
1510
1511    fn walk_cascade_edge(
1512        &self,
1513        edge: &DependencyEdge,
1514        resolved_current: &ObjectId,
1515        visited: &mut HashSet<ObjectId>,
1516        result: &mut CascadeResult,
1517    ) {
1518        match &edge.kind {
1519            DependencyKind::ViewDependency { .. } => {
1520                if self.local.graph.resolve_rename(&edge.referenced) == resolved_current {
1521                    let resolved_view_id = self.local.graph.resolve_rename(&edge.dependent).clone();
1522                    if !visited.contains(&resolved_view_id) {
1523                        self.walk_cascade(&resolved_view_id, visited, result);
1524                    }
1525                }
1526            }
1527            DependencyKind::IndexOnRelation { .. } => {
1528                if self.local.graph.resolve_rename(&edge.referenced) == resolved_current {
1529                    result
1530                        .dropped_indexes
1531                        .insert(self.local.graph.resolve_rename(&edge.dependent).clone());
1532                }
1533            }
1534            DependencyKind::ForeignKey {
1535                constraint_name, ..
1536            } => {
1537                if self.local.graph.resolve_rename(&edge.referenced) == resolved_current
1538                    && let Some(cname) = constraint_name
1539                {
1540                    result.dropped_constraints.insert((
1541                        self.local.graph.resolve_rename(&edge.dependent).clone(),
1542                        cname.clone(),
1543                    ));
1544                }
1545            }
1546            DependencyKind::PartitionOf
1547                if self.local.graph.resolve_rename(&edge.referenced) == resolved_current =>
1548            {
1549                let resolved_child = self.local.graph.resolve_rename(&edge.dependent).clone();
1550                if !visited.contains(&resolved_child) {
1551                    self.walk_cascade(&resolved_child, visited, result);
1552                }
1553            }
1554            _ => {}
1555        }
1556    }
1557
1558    fn resolve_grant_privileges(
1559        &self,
1560        spec: &crate::analysis::facts::PrivilegeSpec,
1561    ) -> HashSet<Privilege> {
1562        let supports_maintain = self
1563            .pg_version_num
1564            .is_some_and(|version| version >= 170_000);
1565        match spec {
1566            crate::analysis::facts::PrivilegeSpec::All => {
1567                let mut privileges = [
1568                    Privilege::Select,
1569                    Privilege::Insert,
1570                    Privilege::Update,
1571                    Privilege::Delete,
1572                    Privilege::Truncate,
1573                    Privilege::References,
1574                    Privilege::Trigger,
1575                ]
1576                .into_iter()
1577                .collect::<HashSet<_>>();
1578                if supports_maintain {
1579                    privileges.insert(Privilege::Maintain);
1580                }
1581                privileges
1582            }
1583            crate::analysis::facts::PrivilegeSpec::List(list) => list
1584                .iter()
1585                .filter_map(|p| match p {
1586                    crate::analysis::facts::PrivilegeFact::Select => Some(Privilege::Select),
1587                    crate::analysis::facts::PrivilegeFact::Insert => Some(Privilege::Insert),
1588                    crate::analysis::facts::PrivilegeFact::Update => Some(Privilege::Update),
1589                    crate::analysis::facts::PrivilegeFact::Delete => Some(Privilege::Delete),
1590                    crate::analysis::facts::PrivilegeFact::Truncate => Some(Privilege::Truncate),
1591                    crate::analysis::facts::PrivilegeFact::References => {
1592                        Some(Privilege::References)
1593                    }
1594                    crate::analysis::facts::PrivilegeFact::Trigger => Some(Privilege::Trigger),
1595                    crate::analysis::facts::PrivilegeFact::Maintain if supports_maintain => {
1596                        Some(Privilege::Maintain)
1597                    }
1598                    _ => None,
1599                })
1600                .collect(),
1601        }
1602    }
1603
1604    fn resolve_role_name(
1605        role: &crate::analysis::facts::RoleFact,
1606        current_role: &str,
1607        session_role: &str,
1608    ) -> Option<ObjectId> {
1609        let name = match role {
1610            crate::analysis::facts::RoleFact::Named { name, .. } => Some(name.clone()),
1611            crate::analysis::facts::RoleFact::CurrentUser
1612            | crate::analysis::facts::RoleFact::CurrentRole => Some(current_role.to_string()),
1613            crate::analysis::facts::RoleFact::SessionUser => Some(session_role.to_string()),
1614            crate::analysis::facts::RoleFact::Unknown => None,
1615        }?;
1616        Some(ObjectId::new("", name))
1617    }
1618
1619    fn role_fact_identity(
1620        &self,
1621        role: &crate::analysis::facts::RoleFact,
1622    ) -> Option<(String, bool)> {
1623        match role {
1624            crate::analysis::facts::RoleFact::Named { name, .. } => Some((name.clone(), true)),
1625            crate::analysis::facts::RoleFact::CurrentUser
1626            | crate::analysis::facts::RoleFact::CurrentRole => Some((
1627                self.local.current_role.clone(),
1628                self.local.current_role_known,
1629            )),
1630            crate::analysis::facts::RoleFact::SessionUser => Some((
1631                self.local.session_role.clone(),
1632                self.local.session_role_known,
1633            )),
1634            crate::analysis::facts::RoleFact::Unknown => None,
1635        }
1636    }
1637
1638    fn present_role(&self, name: &str) -> Option<&crate::model::role::RoleState> {
1639        match self.local.roles.get(&ObjectId::new("", name)) {
1640            Some(crate::model::role::RoleOverlay::Present(role)) => Some(role),
1641            _ => None,
1642        }
1643    }
1644
1645    fn can_set_role_to(&self, target: &str) -> Option<bool> {
1646        if !self.local.roles_known || !self.local.session_role_known {
1647            return None;
1648        }
1649        if self.present_role(target).is_none() {
1650            return Some(false);
1651        }
1652        if self.local.session_role == target {
1653            return Some(true);
1654        }
1655        let session = self.present_role(&self.local.session_role)?;
1656        if session.is_superuser {
1657            return Some(true);
1658        }
1659
1660        let mut pending = session.can_set_role_to.clone();
1661        let mut visited = HashSet::new();
1662        while let Some(role_id) = pending.pop() {
1663            if !visited.insert(role_id.clone()) {
1664                continue;
1665            }
1666            if role_id.name == target {
1667                return Some(true);
1668            }
1669            if let Some(role) = self.present_role(&role_id.name) {
1670                pending.extend(role.can_set_role_to.iter().cloned());
1671            }
1672        }
1673        Some(false)
1674    }
1675
1676    fn can_set_session_authorization_to(&self, target: &str) -> Option<bool> {
1677        if !self.local.roles_known || !self.local.authenticated_role_known {
1678            return None;
1679        }
1680        if self.present_role(target).is_none() {
1681            return Some(false);
1682        }
1683        if self.local.authenticated_role == target {
1684            return Some(true);
1685        }
1686        Some(
1687            self.present_role(&self.local.authenticated_role)
1688                .is_some_and(|role| role.is_superuser),
1689        )
1690    }
1691
1692    fn schema_is_present(&self, name: &str) -> bool {
1693        matches!(
1694            self.local.schemas.get(name),
1695            Some(SchemaOverlay::Present(_))
1696        )
1697    }
1698
1699    fn schema_lookup(&self, name: &str) -> ObjectLookup {
1700        match self.local.schemas.get(name) {
1701            Some(SchemaOverlay::Present(_)) => ObjectLookup::Present,
1702            Some(SchemaOverlay::Dropped) => ObjectLookup::Tombstone,
1703            None if self.schema_absence_is_authoritative(name) => {
1704                ObjectLookup::AuthoritativelyAbsent
1705            }
1706            None => ObjectLookup::Unknown,
1707        }
1708    }
1709
1710    fn schema_absence_is_authoritative(&self, name: &str) -> bool {
1711        if matches!(self.local.schemas.get(name), Some(SchemaOverlay::Dropped)) {
1712            return true;
1713        }
1714        self.baseline_available
1715            && self
1716                .baseline_schemas
1717                .as_ref()
1718                .is_none_or(|schemas| schemas.contains(name))
1719    }
1720
1721    fn refresh_role_sensitive_search_path(&mut self) {
1722        let template = self.local.search_path_template.clone();
1723        let mut effective = Vec::new();
1724        for entry in template {
1725            let schema = if entry == "$user" {
1726                if self.local.current_role_known {
1727                    self.local.current_role.clone()
1728                } else {
1729                    self.local.confidence = Confidence::Tainted;
1730                    continue;
1731                }
1732            } else {
1733                entry
1734            };
1735            if self.schema_is_present(&schema) {
1736                if !effective.contains(&schema) {
1737                    effective.push(schema);
1738                }
1739            } else if !self.schema_absence_is_authoritative(&schema) {
1740                self.local.confidence = Confidence::Tainted;
1741                if !effective.contains(&schema) {
1742                    effective.push(schema);
1743                }
1744            }
1745        }
1746        self.local.search_path = effective;
1747    }
1748
1749    fn remap_schema_id(id: &mut ObjectId, old_name: &str, new_name: &str) {
1750        if id.schema == old_name {
1751            id.schema = new_name.to_string();
1752        }
1753    }
1754
1755    fn rename_schema_namespace(&mut self, old_name: &str, new_name: &str) {
1756        self.snapshot_namespace();
1757
1758        let mut aliases = Vec::new();
1759        let mut relations = HashMap::new();
1760        for (mut id, mut overlay) in std::mem::take(&mut self.local.relations) {
1761            let old_id = id.clone();
1762            Self::remap_schema_id(&mut id, old_name, new_name);
1763            if let RelationOverlay::Present(state) = &mut overlay {
1764                Self::remap_schema_id(&mut state.id, old_name, new_name);
1765            }
1766            if id != old_id {
1767                aliases.push((old_id, id.clone()));
1768            }
1769            relations.insert(id, overlay);
1770        }
1771        self.local.relations = relations;
1772
1773        let mut types = HashMap::new();
1774        for (mut id, mut overlay) in std::mem::take(&mut self.local.types) {
1775            let old_id = id.clone();
1776            Self::remap_schema_id(&mut id, old_name, new_name);
1777            if let TypeOverlay::Present(state) = &mut overlay {
1778                Self::remap_schema_id(&mut state.id, old_name, new_name);
1779            }
1780            if id != old_id {
1781                aliases.push((old_id, id.clone()));
1782            }
1783            types.insert(id, overlay);
1784        }
1785        self.local.types = types;
1786
1787        let mut functions = HashMap::new();
1788        for (mut id, mut overlay) in std::mem::take(&mut self.local.functions) {
1789            let old_id = id.clone();
1790            Self::remap_schema_id(&mut id, old_name, new_name);
1791            if let crate::model::function::FunctionOverlay::Present(state) = &mut overlay {
1792                Self::remap_schema_id(&mut state.id, old_name, new_name);
1793            }
1794            if id != old_id {
1795                aliases.push((old_id, id.clone()));
1796            }
1797            functions.insert(id, overlay);
1798        }
1799        self.local.functions = functions;
1800
1801        let mut sequences = HashMap::new();
1802        for (mut id, mut overlay) in std::mem::take(&mut self.local.sequences) {
1803            let old_id = id.clone();
1804            Self::remap_schema_id(&mut id, old_name, new_name);
1805            if let SequenceOverlay::Present(state) = &mut overlay {
1806                Self::remap_schema_id(&mut state.id, old_name, new_name);
1807                if let Some((table, _)) = &mut state.owned_by {
1808                    Self::remap_schema_id(table, old_name, new_name);
1809                }
1810            }
1811            if id != old_id {
1812                aliases.push((old_id, id.clone()));
1813            }
1814            sequences.insert(id, overlay);
1815        }
1816        self.local.sequences = sequences;
1817
1818        let mut triggers = HashMap::new();
1819        for (mut id, mut overlay) in std::mem::take(&mut self.local.triggers) {
1820            let old_id = id.clone();
1821            Self::remap_schema_id(&mut id, old_name, new_name);
1822            if let TriggerOverlay::Present(state) = &mut overlay {
1823                Self::remap_schema_id(&mut state.id, old_name, new_name);
1824                Self::remap_schema_id(&mut state.table_id, old_name, new_name);
1825            }
1826            if id != old_id {
1827                aliases.push((old_id, id.clone()));
1828            }
1829            triggers.insert(id, overlay);
1830        }
1831        self.local.triggers = triggers;
1832
1833        for overlay in self.local.publications.values_mut() {
1834            let crate::model::replication::PublicationOverlay::Present(publication) = overlay
1835            else {
1836                continue;
1837            };
1838            let crate::analysis::facts::PublicationScope::Explicit(objects) =
1839                &mut publication.scope
1840            else {
1841                continue;
1842            };
1843            for object in objects {
1844                match object {
1845                    crate::analysis::facts::PublicationObjectFact::Table { name, .. } => {
1846                        if name
1847                            .schema
1848                            .as_ref()
1849                            .is_some_and(|schema| schema.resolve() == old_name)
1850                        {
1851                            name.schema = Some(crate::ast::identifiers::Ident::new(new_name, true));
1852                        }
1853                    }
1854                    crate::analysis::facts::PublicationObjectFact::SchemaTables {
1855                        schema, ..
1856                    } if schema == old_name => *schema = new_name.to_string(),
1857                    _ => {}
1858                }
1859            }
1860        }
1861
1862        self.local.constraints = std::mem::take(&mut self.local.constraints)
1863            .into_iter()
1864            .map(|((mut table, name), mut constraint)| {
1865                Self::remap_schema_id(&mut table, old_name, new_name);
1866                Self::remap_schema_id(&mut constraint.table_id, old_name, new_name);
1867                ((table, name), constraint)
1868            })
1869            .collect();
1870        self.local.pending_validation = std::mem::take(&mut self.local.pending_validation)
1871            .into_iter()
1872            .map(|(mut table, name)| {
1873                Self::remap_schema_id(&mut table, old_name, new_name);
1874                (table, name)
1875            })
1876            .collect();
1877
1878        self.local.graph.mutate_edges(|edges| {
1879            for edge in edges {
1880                match &mut edge.kind {
1881                    // Publication nodes are synthetic `public/<name>` IDs;
1882                    // only the included relation is schema-qualified.
1883                    DependencyKind::PublicationIncludes { .. } => {
1884                        Self::remap_schema_id(&mut edge.dependent, old_name, new_name);
1885                    }
1886                    DependencyKind::TriggerOnTable {
1887                        trigger_id,
1888                        function_id,
1889                    } => {
1890                        Self::remap_schema_id(&mut edge.dependent, old_name, new_name);
1891                        Self::remap_schema_id(&mut edge.referenced, old_name, new_name);
1892                        Self::remap_schema_id(trigger_id, old_name, new_name);
1893                        Self::remap_schema_id(function_id, old_name, new_name);
1894                    }
1895                    _ => {
1896                        Self::remap_schema_id(&mut edge.dependent, old_name, new_name);
1897                        Self::remap_schema_id(&mut edge.referenced, old_name, new_name);
1898                    }
1899                }
1900            }
1901        });
1902        for (old_id, new_id) in aliases {
1903            self.local.graph.add_edge(DependencyEdge::new(
1904                old_id,
1905                new_id,
1906                DependencyKind::RenameTo,
1907            ));
1908        }
1909
1910        let remap_set = |set: &mut HashSet<ObjectId>| {
1911            *set = std::mem::take(set)
1912                .into_iter()
1913                .map(|mut id| {
1914                    Self::remap_schema_id(&mut id, old_name, new_name);
1915                    id
1916                })
1917                .collect();
1918        };
1919        remap_set(&mut self.baseline_relations);
1920        remap_set(&mut self.baseline_indexes);
1921        remap_set(&mut self.baseline_fk_dependencies);
1922        remap_set(&mut self.baseline_sequences);
1923        self.baseline_foreign_keys = std::mem::take(&mut self.baseline_foreign_keys)
1924            .into_iter()
1925            .map(|(mut table, name)| {
1926                Self::remap_schema_id(&mut table, old_name, new_name);
1927                (table, name)
1928            })
1929            .collect();
1930        if let Some(schemas) = &mut self.baseline_schemas
1931            && schemas.remove(old_name)
1932        {
1933            schemas.insert(new_name.to_string());
1934        }
1935
1936        if let Some(SchemaOverlay::Present(mut schema)) = self.local.schemas.remove(old_name) {
1937            schema.name = new_name.to_string();
1938            self.local
1939                .schemas
1940                .insert(new_name.to_string(), SchemaOverlay::Present(schema));
1941        }
1942        self.refresh_role_sensitive_search_path();
1943    }
1944
1945    fn restore_persistent_role_context(&mut self) {
1946        self.local.current_role = self.local.persistent_current_role.clone();
1947        self.local.current_role_known = self.local.persistent_current_role_known;
1948        self.local.session_role = self.local.persistent_session_role.clone();
1949        self.local.session_role_known = self.local.persistent_session_role_known;
1950        self.local.search_path_template = self.local.session_search_path_template.clone();
1951        self.local.lock_timeout.reset_effective_to_session();
1952        self.local.statement_timeout.reset_effective_to_session();
1953        self.refresh_role_sensitive_search_path();
1954    }
1955
1956    fn apply_grant_to_relation(
1957        &mut self,
1958        id: &ObjectId,
1959        privileges: &HashSet<Privilege>,
1960        grantees: &[ObjectId],
1961    ) {
1962        self.snapshot_relation(id);
1963        if let Some(RelationOverlay::Present(rel)) = self.local.relations.get_mut(id) {
1964            for grantee in grantees {
1965                rel.privileges.grant(grantee.clone(), privileges.clone());
1966            }
1967        }
1968    }
1969
1970    fn apply_revoke_to_relation(
1971        &mut self,
1972        id: &ObjectId,
1973        privileges: &HashSet<Privilege>,
1974        revokees: &[ObjectId],
1975    ) {
1976        self.snapshot_relation(id);
1977        if let Some(RelationOverlay::Present(rel)) = self.local.relations.get_mut(id) {
1978            for revokee in revokees {
1979                rel.privileges.revoke(revokee, privileges);
1980            }
1981        }
1982    }
1983
1984    pub fn apply(
1985        &mut self,
1986        mutation: &Mutation,
1987        precomputed_cascade: Option<&CascadeResult>,
1988    ) -> MutationResult {
1989        if self.local.transaction_aborted
1990            && !matches!(
1991                mutation,
1992                Mutation::CommitTransaction
1993                    | Mutation::CommitAndChain
1994                    | Mutation::RollbackTransaction
1995                    | Mutation::RollbackAndChain
1996                    | Mutation::RollbackToSavepoint(_)
1997            )
1998        {
1999            return MutationResult::NotExecuted;
2000        }
2001
2002        let result = self.apply_inner(mutation, precomputed_cascade);
2003        if matches!(result, MutationResult::Conflict { .. }) && !self.local.transactions.is_empty()
2004        {
2005            self.local.transaction_aborted = true;
2006        }
2007        result
2008    }
2009
2010    fn apply_inner(
2011        &mut self,
2012        mutation: &Mutation,
2013        precomputed_cascade: Option<&CascadeResult>,
2014    ) -> MutationResult {
2015        match mutation {
2016            Mutation::CreateSchema(create_schema) => self.apply_create_schema(create_schema),
2017            Mutation::AlterSchema(alter_schema) => self.apply_alter_schema(alter_schema),
2018            Mutation::DropSchema(drop_schema) => self.apply_drop_schema(drop_schema),
2019            Mutation::DropTable(drop) => self.apply_drop_table(drop, precomputed_cascade),
2020            Mutation::CreateTable(create) => self.apply_create_table(create),
2021            Mutation::CreateView(create) => self.apply_create_view(create),
2022            Mutation::CreateMaterializedView(create) => self.apply_create_materialized_view(create),
2023            Mutation::RefreshMaterializedView(refresh) => {
2024                self.apply_refresh_materialized_view(refresh)
2025            }
2026            Mutation::CreateIndex(create) => self.apply_create_index(create),
2027            Mutation::CreatePolicy(create_policy) => self.apply_create_policy(create_policy),
2028            Mutation::DropPolicy(drop_policy) => self.apply_drop_policy(drop_policy),
2029            Mutation::CreateTrigger(create_trigger) => self.apply_create_trigger(create_trigger),
2030            Mutation::DropTrigger(drop_trigger) => self.apply_drop_trigger(drop_trigger),
2031            Mutation::RenameTrigger(rename_trigger) => self.apply_rename_trigger(rename_trigger),
2032            Mutation::AlterTable(alter) => self.apply_alter_table(alter),
2033            Mutation::CreateType(create) => self.apply_create_type(create),
2034            Mutation::RenameType(rename) => self.apply_rename_type(rename),
2035            Mutation::AlterType(alter) => self.apply_alter_type(alter),
2036            Mutation::CreateDomain(create) => self.apply_create_domain(create),
2037            Mutation::AlterDomain(alter) => self.apply_alter_domain(alter),
2038            Mutation::DropDomain(drop) => self.apply_drop_domain(drop),
2039            Mutation::DropType(drop) => self.apply_drop_type(drop),
2040            Mutation::CreateSequence(create) => self.apply_create_sequence(create),
2041            Mutation::AlterSequence(alter) => self.apply_alter_sequence(alter),
2042            Mutation::DropSequence(drop) => self.apply_drop_sequence(drop),
2043            Mutation::Rename(rename) => self.apply_rename_relation(rename),
2044            Mutation::DropView(drop) => self.apply_drop_view(drop),
2045            Mutation::DropMaterializedView(drop) => self.apply_drop_materialized_view(drop),
2046            Mutation::DropIndex(drop) => self.apply_drop_index(drop),
2047            Mutation::ChangeRelationOwner { id, new_owner } => {
2048                self.apply_change_relation_owner(id, new_owner)
2049            }
2050            Mutation::SearchPath(search_path) => self.apply_search_path(search_path),
2051            Mutation::TimeoutSetting(timeout) => self.apply_timeout_setting(timeout),
2052            Mutation::ResetSettings(target) => self.apply_reset_settings(target),
2053            Mutation::CheckTimeouts => self.apply_check_timeouts(),
2054            Mutation::SwitchRole {
2055                role,
2056                local,
2057                is_session_auth,
2058            } => self.apply_switch_role(role, *local, *is_session_auth),
2059            Mutation::BeginTransaction => self.apply_begin_transaction(),
2060            Mutation::CommitTransaction => self.apply_commit_transaction(false),
2061            Mutation::CommitAndChain => self.apply_commit_transaction(true),
2062            Mutation::RollbackTransaction => self.apply_rollback_transaction(false),
2063            Mutation::RollbackAndChain => self.apply_rollback_transaction(true),
2064            Mutation::RollbackToSavepoint(rollback) => self.apply_rollback_to_savepoint(rollback),
2065            Mutation::Savepoint(savepoint) => self.apply_savepoint(savepoint),
2066            Mutation::ReleaseSavepoint(release) => self.apply_release_savepoint(release),
2067            Mutation::Opaque(opaque) => self.apply_opaque(opaque),
2068            Mutation::CreateFunction(function) => self.apply_create_function(function),
2069            Mutation::AlterFunction(function) => self.apply_alter_function(function),
2070            Mutation::DropFunction(function) => self.apply_drop_function(function),
2071            Mutation::CreateProcedure(procedure) => self.apply_create_procedure(procedure),
2072            Mutation::AlterProcedure(procedure) => self.apply_alter_procedure(procedure),
2073            Mutation::DropProcedure(procedure) => self.apply_drop_procedure(procedure),
2074            Mutation::CreateAggregate(aggregate) => self.apply_create_aggregate(aggregate),
2075            Mutation::AlterAggregate(aggregate) => self.apply_alter_aggregate(aggregate),
2076            Mutation::DropAggregate(aggregate) => self.apply_drop_aggregate(aggregate),
2077            Mutation::CreatePublication(publication) => self.apply_create_publication(publication),
2078            Mutation::AlterPublication(publication) => self.apply_alter_publication(publication),
2079            Mutation::DropPublication(publication) => self.apply_drop_publication(publication),
2080            Mutation::CreateSubscription(subscription) => {
2081                self.apply_create_subscription(subscription)
2082            }
2083            Mutation::AlterSubscription(subscription) => {
2084                self.apply_alter_subscription(subscription)
2085            }
2086            Mutation::DropSubscription(subscription) => self.apply_drop_subscription(subscription),
2087            Mutation::CreateRole(role) => self.apply_create_role(role),
2088            Mutation::AlterRole(role) => self.apply_alter_role(role),
2089            Mutation::DropRole(role) => self.apply_drop_role(role),
2090            Mutation::Grant(grant) => self.apply_grant(grant),
2091            Mutation::Revoke(revoke) => self.apply_revoke(revoke),
2092            Mutation::CreateDatabase(create_database) => {
2093                self.apply_create_database(create_database)
2094            }
2095            Mutation::AlterDatabase(alter_database) => self.apply_alter_database(alter_database),
2096            Mutation::DropDatabase(drop_database) => self.apply_drop_database(drop_database),
2097            Mutation::Vacuum { table_id, is_full } => self.apply_vacuum(table_id, *is_full),
2098        }
2099    }
2100
2101    fn snapshot_relation(&mut self, id: &ObjectId) {
2102        if let Some(frame) = self.local.transactions.last_mut() {
2103            let previous = self.local.relations.get(id).cloned();
2104            frame.undo_log.push(StateChange::RelationSnapshot {
2105                id: id.clone(),
2106                previous: Box::new(previous),
2107            });
2108        }
2109    }
2110
2111    fn snapshot_schema(&mut self, name: &str) {
2112        if let Some(frame) = self.local.transactions.last_mut() {
2113            frame.undo_log.push(StateChange::SchemaSnapshot {
2114                name: name.to_string(),
2115                previous: self.local.schemas.get(name).cloned(),
2116            });
2117        }
2118    }
2119
2120    fn snapshot_namespace(&mut self) {
2121        if let Some(frame) = self.local.transactions.last_mut() {
2122            frame.undo_log.push(StateChange::NamespaceSnapshot(Box::new(
2123                NamespaceSnapshot {
2124                    schemas: self.local.schemas.clone(),
2125                    relations: self.local.relations.clone(),
2126                    types: self.local.types.clone(),
2127                    functions: self.local.functions.clone(),
2128                    sequences: self.local.sequences.clone(),
2129                    publications: self.local.publications.clone(),
2130                    triggers: self.local.triggers.clone(),
2131                    constraints: self.local.constraints.clone(),
2132                    graph: self.local.graph.edges().to_vec(),
2133                    pending_validation: self.local.pending_validation.clone(),
2134                    baseline_relations: self.baseline_relations.clone(),
2135                    baseline_indexes: self.baseline_indexes.clone(),
2136                    baseline_foreign_keys: self.baseline_foreign_keys.clone(),
2137                    baseline_fk_dependencies: self.baseline_fk_dependencies.clone(),
2138                    baseline_sequences: self.baseline_sequences.clone(),
2139                },
2140            )));
2141        }
2142    }
2143
2144    fn snapshot_type(&mut self, id: &ObjectId) {
2145        if let Some(frame) = self.local.transactions.last_mut() {
2146            let previous = self.local.types.get(id).cloned();
2147            frame.undo_log.push(StateChange::TypeSnapshot {
2148                id: id.clone(),
2149                previous,
2150            });
2151        }
2152    }
2153
2154    fn snapshot_sequence(&mut self, id: &ObjectId) {
2155        if let Some(frame) = self.local.transactions.last_mut() {
2156            let previous = self.local.sequences.get(id).cloned();
2157            frame.undo_log.push(StateChange::SequenceSnapshot {
2158                id: id.clone(),
2159                previous,
2160            });
2161        }
2162    }
2163
2164    fn move_function(&mut self, old_id: &ObjectId, new_id: &ObjectId) {
2165        self.snapshot_function(old_id);
2166        self.snapshot_function(new_id);
2167        if let Some(crate::model::function::FunctionOverlay::Present(mut function)) =
2168            self.local.functions.remove(old_id)
2169        {
2170            function.id = new_id.clone();
2171            self.local.functions.insert(
2172                new_id.clone(),
2173                crate::model::function::FunctionOverlay::Present(function),
2174            );
2175        }
2176
2177        self.snapshot_graph_full();
2178        self.local.graph.propagate_function_rename(old_id, new_id);
2179        self.local.graph.add_edge(DependencyEdge::new(
2180            old_id.clone(),
2181            new_id.clone(),
2182            DependencyKind::RenameTo,
2183        ));
2184    }
2185
2186    pub(super) fn validate_function_move(
2187        &mut self,
2188        old_id: &ObjectId,
2189        new_id: &ObjectId,
2190    ) -> Result<(), MutationResult> {
2191        if old_id == new_id {
2192            return Ok(());
2193        }
2194        self.ensure_schema_target(&new_id.schema)?;
2195        match self.local.functions.get(new_id) {
2196            Some(crate::model::function::FunctionOverlay::Present(_)) => {
2197                Err(MutationResult::Conflict {
2198                    reason: format!("routine '{}' already exists", new_id),
2199                })
2200            }
2201            // A prior DROP in this migration leaves a tombstone but the
2202            // namespace is available again, just as it is in PostgreSQL.
2203            Some(crate::model::function::FunctionOverlay::Dropped) => Ok(()),
2204            None => Ok(()),
2205        }
2206    }
2207
2208    fn snapshot_function(&mut self, id: &ObjectId) {
2209        if let Some(frame) = self.local.transactions.last_mut() {
2210            let previous = self.local.functions.get(id).cloned();
2211            frame.undo_log.push(StateChange::FunctionSnapshot {
2212                id: id.clone(),
2213                previous,
2214            });
2215        }
2216    }
2217
2218    fn snapshot_publication(&mut self, name: &str) {
2219        if let Some(frame) = self.local.transactions.last_mut() {
2220            let previous = self.local.publications.get(name).cloned();
2221            frame.undo_log.push(StateChange::PublicationSnapshot {
2222                id: ObjectId::new("", name),
2223                previous,
2224            });
2225        }
2226    }
2227
2228    fn snapshot_subscription(&mut self, name: &str) {
2229        if let Some(frame) = self.local.transactions.last_mut() {
2230            let previous = self.local.subscriptions.get(name).cloned();
2231            frame.undo_log.push(StateChange::SubscriptionSnapshot {
2232                id: ObjectId::new("", name),
2233                previous,
2234            });
2235        }
2236    }
2237
2238    fn snapshot_role(&mut self, id: &ObjectId) {
2239        if let Some(frame) = self.local.transactions.last_mut() {
2240            let previous = self.local.roles.get(id).cloned();
2241            frame.undo_log.push(StateChange::RoleSnapshot {
2242                id: id.clone(),
2243                previous,
2244            });
2245        }
2246    }
2247
2248    fn snapshot_trigger(&mut self, id: &ObjectId) {
2249        if let Some(frame) = self.local.transactions.last_mut() {
2250            let previous = self.local.triggers.get(id).cloned();
2251            frame.undo_log.push(StateChange::TriggerSnapshot {
2252                id: id.clone(),
2253                previous,
2254            });
2255        }
2256    }
2257
2258    fn snapshot_constraint(&mut self, table_id: &ObjectId, name: &str) {
2259        if let Some(frame) = self.local.transactions.last_mut() {
2260            let key = (table_id.clone(), name.to_string());
2261            let previous = self.local.constraints.get(&key).cloned();
2262            frame.undo_log.push(StateChange::ConstraintSnapshot {
2263                table_id: table_id.clone(),
2264                name: name.to_string(),
2265                previous,
2266            });
2267        }
2268    }
2269
2270    fn snapshot_role_context(&mut self) {
2271        if let Some(frame) = self.local.transactions.last_mut() {
2272            frame.undo_log.push(StateChange::RoleContextSnapshot {
2273                current_role: self.local.current_role.clone(),
2274                current_role_known: self.local.current_role_known,
2275                persistent_current_role: self.local.persistent_current_role.clone(),
2276                persistent_current_role_known: self.local.persistent_current_role_known,
2277                session_role: self.local.session_role.clone(),
2278                session_role_known: self.local.session_role_known,
2279                persistent_session_role: self.local.persistent_session_role.clone(),
2280                persistent_session_role_known: self.local.persistent_session_role_known,
2281            });
2282        }
2283    }
2284
2285    fn snapshot_search_path(&mut self) {
2286        if let Some(frame) = self.local.transactions.last_mut() {
2287            frame.undo_log.push(StateChange::SearchPathSnapshot {
2288                previous: self.local.search_path.clone(),
2289                previous_template: self.local.search_path_template.clone(),
2290                previous_session_template: self.local.session_search_path_template.clone(),
2291            });
2292        }
2293    }
2294
2295    fn snapshot_timeout_settings(&mut self) {
2296        if let Some(frame) = self.local.transactions.last_mut() {
2297            frame.undo_log.push(StateChange::TimeoutSettingsSnapshot {
2298                lock_timeout: self.local.lock_timeout.clone(),
2299                statement_timeout: self.local.statement_timeout.clone(),
2300            });
2301        }
2302    }
2303
2304    fn snapshot_generation_counter(&mut self) {
2305        if let Some(frame) = self.local.transactions.last_mut() {
2306            frame.undo_log.push(StateChange::GenerationCounterSnapshot {
2307                previous: self.local.generation_counter,
2308            });
2309        }
2310    }
2311
2312    #[allow(dead_code)]
2313    fn snapshot_pending_validation(&mut self) {
2314        if let Some(frame) = self.local.transactions.last_mut() {
2315            frame.undo_log.push(StateChange::PendingValidationSnapshot {
2316                previous: self.local.pending_validation.clone(),
2317            });
2318        }
2319    }
2320
2321    fn snapshot_confidence(&mut self) {
2322        if let Some(frame) = self.local.transactions.last_mut() {
2323            frame.undo_log.push(StateChange::ConfidenceSnapshot {
2324                previous: self.local.confidence.clone(),
2325            });
2326        }
2327    }
2328
2329    fn snapshot_graph(&mut self) {
2330        if let Some(frame) = self.local.transactions.last_mut() {
2331            frame.undo_log.push(StateChange::GraphLengthMarker {
2332                len: self.local.graph.edge_count(),
2333            });
2334        }
2335    }
2336
2337    fn snapshot_graph_full(&mut self) {
2338        if let Some(frame) = self.local.transactions.last_mut() {
2339            frame.undo_log.push(StateChange::GraphSnapshot {
2340                previous: self.local.graph.edges().to_vec(),
2341            });
2342        }
2343    }
2344
2345    fn rollback_frame(&mut self, mut frame: TransactionFrame) {
2346        self.rollback_undo_log(std::mem::take(&mut frame.undo_log));
2347    }
2348
2349    fn rollback_undo_log(&mut self, mut undo_log: Vec<StateChange>) {
2350        while let Some(change) = undo_log.pop() {
2351            match change {
2352                StateChange::SchemaSnapshot { name, previous } => match previous {
2353                    Some(overlay) => {
2354                        self.local.schemas.insert(name, overlay);
2355                    }
2356                    None => {
2357                        self.local.schemas.remove(&name);
2358                    }
2359                },
2360                StateChange::NamespaceSnapshot(snapshot) => {
2361                    self.local.schemas = snapshot.schemas;
2362                    self.local.relations = snapshot.relations;
2363                    self.local.types = snapshot.types;
2364                    self.local.functions = snapshot.functions;
2365                    self.local.sequences = snapshot.sequences;
2366                    self.local.publications = snapshot.publications;
2367                    self.local.triggers = snapshot.triggers;
2368                    self.local.constraints = snapshot.constraints;
2369                    self.local.graph.replace_edges(snapshot.graph);
2370                    self.local.pending_validation = snapshot.pending_validation;
2371                    self.baseline_relations = snapshot.baseline_relations;
2372                    self.baseline_indexes = snapshot.baseline_indexes;
2373                    self.baseline_foreign_keys = snapshot.baseline_foreign_keys;
2374                    self.baseline_fk_dependencies = snapshot.baseline_fk_dependencies;
2375                    self.baseline_sequences = snapshot.baseline_sequences;
2376                }
2377                StateChange::RelationSnapshot { id, previous } => {
2378                    if let Some(prev) = *previous {
2379                        self.local.relations.insert(id, prev);
2380                    } else {
2381                        self.local.relations.remove(&id);
2382                    }
2383                }
2384                StateChange::TypeSnapshot { id, previous } => {
2385                    if let Some(prev) = previous {
2386                        self.local.types.insert(id, prev);
2387                    } else {
2388                        self.local.types.remove(&id);
2389                    }
2390                }
2391                StateChange::SequenceSnapshot { id, previous } => {
2392                    if let Some(prev) = previous {
2393                        self.local.sequences.insert(id, prev);
2394                    } else {
2395                        self.local.sequences.remove(&id);
2396                    }
2397                }
2398                StateChange::FunctionSnapshot { id, previous } => {
2399                    if let Some(prev) = previous {
2400                        self.local.functions.insert(id, prev);
2401                    } else {
2402                        self.local.functions.remove(&id);
2403                    }
2404                }
2405                StateChange::PublicationSnapshot { id, previous } => {
2406                    if let Some(prev) = previous {
2407                        self.local.publications.insert(id.name, prev);
2408                    } else {
2409                        self.local.publications.remove(&id.name);
2410                    }
2411                }
2412                StateChange::SubscriptionSnapshot { id, previous } => {
2413                    if let Some(prev) = previous {
2414                        self.local.subscriptions.insert(id.name, prev);
2415                    } else {
2416                        self.local.subscriptions.remove(&id.name);
2417                    }
2418                }
2419                StateChange::RoleSnapshot { id, previous } => {
2420                    if let Some(prev) = previous {
2421                        self.local.roles.insert(id, prev);
2422                    } else {
2423                        self.local.roles.remove(&id);
2424                    }
2425                }
2426                StateChange::TriggerSnapshot { id, previous } => {
2427                    if let Some(prev) = previous {
2428                        self.local.triggers.insert(id, prev);
2429                    } else {
2430                        self.local.triggers.remove(&id);
2431                    }
2432                }
2433                StateChange::ConstraintSnapshot {
2434                    table_id,
2435                    name,
2436                    previous,
2437                } => {
2438                    let key = (table_id, name);
2439                    if let Some(previous) = previous {
2440                        self.local.constraints.insert(key, previous);
2441                    } else {
2442                        self.local.constraints.remove(&key);
2443                    }
2444                }
2445                StateChange::BaselineForeignKeysSnapshot { previous } => {
2446                    self.baseline_foreign_keys = previous;
2447                }
2448                StateChange::GraphLengthMarker { len } => {
2449                    self.local.graph.truncate(len);
2450                }
2451                StateChange::GraphSnapshot { previous } => {
2452                    self.local.graph.replace_edges(previous);
2453                }
2454                StateChange::RoleContextSnapshot {
2455                    current_role,
2456                    current_role_known,
2457                    persistent_current_role,
2458                    persistent_current_role_known,
2459                    session_role,
2460                    session_role_known,
2461                    persistent_session_role,
2462                    persistent_session_role_known,
2463                } => {
2464                    self.local.current_role = current_role;
2465                    self.local.current_role_known = current_role_known;
2466                    self.local.persistent_current_role = persistent_current_role;
2467                    self.local.persistent_current_role_known = persistent_current_role_known;
2468                    self.local.session_role = session_role;
2469                    self.local.session_role_known = session_role_known;
2470                    self.local.persistent_session_role = persistent_session_role;
2471                    self.local.persistent_session_role_known = persistent_session_role_known;
2472                }
2473                StateChange::SearchPathSnapshot {
2474                    previous,
2475                    previous_template,
2476                    previous_session_template,
2477                } => {
2478                    self.local.search_path = previous;
2479                    self.local.search_path_template = previous_template;
2480                    self.local.session_search_path_template = previous_session_template;
2481                }
2482                StateChange::TimeoutSettingsSnapshot {
2483                    lock_timeout,
2484                    statement_timeout,
2485                } => {
2486                    self.local.lock_timeout = lock_timeout;
2487                    self.local.statement_timeout = statement_timeout;
2488                }
2489                StateChange::GenerationCounterSnapshot { previous } => {
2490                    self.local.generation_counter = previous;
2491                }
2492                StateChange::PendingValidationSnapshot { previous } => {
2493                    self.local.pending_validation = previous;
2494                }
2495                StateChange::ConfidenceSnapshot { previous } => {
2496                    self.local.confidence = previous;
2497                }
2498            }
2499        }
2500    }
2501
2502    pub(crate) fn transaction_undo_checkpoint(&self) -> Option<(usize, usize)> {
2503        self.local
2504            .transactions
2505            .last()
2506            .map(|frame| (self.local.transactions.len(), frame.undo_log.len()))
2507    }
2508
2509    pub(crate) fn rollback_to_transaction_undo_checkpoint(
2510        &mut self,
2511        transaction_depth: usize,
2512        undo_len: usize,
2513    ) -> Result<(), &'static str> {
2514        if self.local.transactions.len() != transaction_depth {
2515            return Err("statement changed transaction depth while using an undo checkpoint");
2516        }
2517        let Some(frame) = self.local.transactions.last_mut() else {
2518            return Err("statement undo checkpoint lost its transaction frame");
2519        };
2520        if frame.undo_log.len() < undo_len {
2521            return Err("statement shortened the transaction undo log unexpectedly");
2522        }
2523        let statement_undo = frame.undo_log.split_off(undo_len);
2524        self.rollback_undo_log(statement_undo);
2525        Ok(())
2526    }
2527}