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 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 pub current_role: String,
119 pub current_role_known: bool,
122 pub persistent_current_role: String,
125 pub persistent_current_role_known: bool,
126 pub session_role: String,
130 pub session_role_known: bool,
133 pub persistent_session_role: String,
135 pub persistent_session_role_known: bool,
136 pub authenticated_role: String,
138 pub authenticated_role_known: bool,
139 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 pub baseline_available: bool,
247 pub baseline_boundary_queries_complete: bool,
251 pub baseline_coverage: CatalogCoverage,
254 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 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 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 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 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 pub fn try_new(cache: DbCache) -> Result<Self, String> {
590 Ok(Self::new(cache.validated()?))
591 }
592
593 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 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 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 let roles_known = baseline_available
823 && baseline_coverage.has(crate::_internal::db::cache::CatalogFamily::Roles)
824 && (!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 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_known: true,
938 },
939 ));
940 }
941
942 for dependency in cache.dependencies {
943 let dependent = dependency.dependent;
944 let referenced = dependency.referenced;
945 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 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 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 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 pub(crate) fn effective_pg_version_num(&self, fallback: u32) -> u32 {
1294 self.pg_version_num.unwrap_or(fallback)
1295 }
1296
1297 pub(crate) fn search_path(&self) -> &[String] {
1300 &self.local.search_path
1301 }
1302
1303 pub fn baseline_covers_object(&self, id: &ObjectId) -> bool {
1307 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 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 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 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 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 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 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 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 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(¤t, 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(¤t, 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 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 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 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 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 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 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 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 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 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}