Skip to main content

safe_migrate/_internal/analysis/
state.rs

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