1use crate::_internal::ast::identifiers::ObjectId;
2use crate::_internal::model::constraint::{ConstraintKind, ConstraintState};
3use crate::_internal::model::function::FunctionState;
4use crate::_internal::model::relation::{RelationKind, RelationState};
5use crate::_internal::model::replication::{PublicationState, SubscriptionState};
6use crate::_internal::model::role::{RoleMembershipGrantor, RoleState};
7use crate::_internal::model::schema::SchemaState;
8use crate::_internal::model::sequence::SequenceState;
9use crate::_internal::model::trigger::TriggerEnableMode;
10use crate::_internal::model::types::TypeState;
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeSet, HashMap, HashSet};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum CatalogFamily {
20 Schemas,
21 Relations,
22 Sequences,
23 Indexes,
24 Constraints,
25 Triggers,
26 Routines,
27 Types,
28 Dependencies,
29 Inheritance,
30 Roles,
31 Publications,
32 Subscriptions,
33}
34
35impl CatalogFamily {
36 pub const fn as_str(self) -> &'static str {
37 match self {
38 Self::Schemas => "schemas",
39 Self::Relations => "relations",
40 Self::Sequences => "sequences",
41 Self::Indexes => "indexes",
42 Self::Constraints => "constraints",
43 Self::Triggers => "triggers",
44 Self::Routines => "routines",
45 Self::Types => "types",
46 Self::Dependencies => "dependencies",
47 Self::Inheritance => "inheritance",
48 Self::Roles => "roles",
49 Self::Publications => "publications",
50 Self::Subscriptions => "subscriptions",
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum SchemaCoverage {
59 AllNonSystem,
60 Explicit(BTreeSet<String>),
61}
62
63impl SchemaCoverage {
64 pub fn from_sync_scope(schemas: Option<&[String]>) -> Self {
65 match schemas {
66 Some(schemas) => Self::Explicit(schemas.iter().cloned().collect()),
67 None => Self::AllNonSystem,
68 }
69 }
70
71 pub fn covers(&self, schema: &str) -> bool {
72 match self {
73 Self::AllNonSystem => true,
74 Self::Explicit(schemas) => schemas.contains(schema),
75 }
76 }
77
78 pub fn explicit_schemas(&self) -> Option<Vec<String>> {
79 match self {
80 Self::AllNonSystem => None,
81 Self::Explicit(schemas) => Some(schemas.iter().cloned().collect()),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct CatalogCoverage {
91 pub schema_scope: SchemaCoverage,
92 pub families: BTreeSet<CatalogFamily>,
93}
94
95impl CatalogCoverage {
96 pub fn from_sync_scope(schemas: Option<&[String]>) -> Self {
97 Self {
98 schema_scope: SchemaCoverage::from_sync_scope(schemas),
99 families: [
100 CatalogFamily::Schemas,
101 CatalogFamily::Relations,
102 CatalogFamily::Sequences,
103 CatalogFamily::Indexes,
104 CatalogFamily::Constraints,
105 CatalogFamily::Triggers,
106 CatalogFamily::Routines,
107 CatalogFamily::Types,
108 CatalogFamily::Dependencies,
109 CatalogFamily::Inheritance,
110 CatalogFamily::Roles,
111 CatalogFamily::Publications,
112 CatalogFamily::Subscriptions,
113 ]
114 .into_iter()
115 .collect(),
116 }
117 }
118
119 pub fn has(&self, family: CatalogFamily) -> bool {
120 self.families.contains(&family)
121 }
122
123 pub fn family_names(&self) -> impl Iterator<Item = &'static str> + '_ {
124 self.families.iter().copied().map(CatalogFamily::as_str)
125 }
126}
127
128impl Default for CatalogCoverage {
129 fn default() -> Self {
130 Self::from_sync_scope(None)
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct ForeignKeyCache {
139 pub constraint_name: String,
140 pub from_table: ObjectId,
141 pub to_table: ObjectId,
142 pub from_columns: Vec<String>,
145 pub to_columns: Vec<String>,
148 pub pk_fk_equality_operators: Vec<String>,
152 pub pk_pk_equality_operators: Vec<String>,
154 pub fk_fk_equality_operators: Vec<String>,
156}
157
158impl ForeignKeyCache {
159 pub fn has_complete_operator_evidence(&self) -> bool {
160 let count = self.from_columns.len();
161 count > 0
162 && self.to_columns.len() == count
163 && [
164 &self.pk_fk_equality_operators,
165 &self.pk_pk_equality_operators,
166 &self.fk_fk_equality_operators,
167 ]
168 .iter()
169 .all(|operators| {
170 operators.len() == count
171 && operators.iter().all(|operator| !operator.trim().is_empty())
172 })
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct ConstraintKeyCache {
181 pub table_id: ObjectId,
182 pub constraint_name: String,
183 pub columns: Vec<String>,
184 pub is_primary: bool,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct ConstraintDependencyCache {
192 pub table_id: ObjectId,
193 pub constraint_name: String,
194 pub columns: Vec<String>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct GeneratedColumnDependencyCache {
203 pub table_id: ObjectId,
204 pub column_name: String,
205 pub depends_on_column: String,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212pub struct DefaultSequenceDependencyCache {
213 pub table_id: ObjectId,
214 pub column_name: String,
215 pub sequence_id: ObjectId,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct IndexCache {
220 pub index_id: ObjectId,
221 pub table_id: ObjectId,
222 pub using_method: String,
223 pub key_columns: Vec<String>,
226 pub included_columns: Vec<String>,
228 pub dependency_columns: Vec<String>,
232 pub dependency_columns_known: bool,
235 pub has_expression_keys: bool,
236 pub has_predicate: bool,
237 pub is_unique: bool,
238 pub is_valid: bool,
239 pub is_ready: bool,
240 pub is_live: bool,
241 pub has_default_sort_order: bool,
242 pub has_default_opclasses: bool,
243 pub has_default_collations: bool,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct TriggerCache {
248 pub trigger_id: ObjectId,
249 pub table_id: ObjectId,
250 pub function_id: ObjectId,
251 pub enabled_mode: TriggerEnableMode,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub struct ViewDependencyCache {
263 pub dependent: ObjectId,
264 pub referenced: ObjectId,
265 pub referenced_column: Option<String>,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct InheritanceCache {
278 pub child: ObjectId,
279 pub parent: ObjectId,
280 pub sequence: i32,
281 pub is_partition: bool,
284 pub detach_pending: bool,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, Default)]
288pub struct CacheMetadata {
289 pub created_at_unix_secs: Option<u64>,
292 #[serde(default)]
296 pub boundary_queries_complete: bool,
297 pub source_database: Option<String>,
300 pub source_role: Option<String>,
303 pub source_session_role: Option<String>,
306 pub source_search_path: Option<Vec<String>>,
308 pub source_lock_timeout_ms: u64,
312 pub source_statement_timeout_ms: u64,
316 pub schemas: Option<Vec<String>>,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct DbCache {
323 pub pg_version_num: Option<u32>,
324 pub metadata: CacheMetadata,
325 pub coverage: CatalogCoverage,
326 pub search_path: Vec<String>,
327 pub relations: HashMap<ObjectId, RelationState>,
328 pub foreign_keys: Vec<ForeignKeyCache>,
329 pub indexes: Vec<IndexCache>,
330 pub constraints: Vec<ConstraintState>,
331 pub constraint_keys: Vec<ConstraintKeyCache>,
332 pub constraint_dependencies: Vec<ConstraintDependencyCache>,
333 pub generated_column_dependencies: Vec<GeneratedColumnDependencyCache>,
334 pub default_sequence_dependencies: Vec<DefaultSequenceDependencyCache>,
335 pub triggers: Vec<TriggerCache>,
336 pub functions: HashMap<ObjectId, FunctionState>,
337 pub types: HashMap<ObjectId, TypeState>,
338 pub roles: HashMap<ObjectId, RoleState>,
339 #[serde(default)]
341 pub role_membership_grantors: Vec<RoleMembershipGrantor>,
342 #[serde(default)]
344 pub role_membership_grantors_complete: bool,
345 pub schemas: HashMap<String, SchemaState>,
346 pub sequences: HashMap<ObjectId, SequenceState>,
347 pub dependencies: Vec<ViewDependencyCache>,
348 pub scoped_external_relation_dependencies: Vec<ObjectId>,
353 pub scoped_external_type_dependencies: Vec<ObjectId>,
354 pub scoped_external_routine_dependencies: Vec<ObjectId>,
355 pub inheritances: Vec<InheritanceCache>,
356 pub publications: HashMap<String, PublicationState>,
357 pub subscriptions: HashMap<String, SubscriptionState>,
358}
359
360pub const CACHE_FORMAT_VERSION: u32 = 7;
361
362pub const CACHE_V7_MAGIC: &[u8] = b"SMCACHE07";
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub enum DbCacheVersioned {
368 V1,
372 V2,
373 V3,
374 V4,
375 V5(Box<DbCache>),
376 V6(Box<DbCache>),
377 V7(Box<DbCache>),
378}
379
380impl DbCacheVersioned {
381 pub fn format_version(&self) -> u32 {
382 match self {
383 DbCacheVersioned::V1 => 1,
384 DbCacheVersioned::V2 => 2,
385 DbCacheVersioned::V3 => 3,
386 DbCacheVersioned::V4 => 4,
387 DbCacheVersioned::V5(_) => 5,
388 DbCacheVersioned::V6(_) => 6,
389 DbCacheVersioned::V7(_) => 7,
390 }
391 }
392
393 pub fn into_cache(self) -> Result<DbCache, String> {
394 match self {
395 DbCacheVersioned::V1
396 | DbCacheVersioned::V2
397 | DbCacheVersioned::V3
398 | DbCacheVersioned::V4
399 | DbCacheVersioned::V5(_)
400 | DbCacheVersioned::V6(_) => Err(
401 "This cache format is unsupported. Run `safe-migrate sync` to rebuild it."
402 .to_string(),
403 ),
404 DbCacheVersioned::V7(c) => {
405 c.validate_semantics()?;
406 Ok(*c)
407 }
408 }
409 }
410}
411
412impl Default for DbCache {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418impl DbCache {
419 pub fn new() -> Self {
420 Self {
421 pg_version_num: None,
422 metadata: CacheMetadata::default(),
423 coverage: CatalogCoverage::default(),
424 search_path: vec!["public".to_string()],
425 relations: HashMap::new(),
426 foreign_keys: Vec::new(),
427 indexes: Vec::new(),
428 constraints: Vec::new(),
429 constraint_keys: Vec::new(),
430 constraint_dependencies: Vec::new(),
431 generated_column_dependencies: Vec::new(),
432 default_sequence_dependencies: Vec::new(),
433 triggers: Vec::new(),
434 functions: HashMap::new(),
435 types: HashMap::new(),
436 roles: HashMap::new(),
437 role_membership_grantors: Vec::new(),
438 role_membership_grantors_complete: false,
439 schemas: HashMap::new(),
440 sequences: HashMap::new(),
441 dependencies: Vec::new(),
442 scoped_external_relation_dependencies: Vec::new(),
443 scoped_external_type_dependencies: Vec::new(),
444 scoped_external_routine_dependencies: Vec::new(),
445 inheritances: Vec::new(),
446 publications: HashMap::new(),
447 subscriptions: HashMap::new(),
448 }
449 }
450
451 pub fn insert_baseline(&mut self, id: ObjectId, state: RelationState) {
452 self.relations.insert(id, state);
453 }
454
455 pub fn baseline_relations(&self) -> impl Iterator<Item = (&ObjectId, &RelationState)> {
456 self.relations.iter()
457 }
458
459 fn role_membership_cycle(&self) -> Option<ObjectId> {
460 fn visit(
461 role_id: &ObjectId,
462 roles: &HashMap<ObjectId, RoleState>,
463 visiting: &mut HashSet<ObjectId>,
464 visited: &mut HashSet<ObjectId>,
465 ) -> Option<ObjectId> {
466 if visiting.contains(role_id) {
467 return Some(role_id.clone());
468 }
469 if !visited.insert(role_id.clone()) {
470 return None;
471 }
472 visiting.insert(role_id.clone());
473 if let Some(role) = roles.get(role_id) {
474 for parent in &role.member_of {
475 if let Some(cycle) = visit(parent, roles, visiting, visited) {
476 return Some(cycle);
477 }
478 }
479 }
480 visiting.remove(role_id);
481 None
482 }
483
484 let mut visiting = HashSet::new();
485 let mut visited = HashSet::new();
486 self.roles
487 .keys()
488 .find_map(|role_id| visit(role_id, &self.roles, &mut visiting, &mut visited))
489 }
490
491 pub fn validate_semantics(&self) -> Result<(), String> {
494 let validate_id = |label: &str, id: &ObjectId, require_schema: bool| {
498 if id.name.is_empty() {
499 return Err(format!("{label} has an empty object name"));
500 }
501 if require_schema && id.schema.is_empty() {
502 return Err(format!(
503 "{label} '{}' must include a schema identity",
504 id.name
505 ));
506 }
507 if id.inferred_schema {
508 return Err(format!(
509 "{label} '{}' is marked as an inferred schema identity",
510 id
511 ));
512 }
513 Ok(())
514 };
515 let validate_role_ref = |label: &str, id: &ObjectId| {
516 validate_id(label, id, false)?;
517 if !self.roles.is_empty() && !self.roles.contains_key(id) {
518 return Err(format!(
519 "{label} '{}' references a role absent from the synchronized role catalog",
520 id.name
521 ));
522 }
523 Ok(())
524 };
525
526 if let Some(schemas) = &self.metadata.schemas {
527 let mut seen = HashSet::new();
528 for schema in schemas {
529 if schema.is_empty() {
530 return Err("cache schema scope contains an empty schema identity".to_string());
531 }
532 if !seen.insert(schema) {
533 return Err(format!(
534 "cache schema scope contains duplicate schema '{}'",
535 schema
536 ));
537 }
538 }
539 }
540 for (label, value) in [
541 ("source database", self.metadata.source_database.as_deref()),
542 ("source role", self.metadata.source_role.as_deref()),
543 (
544 "source session role",
545 self.metadata.source_session_role.as_deref(),
546 ),
547 ] {
548 if value.is_some_and(str::is_empty) {
549 return Err(format!("cache metadata contains an empty {label} identity"));
550 }
551 }
552 if let Some(template) = &self.metadata.source_search_path
553 && template.iter().any(String::is_empty)
554 {
555 return Err(
556 "cache metadata source search path contains an empty schema identity".to_string(),
557 );
558 }
559 let mut search_path = HashSet::new();
560 for schema in &self.search_path {
561 if schema.is_empty() {
562 return Err("cache search path contains an empty schema identity".to_string());
563 }
564 if !search_path.insert(schema) {
565 return Err(format!(
566 "cache search path contains duplicate schema '{}'",
567 schema
568 ));
569 }
570 }
571 let required_families = [
572 CatalogFamily::Schemas,
573 CatalogFamily::Relations,
574 CatalogFamily::Sequences,
575 CatalogFamily::Indexes,
576 CatalogFamily::Constraints,
577 CatalogFamily::Triggers,
578 CatalogFamily::Routines,
579 CatalogFamily::Types,
580 CatalogFamily::Dependencies,
581 CatalogFamily::Inheritance,
582 CatalogFamily::Roles,
583 CatalogFamily::Publications,
584 CatalogFamily::Subscriptions,
585 ];
586 for family in required_families {
587 if !self.coverage.has(family) {
588 return Err(format!(
589 "Cache V7 coverage is missing the required '{}' catalog family",
590 family.as_str(),
591 ));
592 }
593 }
594 let mut scoped_external_relations = HashSet::new();
595 for id in &self.scoped_external_relation_dependencies {
596 validate_id("scoped external relation dependency identity", id, true)?;
597 if !scoped_external_relations.insert(id) {
598 return Err(format!(
599 "scoped external relation dependency '{}' appears more than once",
600 id
601 ));
602 }
603 if !matches!(self.coverage.schema_scope, SchemaCoverage::Explicit(_)) {
604 return Err(format!(
605 "scoped external relation dependency '{}' requires an explicit schema scope",
606 id
607 ));
608 }
609 if !self.coverage.schema_scope.covers(&id.schema) {
610 return Err(format!(
611 "scoped external relation dependency '{}' is outside the cache schema scope",
612 id
613 ));
614 }
615 }
616 for (label, ids) in [
617 (
618 "scoped external type dependency",
619 &self.scoped_external_type_dependencies,
620 ),
621 (
622 "scoped external routine dependency",
623 &self.scoped_external_routine_dependencies,
624 ),
625 ] {
626 let mut seen = HashSet::new();
627 for id in ids {
628 validate_id(label, id, true)?;
629 if !seen.insert(id) {
630 return Err(format!("{label} '{}' appears more than once", id));
631 }
632 if !matches!(self.coverage.schema_scope, SchemaCoverage::Explicit(_)) {
633 return Err(format!(
634 "{label} '{}' requires an explicit schema scope",
635 id
636 ));
637 }
638 if !self.coverage.schema_scope.covers(&id.schema) {
639 return Err(format!(
640 "{label} '{}' is outside the cache schema scope",
641 id
642 ));
643 }
644 }
645 }
646 let coverage_scope = self
647 .coverage
648 .schema_scope
649 .explicit_schemas()
650 .map(|schemas| schemas.into_iter().collect::<BTreeSet<_>>());
651 let metadata_scope = self
652 .metadata
653 .schemas
654 .as_ref()
655 .map(|schemas| schemas.iter().cloned().collect::<BTreeSet<_>>());
656 if coverage_scope != metadata_scope {
657 return Err(
658 "Cache V7 schema coverage disagrees with legacy metadata schema scope".to_string(),
659 );
660 }
661 for (id, relation) in &self.relations {
662 validate_id("relation cache identity", id, true)?;
663 validate_id("relation embedded identity", &relation.id, true)?;
664 validate_role_ref("relation owner identity", &relation.owner)?;
665 if id != &relation.id {
666 return Err(format!(
667 "relation cache key '{}' disagrees with embedded identity '{}'",
668 id, relation.id
669 ));
670 }
671 if relation.is_populated.is_some()
672 && !matches!(relation.kind, RelationKind::MaterializedView)
673 {
674 return Err(format!(
675 "relation '{}' carries materialized-view population state but is not a materialized view",
676 id
677 ));
678 }
679 let mut column_names = HashSet::new();
680 for column in &relation.columns {
681 if column.name.is_empty() {
682 return Err(format!(
683 "relation '{}' contains a column with an empty identity",
684 id
685 ));
686 }
687 if !column_names.insert(column.name.as_str()) {
688 return Err(format!(
689 "relation '{}' contains duplicate column '{}', which makes lookup ambiguous",
690 id, column.name
691 ));
692 }
693 }
694 for (grantee, privileges) in &relation.privileges.grants {
695 validate_id("relation privilege grantee", grantee, false)?;
696 if !grantee.schema.is_empty() {
697 return Err(format!(
698 "relation '{}' privilege grantee '{}' must use the cluster role namespace",
699 id, grantee
700 ));
701 }
702 if privileges.is_empty() {
703 return Err(format!(
704 "relation '{}' has an empty privilege set for grantee '{}'",
705 id, grantee
706 ));
707 }
708 if grantee.name != "public" && !self.roles.contains_key(grantee) {
709 return Err(format!(
710 "relation '{}' privilege references missing grantee role '{}'",
711 id, grantee
712 ));
713 }
714 }
715 for ((grantee, privilege), grantors) in &relation.privileges.grantors {
716 validate_id("relation privilege provenance grantee", grantee, false)?;
717 if !grantee.schema.is_empty() {
718 return Err(format!(
719 "relation '{}' privilege provenance grantee '{}' must use the cluster role namespace",
720 id, grantee
721 ));
722 }
723 if !relation.privileges.has_privilege(grantee, *privilege) {
724 return Err(format!(
725 "relation '{}' privilege provenance exists without the effective '{}' privilege for '{}'",
726 id,
727 format_args!("{privilege:?}"),
728 grantee
729 ));
730 }
731 for grantor in grantors {
732 validate_id("relation privilege provenance grantor", grantor, false)?;
733 if !grantor.schema.is_empty() {
734 return Err(format!(
735 "relation '{}' privilege provenance grantor '{}' must use the cluster role namespace",
736 id, grantor
737 ));
738 }
739 if grantor.name != "public" && !self.roles.contains_key(grantor) {
740 return Err(format!(
741 "relation '{}' privilege provenance references missing grantor role '{}'",
742 id, grantor
743 ));
744 }
745 }
746 }
747 for ((grantee, privilege), grantors) in &relation.privileges.grant_option_grantors {
748 validate_id("relation grant-option provenance grantee", grantee, false)?;
749 if !relation.privileges.has_grant_option(grantee, *privilege) {
750 return Err(format!(
751 "relation '{}' grant-option provenance exists without the grant option for '{}'",
752 id, grantee
753 ));
754 }
755 for grantor in grantors {
756 validate_id("relation grant-option provenance grantor", grantor, false)?;
757 if !grantor.schema.is_empty()
758 || (grantor.name != "public" && !self.roles.contains_key(grantor))
759 {
760 return Err(format!(
761 "relation '{}' grant-option provenance references invalid grantor role '{}'",
762 id, grantor
763 ));
764 }
765 }
766 }
767 }
768 for (id, function) in &self.functions {
769 validate_id("routine cache identity", id, true)?;
770 validate_id("routine embedded identity", &function.id, true)?;
771 if id != &function.id {
772 return Err(format!(
773 "routine cache key '{}' disagrees with embedded identity '{}'",
774 id, function.id
775 ));
776 }
777 }
778 for (id, ty) in &self.types {
779 validate_id("type cache identity", id, true)?;
780 validate_id("type embedded identity", &ty.id, true)?;
781 if id != &ty.id {
782 return Err(format!(
783 "type cache key '{}' disagrees with embedded identity '{}'",
784 id, ty.id
785 ));
786 }
787 }
788 for (id, role) in &self.roles {
789 validate_id("role cache identity", id, false)?;
790 validate_id("role embedded identity", &role.id, false)?;
791 if id != &role.id {
792 return Err(format!(
793 "role cache key '{}' disagrees with embedded identity '{}'",
794 id, role.id
795 ));
796 }
797 if !id.schema.is_empty() {
798 return Err(format!(
799 "role '{}' must use the cluster role namespace, not schema '{}'",
800 id.name, id.schema
801 ));
802 }
803 }
804 if let Some(role_id) = self.role_membership_cycle() {
805 return Err(format!(
806 "role membership graph contains a circular path at '{}'",
807 role_id.name
808 ));
809 }
810 for (name, schema) in &self.schemas {
811 if name.is_empty() {
812 return Err("schema cache contains an empty schema identity".to_string());
813 }
814 if schema.name.is_empty() {
815 return Err("schema cache contains an empty embedded schema identity".to_string());
816 }
817 validate_role_ref("schema owner identity", &schema.owner)?;
818 if name != &schema.name {
819 return Err(format!(
820 "schema cache key '{}' disagrees with embedded identity '{}'",
821 name, schema.name
822 ));
823 }
824 }
825 for (id, sequence) in &self.sequences {
826 validate_id("sequence cache identity", id, true)?;
827 validate_id("sequence embedded identity", &sequence.id, true)?;
828 validate_role_ref("sequence owner identity", &sequence.owner)?;
829 if id != &sequence.id {
830 return Err(format!(
831 "sequence cache key '{}' disagrees with embedded identity '{}'",
832 id, sequence.id
833 ));
834 }
835 if self.relations.contains_key(id) {
836 return Err(format!(
837 "sequence '{}' collides with another relation-namespace object",
838 id
839 ));
840 }
841 }
842 for (name, publication) in &self.publications {
843 if name.is_empty() || publication.name.is_empty() {
844 return Err("publication cache contains an empty identity".to_string());
845 }
846 if name != &publication.name {
847 return Err(format!(
848 "publication cache key '{}' disagrees with embedded identity '{}'",
849 name, publication.name
850 ));
851 }
852 if let Some(owner) = &publication.owner {
853 if owner.is_empty() {
854 return Err(format!(
855 "publication '{}' has an empty owner identity",
856 name
857 ));
858 }
859 if !self.roles.is_empty() && !self.roles.contains_key(&ObjectId::new("", owner)) {
860 return Err(format!(
861 "publication '{}' owner '{}' is absent from the synchronized role catalog",
862 name, owner
863 ));
864 }
865 }
866 match &publication.scope {
867 crate::_internal::analysis::facts::PublicationScope::AllTables { except } => {
868 let mut seen = HashSet::new();
869 for table in except {
870 let resolved =
871 crate::_internal::ast::identifiers::Ident::new(table, false).resolve();
872 if resolved.is_empty() || !seen.insert(resolved) {
873 return Err(format!(
874 "publication '{}' has an empty or duplicate EXCEPT table",
875 name
876 ));
877 }
878 }
879 }
880 crate::_internal::analysis::facts::PublicationScope::Explicit(objects) => {
881 let mut seen = HashSet::new();
882 for object in objects {
883 let key = match object {
884 crate::_internal::analysis::facts::PublicationObjectFact::Table {
885 name: table,
886 columns,
887 ..
888 } => {
889 let table_name = table.name.resolve();
890 let schema = table
891 .schema
892 .as_ref()
893 .map(|schema| schema.resolve());
894 if table_name.is_empty()
895 || schema.as_ref().is_some_and(String::is_empty)
896 {
897 return Err(format!(
898 "publication '{}' contains an empty table identity",
899 name
900 ));
901 }
902 if let Some(columns) = columns {
903 let mut column_names = HashSet::new();
904 for column in columns {
905 let resolved =
906 crate::_internal::ast::identifiers::Ident::new(column, false)
907 .resolve();
908 if resolved.is_empty() || !column_names.insert(resolved) {
909 return Err(format!(
910 "publication '{}' contains an empty or duplicate table column",
911 name
912 ));
913 }
914 }
915 }
916 format!(
917 "table:{}:{table_name}",
918 schema.unwrap_or_else(|| "<unqualified>".to_string())
919 )
920 }
921 crate::_internal::analysis::facts::PublicationObjectFact::SchemaTables {
922 schema,
923 ..
924 } => {
925 if schema.is_empty() {
926 return Err(format!(
927 "publication '{}' contains an empty schema identity",
928 name
929 ));
930 }
931 format!("schema:{schema}")
932 }
933 crate::_internal::analysis::facts::PublicationObjectFact::CurrentSchemaShorthand =>
934 "current_schema".to_string(),
935 crate::_internal::analysis::facts::PublicationObjectFact::Unknown => {
936 return Err(format!(
937 "publication '{}' contains unsupported unknown scope metadata",
938 name
939 ));
940 }
941 };
942 if !seen.insert(key) {
943 return Err(format!(
944 "publication '{}' contains duplicate scope metadata",
945 name
946 ));
947 }
948 }
949 }
950 }
951 }
952 for (name, subscription) in &self.subscriptions {
953 if name.is_empty() || subscription.name.is_empty() {
954 return Err("subscription cache contains an empty identity".to_string());
955 }
956 if name != &subscription.name {
957 return Err(format!(
958 "subscription cache key '{}' disagrees with embedded identity '{}'",
959 name, subscription.name
960 ));
961 }
962 if let Some(owner) = &subscription.owner {
963 if owner.is_empty() {
964 return Err(format!(
965 "subscription '{}' has an empty owner identity",
966 name
967 ));
968 }
969 if !self.roles.is_empty() && !self.roles.contains_key(&ObjectId::new("", owner)) {
970 return Err(format!(
971 "subscription '{}' owner '{}' is absent from the synchronized role catalog",
972 name, owner
973 ));
974 }
975 }
976 let mut publication_names = HashSet::new();
977 for publication in &subscription.publications {
978 if publication.is_empty() || !publication_names.insert(publication) {
979 return Err(format!(
980 "subscription '{}' contains an empty or duplicate publication name",
981 name
982 ));
983 }
984 }
985 }
986
987 for schema in &self.search_path {
988 if !self.schemas.is_empty() && !self.schemas.contains_key(schema) {
989 return Err(format!(
990 "effective search path references missing schema '{}'",
991 schema
992 ));
993 }
994 }
995
996 for (id, sequence) in &self.sequences {
997 if let Some((table_id, column_name)) = &sequence.owned_by {
998 validate_id("sequence ownership table identity", table_id, true)?;
999 if id.schema != table_id.schema {
1000 return Err(format!(
1001 "sequence '{}' must be in the same schema as owning table '{}'",
1002 id, table_id
1003 ));
1004 }
1005 if column_name.is_empty() {
1006 return Err(format!(
1007 "sequence '{}' ownership has an empty column identity",
1008 id
1009 ));
1010 }
1011 let Some(relation) = self.relations.get(table_id) else {
1012 let omitted_owner_schema =
1013 self.metadata.schemas.as_ref().is_some_and(|schemas| {
1014 !schemas.iter().any(|schema| schema == &table_id.schema)
1015 });
1016 if omitted_owner_schema {
1017 continue;
1018 }
1019 return Err(format!(
1020 "sequence '{}' ownership references missing relation '{}'",
1021 id, table_id
1022 ));
1023 };
1024 if !matches!(relation.kind, RelationKind::Table) {
1025 return Err(format!(
1026 "sequence '{}' ownership target '{}' is not a table",
1027 id, table_id
1028 ));
1029 }
1030 if !relation.has_column(column_name) {
1031 return Err(format!(
1032 "sequence '{}' ownership references missing column '{}.{}'",
1033 id, table_id, column_name
1034 ));
1035 }
1036 }
1037 }
1038
1039 for (id, role) in &self.roles {
1040 let mut memberships = HashSet::new();
1041 for target in role.member_of.iter().chain(&role.can_set_role_to) {
1042 validate_id("role membership target", target, false)?;
1043 if !target.schema.is_empty() {
1044 return Err(format!(
1045 "role '{}' membership target '{}' must use the cluster role namespace",
1046 id.name, target
1047 ));
1048 }
1049 if !self.roles.contains_key(target) {
1050 return Err(format!(
1051 "role '{}' membership references missing role '{}'",
1052 id, target
1053 ));
1054 }
1055 }
1056 for target in &role.member_of {
1057 if target == id {
1058 return Err(format!("role '{}' cannot be a member of itself", id.name));
1059 }
1060 if !memberships.insert(target) {
1061 return Err(format!(
1062 "role '{}' contains duplicate membership in '{}'",
1063 id.name, target.name
1064 ));
1065 }
1066 }
1067 let mut set_role_targets = HashSet::new();
1068 for target in &role.can_set_role_to {
1069 if !memberships.contains(target) {
1070 return Err(format!(
1071 "role '{}' has SET ROLE access to '{}' without membership",
1072 id.name, target.name
1073 ));
1074 }
1075 if !set_role_targets.insert(target) {
1076 return Err(format!(
1077 "role '{}' contains duplicate SET ROLE target '{}'",
1078 id.name, target.name
1079 ));
1080 }
1081 }
1082 let mut admin_targets = HashSet::new();
1083 for target in &role.can_administer_membership {
1084 if !memberships.contains(target) {
1085 return Err(format!(
1086 "role '{}' has ADMIN option for '{}' without membership",
1087 id.name, target.name
1088 ));
1089 }
1090 if !admin_targets.insert(target) {
1091 return Err(format!(
1092 "role '{}' contains duplicate ADMIN target '{}'",
1093 id.name, target.name
1094 ));
1095 }
1096 }
1097 let mut inherit_targets = HashSet::new();
1098 for target in &role.can_inherit_from {
1099 if !memberships.contains(target) {
1100 return Err(format!(
1101 "role '{}' has INHERIT option for '{}' without membership",
1102 id.name, target.name
1103 ));
1104 }
1105 if !inherit_targets.insert(target) {
1106 return Err(format!(
1107 "role '{}' contains duplicate INHERIT target '{}'",
1108 id.name, target.name
1109 ));
1110 }
1111 }
1112 }
1113
1114 let mut membership_grantors = HashSet::new();
1115 for provenance in &self.role_membership_grantors {
1116 validate_id("role membership grantee", &provenance.member, false)?;
1117 validate_id("role membership role", &provenance.role, false)?;
1118 validate_id("role membership grantor", &provenance.grantor, false)?;
1119 for id in [&provenance.member, &provenance.role, &provenance.grantor] {
1120 if !id.schema.is_empty() {
1121 return Err(format!(
1122 "role membership provenance '{}' must use the cluster role namespace",
1123 id
1124 ));
1125 }
1126 if !self.roles.contains_key(id) {
1127 return Err(format!(
1128 "role membership provenance references missing role '{}'",
1129 id
1130 ));
1131 }
1132 }
1133 let Some(member) = self.roles.get(&provenance.member) else {
1134 unreachable!("validated role provenance member disappeared")
1135 };
1136 if !member.member_of.contains(&provenance.role) {
1137 return Err(format!(
1138 "role membership provenance '{}' -> '{}' has no membership edge",
1139 provenance.member, provenance.role
1140 ));
1141 }
1142 if !membership_grantors.insert((provenance.member.clone(), provenance.role.clone())) {
1143 return Err(format!(
1144 "duplicate role membership provenance for '{}' -> '{}'",
1145 provenance.member, provenance.role
1146 ));
1147 }
1148 }
1149 if self.role_membership_grantors_complete {
1150 for (member_id, role) in &self.roles {
1151 for role_id in &role.member_of {
1152 if !membership_grantors.contains(&(member_id.clone(), role_id.clone())) {
1153 return Err(format!(
1154 "complete role membership provenance is missing for '{}' -> '{}'",
1155 member_id, role_id
1156 ));
1157 }
1158 }
1159 }
1160 }
1161
1162 let mut constraint_ids = HashSet::new();
1163 for constraint in &self.constraints {
1164 validate_id("constraint table identity", &constraint.table_id, true)?;
1165 if constraint.name.is_empty() {
1166 return Err(format!(
1167 "constraint on '{}' has an empty constraint name",
1168 constraint.table_id
1169 ));
1170 }
1171 let Some(relation) = self.relations.get(&constraint.table_id) else {
1172 return Err(format!(
1173 "constraint '{}.{}' references a missing relation",
1174 constraint.table_id, constraint.name
1175 ));
1176 };
1177 if !matches!(relation.kind, RelationKind::Table) {
1178 return Err(format!(
1179 "constraint '{}.{}' targets a non-table relation",
1180 constraint.table_id, constraint.name
1181 ));
1182 }
1183 if !constraint_ids.insert((constraint.table_id.clone(), constraint.name.clone())) {
1184 return Err(format!(
1185 "constraint '{}.{}' appears more than once",
1186 constraint.table_id, constraint.name
1187 ));
1188 }
1189 if let Some(backing_index) = &constraint.backing_index {
1190 if !matches!(
1191 constraint.kind,
1192 ConstraintKind::PrimaryKey | ConstraintKind::Unique | ConstraintKind::Exclusion
1193 ) {
1194 return Err(format!(
1195 "constraint '{}.{}' has a backing index but is not a key or exclusion constraint",
1196 constraint.table_id, constraint.name
1197 ));
1198 }
1199 validate_id("constraint backing index identity", backing_index, true)?;
1200 let Some(index) = self.indexes.iter().find(|index| {
1201 index.index_id == *backing_index && index.table_id == constraint.table_id
1202 }) else {
1203 return Err(format!(
1204 "constraint '{}.{}' references missing or unrelated backing index '{}'",
1205 constraint.table_id, constraint.name, backing_index
1206 ));
1207 };
1208 if !index.is_valid || !index.is_ready || !index.is_live {
1209 return Err(format!(
1210 "constraint '{}.{}' references an unusable backing index '{}'",
1211 constraint.table_id, constraint.name, backing_index
1212 ));
1213 }
1214 if matches!(
1215 constraint.kind,
1216 ConstraintKind::PrimaryKey | ConstraintKind::Unique
1217 ) && !index.is_unique
1218 {
1219 return Err(format!(
1220 "constraint '{}.{}' references non-unique backing index '{}'",
1221 constraint.table_id, constraint.name, backing_index
1222 ));
1223 }
1224 }
1225 }
1226
1227 let mut constraint_key_ids = HashSet::new();
1228 for key in &self.constraint_keys {
1229 validate_id("constraint key table identity", &key.table_id, true)?;
1230 if key.constraint_name.is_empty() {
1231 return Err(format!(
1232 "constraint key on '{}' has an empty constraint name",
1233 key.table_id
1234 ));
1235 }
1236 let Some(relation) = self.relations.get(&key.table_id) else {
1237 return Err(format!(
1238 "constraint key '{}.{}' references a missing relation",
1239 key.table_id, key.constraint_name
1240 ));
1241 };
1242 if !matches!(relation.kind, RelationKind::Table) {
1243 return Err(format!(
1244 "constraint key '{}.{}' targets a non-table relation",
1245 key.table_id, key.constraint_name
1246 ));
1247 }
1248 let valid_target_kind = self.constraints.iter().any(|constraint| {
1249 constraint.table_id == key.table_id
1250 && constraint.name == key.constraint_name
1251 && (constraint.kind == ConstraintKind::PrimaryKey
1252 || constraint.kind == ConstraintKind::Unique
1253 || constraint.kind == ConstraintKind::NotNull)
1254 });
1255 if !valid_target_kind {
1256 return Err(format!(
1257 "constraint key '{}.{}' has no matching primary, unique, or not-null constraint",
1258 key.table_id, key.constraint_name
1259 ));
1260 }
1261 if key.columns.is_empty() {
1262 return Err(format!(
1263 "constraint key '{}.{}' has no column identities",
1264 key.table_id, key.constraint_name
1265 ));
1266 }
1267 let mut key_columns = HashSet::new();
1268 if let Some(column) = key
1269 .columns
1270 .iter()
1271 .find(|column| !key_columns.insert(column.as_str()))
1272 {
1273 return Err(format!(
1274 "constraint key '{}.{}' repeats column '{}'",
1275 key.table_id, key.constraint_name, column
1276 ));
1277 }
1278 for column in &key.columns {
1279 if !relation.has_column(column) {
1280 return Err(format!(
1281 "constraint key '{}.{}' references missing column '{}.{}'",
1282 key.table_id, key.constraint_name, key.table_id, column
1283 ));
1284 }
1285 }
1286 if !constraint_key_ids.insert((key.table_id.clone(), key.constraint_name.clone())) {
1287 return Err(format!(
1288 "constraint key '{}.{}' appears more than once",
1289 key.table_id, key.constraint_name
1290 ));
1291 }
1292 }
1293
1294 let mut index_ids = HashSet::new();
1295 for index in &self.indexes {
1296 validate_id("index identity", &index.index_id, true)?;
1297 validate_id("index table identity", &index.table_id, true)?;
1298 if index.index_id.schema != index.table_id.schema {
1299 return Err(format!(
1300 "index '{}' must be in the same schema as indexed relation '{}'",
1301 index.index_id, index.table_id
1302 ));
1303 }
1304 if self.relations.contains_key(&index.index_id)
1305 || self.sequences.contains_key(&index.index_id)
1306 {
1307 return Err(format!(
1308 "index '{}' collides with another relation-namespace object",
1309 index.index_id
1310 ));
1311 }
1312 if index.using_method.is_empty() {
1313 return Err(format!(
1314 "index '{}' has an empty access method",
1315 index.index_id
1316 ));
1317 }
1318 let Some(relation) = self.relations.get(&index.table_id) else {
1319 return Err(format!(
1320 "index '{}' references missing relation '{}'",
1321 index.index_id, index.table_id
1322 ));
1323 };
1324 if !matches!(
1325 relation.kind,
1326 RelationKind::Table | RelationKind::MaterializedView
1327 ) {
1328 return Err(format!(
1329 "index '{}' targets a non-indexable relation '{}'",
1330 index.index_id, index.table_id
1331 ));
1332 }
1333 if !index_ids.insert(index.index_id.clone()) {
1334 return Err(format!("index '{}' appears more than once", index.index_id));
1335 }
1336 if !index.has_expression_keys && index.key_columns.is_empty() {
1337 return Err(format!(
1338 "index '{}' has no key column identities",
1339 index.index_id
1340 ));
1341 }
1342 if !index.dependency_columns_known {
1343 return Err(format!(
1344 "index '{}' is missing complete dependency-column evidence",
1345 index.index_id
1346 ));
1347 }
1348 for column in index
1349 .key_columns
1350 .iter()
1351 .chain(&index.included_columns)
1352 .chain(&index.dependency_columns)
1353 {
1354 if !relation.has_column(column) {
1355 return Err(format!(
1356 "index '{}' references missing column '{}.{}'",
1357 index.index_id, index.table_id, column
1358 ));
1359 }
1360 }
1361 }
1362
1363 let mut trigger_ids = HashSet::new();
1364 for trigger in &self.triggers {
1365 validate_id("trigger identity", &trigger.trigger_id, true)?;
1366 validate_id("trigger table identity", &trigger.table_id, true)?;
1367 validate_id("trigger function identity", &trigger.function_id, true)?;
1368 if trigger.trigger_id.schema != trigger.table_id.schema {
1369 return Err(format!(
1370 "trigger '{}' must be in the same schema as trigger table '{}'",
1371 trigger.trigger_id, trigger.table_id
1372 ));
1373 }
1374 let Some(relation) = self.relations.get(&trigger.table_id) else {
1375 return Err(format!(
1376 "trigger '{}' references missing relation '{}'",
1377 trigger.trigger_id, trigger.table_id
1378 ));
1379 };
1380 if !matches!(relation.kind, RelationKind::Table | RelationKind::View) {
1381 return Err(format!(
1382 "trigger '{}' targets a relation kind that cannot have triggers",
1383 trigger.trigger_id
1384 ));
1385 }
1386 if !trigger_ids.insert(trigger.trigger_id.clone()) {
1387 return Err(format!(
1388 "trigger '{}' appears more than once",
1389 trigger.trigger_id
1390 ));
1391 }
1392 }
1393
1394 let mut foreign_key_ids = HashSet::new();
1395 for foreign_key in &self.foreign_keys {
1396 validate_id("foreign-key source identity", &foreign_key.from_table, true)?;
1397 validate_id("foreign-key target identity", &foreign_key.to_table, true)?;
1398 if foreign_key.constraint_name.is_empty() {
1399 return Err(format!(
1400 "foreign key on '{}' has an empty constraint name",
1401 foreign_key.from_table
1402 ));
1403 }
1404 let Some(from_relation) = self.relations.get(&foreign_key.from_table) else {
1405 return Err(format!(
1406 "foreign key '{}.{}' references a missing relation",
1407 foreign_key.from_table, foreign_key.constraint_name
1408 ));
1409 };
1410 let Some(to_relation) = self.relations.get(&foreign_key.to_table) else {
1411 return Err(format!(
1412 "foreign key '{}.{}' references a missing relation",
1413 foreign_key.from_table, foreign_key.constraint_name
1414 ));
1415 };
1416 if !matches!(from_relation.kind, RelationKind::Table)
1417 || !matches!(to_relation.kind, RelationKind::Table)
1418 {
1419 return Err(format!(
1420 "foreign key '{}.{}' must reference tables",
1421 foreign_key.from_table, foreign_key.constraint_name
1422 ));
1423 }
1424 if foreign_key.from_columns.is_empty() || foreign_key.to_columns.is_empty() {
1425 return Err(format!(
1426 "foreign key '{}.{}' is missing ordered column identities",
1427 foreign_key.from_table, foreign_key.constraint_name
1428 ));
1429 }
1430 if foreign_key.from_columns.len() != foreign_key.to_columns.len() {
1431 return Err(format!(
1432 "foreign key '{}.{}' has {} source columns but {} referenced columns",
1433 foreign_key.from_table,
1434 foreign_key.constraint_name,
1435 foreign_key.from_columns.len(),
1436 foreign_key.to_columns.len()
1437 ));
1438 }
1439 let column_count = foreign_key.from_columns.len();
1440 for (label, operators) in [
1441 ("PK/FK", &foreign_key.pk_fk_equality_operators),
1442 ("PK/PK", &foreign_key.pk_pk_equality_operators),
1443 ("FK/FK", &foreign_key.fk_fk_equality_operators),
1444 ] {
1445 if operators.len() != column_count
1446 || operators.iter().any(|operator| operator.trim().is_empty())
1447 {
1448 return Err(format!(
1449 "foreign key '{}.{}' has incomplete {} equality-operator evidence",
1450 foreign_key.from_table, foreign_key.constraint_name, label
1451 ));
1452 }
1453 }
1454 let mut source_columns = HashSet::new();
1455 if let Some(column) = foreign_key
1456 .from_columns
1457 .iter()
1458 .find(|column| !source_columns.insert(column.as_str()))
1459 {
1460 return Err(format!(
1461 "foreign key '{}.{}' repeats source column '{}'",
1462 foreign_key.from_table, foreign_key.constraint_name, column
1463 ));
1464 }
1465 let mut target_columns = HashSet::new();
1466 if let Some(column) = foreign_key
1467 .to_columns
1468 .iter()
1469 .find(|column| !target_columns.insert(column.as_str()))
1470 {
1471 return Err(format!(
1472 "foreign key '{}.{}' repeats referenced column '{}'",
1473 foreign_key.from_table, foreign_key.constraint_name, column
1474 ));
1475 }
1476 for column in &foreign_key.from_columns {
1477 if !from_relation.has_column(column) {
1478 return Err(format!(
1479 "foreign key '{}.{}' references missing source column '{}.{}'",
1480 foreign_key.from_table,
1481 foreign_key.constraint_name,
1482 foreign_key.from_table,
1483 column
1484 ));
1485 }
1486 }
1487 for column in &foreign_key.to_columns {
1488 if !to_relation.has_column(column) {
1489 return Err(format!(
1490 "foreign key '{}.{}' references missing target column '{}.{}'",
1491 foreign_key.from_table,
1492 foreign_key.constraint_name,
1493 foreign_key.to_table,
1494 column
1495 ));
1496 }
1497 }
1498 if !foreign_key_ids.insert((
1499 foreign_key.from_table.clone(),
1500 foreign_key.constraint_name.clone(),
1501 )) {
1502 return Err(format!(
1503 "foreign key '{}.{}' appears more than once",
1504 foreign_key.from_table, foreign_key.constraint_name
1505 ));
1506 }
1507 if !self.constraints.iter().any(|constraint| {
1508 constraint.table_id == foreign_key.from_table
1509 && constraint.name == foreign_key.constraint_name
1510 && matches!(
1511 constraint.kind,
1512 crate::_internal::model::constraint::ConstraintKind::ForeignKey
1513 )
1514 }) {
1515 return Err(format!(
1516 "foreign key '{}.{}' has no matching constraint",
1517 foreign_key.from_table, foreign_key.constraint_name
1518 ));
1519 }
1520 }
1521
1522 let mut inheritance_pairs = HashSet::new();
1523 for inheritance in &self.inheritances {
1524 validate_id("inheritance child identity", &inheritance.child, true)?;
1525 validate_id("inheritance parent identity", &inheritance.parent, true)?;
1526 if inheritance.child == inheritance.parent || inheritance.sequence < 1 {
1527 return Err(format!(
1528 "inheritance '{} -> {}' has invalid direct relationship metadata",
1529 inheritance.child, inheritance.parent
1530 ));
1531 }
1532 if inheritance.detach_pending {
1533 return Err(format!(
1534 "inheritance '{} -> {}' is being detached; synchronize after the detach completes",
1535 inheritance.child, inheritance.parent
1536 ));
1537 }
1538 let omitted_schema = |schema: &str| {
1539 self.metadata
1540 .schemas
1541 .as_ref()
1542 .is_some_and(|schemas| !schemas.iter().any(|known| known == schema))
1543 };
1544 for id in [&inheritance.child, &inheritance.parent] {
1545 if let Some(relation) = self.relations.get(id) {
1546 if !matches!(relation.kind, RelationKind::Table) {
1547 return Err(format!(
1548 "inheritance '{} -> {}' must reference tables",
1549 inheritance.child, inheritance.parent
1550 ));
1551 }
1552 } else if !omitted_schema(&id.schema) {
1553 return Err(format!(
1554 "inheritance '{} -> {}' references a missing relation '{}'",
1555 inheritance.child, inheritance.parent, id
1556 ));
1557 }
1558 }
1559 if !inheritance_pairs.insert((
1560 inheritance.child.clone(),
1561 inheritance.parent.clone(),
1562 inheritance.sequence,
1563 )) {
1564 return Err(format!(
1565 "inheritance '{} -> {}' appears more than once at sequence {}",
1566 inheritance.child, inheritance.parent, inheritance.sequence
1567 ));
1568 }
1569 }
1570
1571 let mut constraint_dependency_ids = HashSet::new();
1572 for dependency in &self.constraint_dependencies {
1573 validate_id(
1574 "constraint dependency table identity",
1575 &dependency.table_id,
1576 true,
1577 )?;
1578 if dependency.constraint_name.is_empty() {
1579 return Err(format!(
1580 "constraint dependency on '{}' has an empty constraint name",
1581 dependency.table_id
1582 ));
1583 }
1584 if !constraint_dependency_ids.insert((
1585 dependency.table_id.clone(),
1586 dependency.constraint_name.clone(),
1587 )) {
1588 return Err(format!(
1589 "constraint dependency '{}.{}' appears more than once",
1590 dependency.table_id, dependency.constraint_name
1591 ));
1592 }
1593 let Some(relation) = self.relations.get(&dependency.table_id) else {
1594 return Err(format!(
1595 "constraint dependency '{}' references missing relation '{}'",
1596 dependency.constraint_name, dependency.table_id
1597 ));
1598 };
1599 if !self.constraints.iter().any(|constraint| {
1600 constraint.table_id == dependency.table_id
1601 && constraint.name == dependency.constraint_name
1602 && matches!(
1603 constraint.kind,
1604 crate::_internal::model::constraint::ConstraintKind::Check
1605 | crate::_internal::model::constraint::ConstraintKind::Exclusion
1606 )
1607 }) {
1608 return Err(format!(
1609 "constraint dependency '{}.{}' has no matching CHECK or EXCLUDE constraint",
1610 dependency.table_id, dependency.constraint_name
1611 ));
1612 }
1613 for column in &dependency.columns {
1614 if !relation.has_column(column) {
1615 return Err(format!(
1616 "constraint dependency '{}.{}' references missing column '{}.{}'",
1617 dependency.table_id,
1618 dependency.constraint_name,
1619 dependency.table_id,
1620 column
1621 ));
1622 }
1623 }
1624 }
1625
1626 let mut generated_dependency_ids = HashSet::new();
1627 for dependency in &self.generated_column_dependencies {
1628 validate_id(
1629 "generated dependency table identity",
1630 &dependency.table_id,
1631 true,
1632 )?;
1633 if dependency.column_name.is_empty() || dependency.depends_on_column.is_empty() {
1634 return Err(format!(
1635 "generated dependency on '{}' has an empty column identity",
1636 dependency.table_id
1637 ));
1638 }
1639 if !generated_dependency_ids.insert((
1640 dependency.table_id.clone(),
1641 dependency.column_name.clone(),
1642 dependency.depends_on_column.clone(),
1643 )) {
1644 return Err(format!(
1645 "generated column dependency '{}.{} -> {}' appears more than once",
1646 dependency.table_id, dependency.column_name, dependency.depends_on_column
1647 ));
1648 }
1649 if dependency.column_name == dependency.depends_on_column {
1650 return Err(format!(
1651 "generated column dependency '{}.{}' cannot depend on itself",
1652 dependency.table_id, dependency.column_name
1653 ));
1654 }
1655 let Some(relation) = self.relations.get(&dependency.table_id) else {
1656 return Err(format!(
1657 "generated column dependency references missing relation '{}'",
1658 dependency.table_id
1659 ));
1660 };
1661 if !relation.has_column(&dependency.column_name) {
1662 return Err(format!(
1663 "generated column dependency references missing generated column '{}.{}'",
1664 dependency.table_id, dependency.column_name
1665 ));
1666 }
1667 if !relation.has_column(&dependency.depends_on_column) {
1668 return Err(format!(
1669 "generated column dependency '{}.{}' references missing source column '{}.{}'",
1670 dependency.table_id,
1671 dependency.column_name,
1672 dependency.table_id,
1673 dependency.depends_on_column
1674 ));
1675 }
1676 }
1677
1678 let mut default_sequence_dependency_ids = HashSet::new();
1679 for dependency in &self.default_sequence_dependencies {
1680 validate_id(
1681 "default dependency table identity",
1682 &dependency.table_id,
1683 true,
1684 )?;
1685 validate_id(
1686 "default dependency sequence identity",
1687 &dependency.sequence_id,
1688 true,
1689 )?;
1690 if dependency.column_name.is_empty() {
1691 return Err(format!(
1692 "default sequence dependency on '{}' has an empty column identity",
1693 dependency.table_id
1694 ));
1695 }
1696 if !default_sequence_dependency_ids.insert((
1697 dependency.table_id.clone(),
1698 dependency.column_name.clone(),
1699 dependency.sequence_id.clone(),
1700 )) {
1701 return Err(format!(
1702 "default sequence dependency '{}.{} -> {}' appears more than once",
1703 dependency.table_id, dependency.column_name, dependency.sequence_id
1704 ));
1705 }
1706 let Some(relation) = self.relations.get(&dependency.table_id) else {
1707 return Err(format!(
1708 "default sequence dependency references missing relation '{}'",
1709 dependency.table_id
1710 ));
1711 };
1712 if !relation.has_column(&dependency.column_name) {
1713 return Err(format!(
1714 "default sequence dependency '{}.{}' references missing column",
1715 dependency.table_id, dependency.column_name
1716 ));
1717 }
1718 if !self.sequences.contains_key(&dependency.sequence_id)
1719 && !self.metadata.schemas.as_ref().is_some_and(|schemas| {
1720 !schemas.iter().any(|s| s == &dependency.sequence_id.schema)
1721 })
1722 {
1723 return Err(format!(
1724 "default sequence dependency references missing sequence '{}'",
1725 dependency.sequence_id
1726 ));
1727 }
1728 }
1729
1730 let mut view_dependency_ids = HashSet::new();
1731 for dependency in &self.dependencies {
1732 validate_id("view dependent identity", &dependency.dependent, true)?;
1733 validate_id("view referenced identity", &dependency.referenced, true)?;
1734 if !view_dependency_ids.insert((
1735 dependency.dependent.clone(),
1736 dependency.referenced.clone(),
1737 dependency.referenced_column.clone(),
1738 )) {
1739 return Err(format!(
1740 "view dependency '{} -> {}' appears more than once",
1741 dependency.dependent, dependency.referenced
1742 ));
1743 }
1744 let omitted_schema = |schema: Option<&str>| {
1745 self.metadata
1746 .schemas
1747 .as_ref()
1748 .zip(schema)
1749 .is_some_and(|(schemas, schema)| !schemas.iter().any(|known| known == schema))
1750 };
1751 let object_missing = !self.relations.contains_key(&dependency.dependent);
1752 let referenced_missing = !self.relations.contains_key(&dependency.referenced);
1753 if (object_missing && !omitted_schema(Some(&dependency.dependent.schema)))
1754 || (referenced_missing && !omitted_schema(Some(&dependency.referenced.schema)))
1755 {
1756 return Err(format!(
1757 "view dependency '{} -> {}' references a missing relation",
1758 dependency.dependent, dependency.referenced
1759 ));
1760 }
1761 if let Some(relation) = self.relations.get(&dependency.dependent)
1762 && !matches!(
1763 relation.kind,
1764 RelationKind::View | RelationKind::MaterializedView
1765 )
1766 {
1767 return Err(format!(
1768 "view dependency '{} -> {}' has a non-view dependent",
1769 dependency.dependent, dependency.referenced
1770 ));
1771 }
1772 if let Some(column) = &dependency.referenced_column
1773 && column.is_empty()
1774 {
1775 return Err(format!(
1776 "view dependency '{} -> {}' has an empty referenced column identity",
1777 dependency.dependent, dependency.referenced
1778 ));
1779 }
1780 if let Some(column) = &dependency.referenced_column
1781 && let Some(relation) = self.relations.get(&dependency.referenced)
1782 && !relation.has_column(column)
1783 {
1784 return Err(format!(
1785 "view dependency '{} -> {}' references missing column '{}.{}'",
1786 dependency.dependent, dependency.referenced, dependency.referenced, column
1787 ));
1788 }
1789 }
1790
1791 Ok(())
1792 }
1793
1794 pub fn validated(self) -> Result<Self, String> {
1798 self.validate_semantics()?;
1799 Ok(self)
1800 }
1801}
1802
1803#[cfg(test)]
1804mod tests {
1805 use super::*;
1806
1807 fn table(id: ObjectId, columns: &[&str]) -> RelationState {
1808 let mut relation = RelationState::new(
1809 id,
1810 ObjectId::new("", "postgres"),
1811 0,
1812 None,
1813 RelationKind::Table,
1814 crate::_internal::model::relation::Persistence::Permanent,
1815 0,
1816 );
1817 relation.columns = columns
1818 .iter()
1819 .map(|name| crate::_internal::model::column::Column {
1820 name: (*name).to_string(),
1821 data_type: Some("integer".to_string()),
1822 type_id: None,
1823 is_nullable: false,
1824 default: None,
1825 avg_width: Some(4),
1826 default_expr_text: None,
1827 type_modifier: None,
1828 })
1829 .collect();
1830 relation
1831 }
1832
1833 #[test]
1834 fn every_legacy_cache_variant_is_rejected_generically() {
1835 for (versioned, expected_version) in [
1836 (DbCacheVersioned::V1, 1),
1837 (DbCacheVersioned::V2, 2),
1838 (DbCacheVersioned::V3, 3),
1839 (DbCacheVersioned::V4, 4),
1840 ] {
1841 assert_eq!(versioned.format_version(), expected_version);
1842 assert_eq!(
1843 versioned.into_cache().unwrap_err(),
1844 "This cache format is unsupported. Run `safe-migrate sync` to rebuild it."
1845 );
1846 }
1847 let v5 = DbCacheVersioned::V5(Box::default());
1848 assert_eq!(v5.format_version(), 5);
1849 assert_eq!(
1850 v5.into_cache().unwrap_err(),
1851 "This cache format is unsupported. Run `safe-migrate sync` to rebuild it."
1852 );
1853 let v6 = DbCacheVersioned::V6(Box::default());
1854 assert_eq!(v6.format_version(), 6);
1855 assert_eq!(
1856 v6.into_cache().unwrap_err(),
1857 "This cache format is unsupported. Run `safe-migrate sync` to rebuild it."
1858 );
1859 }
1860
1861 #[test]
1862 fn current_cache_format_is_v7() {
1863 assert_eq!(CACHE_FORMAT_VERSION, 7);
1864 assert_eq!(DbCacheVersioned::V7(Box::default()).format_version(), 7);
1865 assert_eq!(CACHE_V7_MAGIC, b"SMCACHE07");
1866 }
1867
1868 #[test]
1869 fn current_cache_rejects_external_boundary_identity_outside_scope() {
1870 let mut cache = DbCache::new();
1871 let schemas = vec!["public".to_string()];
1872 cache.coverage = CatalogCoverage::from_sync_scope(Some(&schemas));
1873 cache.metadata.schemas = Some(schemas);
1874 cache
1875 .scoped_external_relation_dependencies
1876 .push(ObjectId::new("omitted", "table"));
1877
1878 let error = cache.validate_semantics().unwrap_err();
1879 assert!(error.contains("outside the cache schema scope"), "{error}");
1880 }
1881
1882 #[test]
1883 fn current_cache_rejects_mismatched_embedded_identity() {
1884 let mut cache = DbCache::new();
1885 cache.schemas.insert(
1886 "app".to_string(),
1887 SchemaState {
1888 name: "other".to_string(),
1889 owner: ObjectId::new("", "postgres"),
1890 generation: 0,
1891 },
1892 );
1893
1894 let error = DbCacheVersioned::V7(Box::new(cache))
1895 .into_cache()
1896 .unwrap_err();
1897 assert!(error.contains("schema cache key 'app'"));
1898 }
1899
1900 #[test]
1901 fn current_cache_rejects_mismatched_schema_coverage() {
1902 let mut cache = DbCache::new();
1903 cache.metadata.schemas = Some(vec!["app".to_string()]);
1904
1905 let error = DbCacheVersioned::V7(Box::new(cache))
1906 .into_cache()
1907 .unwrap_err();
1908 assert!(error.contains("schema coverage disagrees"));
1909 }
1910
1911 #[test]
1912 fn current_cache_rejects_invalid_foreign_key_column_identity() {
1913 let child = ObjectId::new("public", "child");
1914 let parent = ObjectId::new("public", "parent");
1915 let mut cache = DbCache::new();
1916 cache.insert_baseline(child.clone(), table(child.clone(), &["parent_id"]));
1917 cache.insert_baseline(parent.clone(), table(parent.clone(), &["id"]));
1918 cache.constraints.push(ConstraintState {
1919 table_id: child.clone(),
1920 name: "child_parent_id_fkey".to_string(),
1921 kind: crate::_internal::model::constraint::ConstraintKind::ForeignKey,
1922 validated: true,
1923 backing_index: None,
1924 });
1925 cache.foreign_keys.push(ForeignKeyCache {
1926 constraint_name: "child_parent_id_fkey".to_string(),
1927 from_table: child,
1928 to_table: parent,
1929 from_columns: vec!["missing".to_string()],
1930 to_columns: vec!["id".to_string()],
1931 pk_fk_equality_operators: vec!["=".to_string()],
1932 pk_pk_equality_operators: vec!["=".to_string()],
1933 fk_fk_equality_operators: vec!["=".to_string()],
1934 });
1935
1936 let error = cache.validate_semantics().unwrap_err();
1937 assert!(error.contains("missing source column 'public.child.missing'"));
1938 }
1939
1940 #[test]
1941 fn current_cache_rejects_repeated_foreign_key_columns() {
1942 let child = ObjectId::new("public", "child");
1943 let parent = ObjectId::new("public", "parent");
1944 let mut cache = DbCache::new();
1945 cache.insert_baseline(child.clone(), table(child.clone(), &["a", "b"]));
1946 cache.insert_baseline(parent.clone(), table(parent.clone(), &["id", "other"]));
1947 cache.constraints.push(ConstraintState {
1948 table_id: child.clone(),
1949 name: "child_parent_fkey".to_string(),
1950 kind: ConstraintKind::ForeignKey,
1951 validated: true,
1952 backing_index: None,
1953 });
1954 cache.foreign_keys.push(ForeignKeyCache {
1955 constraint_name: "child_parent_fkey".to_string(),
1956 from_table: child,
1957 to_table: parent,
1958 from_columns: vec!["a".to_string(), "a".to_string()],
1959 to_columns: vec!["id".to_string(), "other".to_string()],
1960 pk_fk_equality_operators: vec!["=".to_string(), "=".to_string()],
1961 pk_pk_equality_operators: vec!["=".to_string(), "=".to_string()],
1962 fk_fk_equality_operators: vec!["=".to_string(), "=".to_string()],
1963 });
1964
1965 let error = cache.validate_semantics().unwrap_err();
1966 assert!(error.contains("repeats source column 'a'"));
1967 }
1968
1969 #[test]
1970 fn current_cache_rejects_incomplete_foreign_key_operator_evidence() {
1971 let child = ObjectId::new("public", "child");
1972 let parent = ObjectId::new("public", "parent");
1973 let mut cache = DbCache::new();
1974 cache.insert_baseline(child.clone(), table(child.clone(), &["parent_id"]));
1975 cache.insert_baseline(parent.clone(), table(parent.clone(), &["id"]));
1976 cache.constraints.push(ConstraintState {
1977 table_id: child.clone(),
1978 name: "child_parent_fkey".into(),
1979 kind: ConstraintKind::ForeignKey,
1980 validated: true,
1981 backing_index: None,
1982 });
1983 cache.foreign_keys.push(ForeignKeyCache {
1984 constraint_name: "child_parent_fkey".into(),
1985 from_table: child,
1986 to_table: parent,
1987 from_columns: vec!["parent_id".into()],
1988 to_columns: vec!["id".into()],
1989 pk_fk_equality_operators: vec!["".into()],
1990 pk_pk_equality_operators: vec!["=".into()],
1991 fk_fk_equality_operators: vec!["=".into()],
1992 });
1993
1994 let error = cache.validate_semantics().unwrap_err();
1995 assert!(error.contains("incomplete PK/FK equality-operator evidence"));
1996 }
1997
1998 #[test]
1999 fn current_cache_accepts_a_valid_primary_key_record() {
2000 let parent = ObjectId::new("public", "parent");
2001 let mut cache = DbCache::new();
2002 cache.insert_baseline(parent.clone(), table(parent.clone(), &["id"]));
2003 cache.constraints.push(ConstraintState {
2004 table_id: parent.clone(),
2005 name: "parent_pkey".to_string(),
2006 kind: ConstraintKind::PrimaryKey,
2007 validated: true,
2008 backing_index: None,
2009 });
2010 cache.constraint_keys.push(ConstraintKeyCache {
2011 table_id: parent,
2012 constraint_name: "parent_pkey".to_string(),
2013 columns: vec!["id".to_string()],
2014 is_primary: true,
2015 });
2016
2017 assert!(cache.validate_semantics().is_ok());
2018 }
2019
2020 #[test]
2021 fn current_cache_accepts_non_unique_exclusion_backing_index() {
2022 let table_id = ObjectId::new("public", "ranges");
2023 let index_id = ObjectId::new("public", "ranges_excl");
2024 let mut cache = DbCache::new();
2025 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2026 cache.indexes.push(IndexCache {
2027 index_id: index_id.clone(),
2028 table_id: table_id.clone(),
2029 using_method: "gist".to_string(),
2030 key_columns: vec!["id".to_string()],
2031 included_columns: Vec::new(),
2032 dependency_columns: vec!["id".to_string()],
2033 dependency_columns_known: true,
2034 has_expression_keys: false,
2035 has_predicate: false,
2036 is_unique: false,
2037 is_valid: true,
2038 is_ready: true,
2039 is_live: true,
2040 has_default_sort_order: true,
2041 has_default_opclasses: true,
2042 has_default_collations: true,
2043 });
2044 cache.constraints.push(ConstraintState {
2045 table_id,
2046 name: "ranges_excl_constraint".to_string(),
2047 kind: ConstraintKind::Exclusion,
2048 validated: true,
2049 backing_index: Some(index_id),
2050 });
2051
2052 assert!(cache.validate_semantics().is_ok());
2053 }
2054
2055 #[test]
2056 fn current_cache_rejects_population_state_on_non_materialized_relation() {
2057 let table_id = ObjectId::new("public", "events");
2058 let mut relation = table(table_id.clone(), &[]);
2059 relation.is_populated = Some(true);
2060 let mut cache = DbCache::new();
2061 cache.insert_baseline(table_id, relation);
2062
2063 let error = cache.validate_semantics().unwrap_err();
2064 assert!(error.contains("materialized-view population state"));
2065 }
2066
2067 #[test]
2068 fn current_cache_rejects_inconsistent_role_membership_edges() {
2069 let member = ObjectId::new("", "member");
2070 let parent = ObjectId::new("", "parent");
2071 let mut cache = DbCache::new();
2072 cache.roles.insert(
2073 member.clone(),
2074 RoleState {
2075 id: member.clone(),
2076 can_login: true,
2077 is_superuser: false,
2078 inherits: true,
2079 member_of: Vec::new(),
2080 can_administer_membership: Vec::new(),
2081 can_inherit_from: Vec::new(),
2082 can_set_role_to: vec![parent.clone()],
2083 },
2084 );
2085 cache.roles.insert(
2086 parent.clone(),
2087 RoleState {
2088 id: parent,
2089 can_login: false,
2090 is_superuser: false,
2091 inherits: true,
2092 member_of: Vec::new(),
2093 can_administer_membership: Vec::new(),
2094 can_inherit_from: Vec::new(),
2095 can_set_role_to: Vec::new(),
2096 },
2097 );
2098
2099 let error = cache.validate_semantics().unwrap_err();
2100 assert!(error.contains("SET ROLE access") && error.contains("without membership"));
2101 }
2102
2103 #[test]
2104 fn current_cache_rejects_membership_options_without_membership() {
2105 let member = ObjectId::new("", "member");
2106 let parent = ObjectId::new("", "parent");
2107 let mut cache = DbCache::new();
2108 cache.roles.insert(
2109 member.clone(),
2110 RoleState {
2111 id: member,
2112 can_login: true,
2113 is_superuser: false,
2114 inherits: true,
2115 member_of: Vec::new(),
2116 can_administer_membership: vec![parent.clone()],
2117 can_inherit_from: Vec::new(),
2118 can_set_role_to: Vec::new(),
2119 },
2120 );
2121 cache.roles.insert(
2122 parent.clone(),
2123 RoleState {
2124 id: parent,
2125 can_login: false,
2126 is_superuser: false,
2127 inherits: true,
2128 member_of: Vec::new(),
2129 can_administer_membership: Vec::new(),
2130 can_inherit_from: Vec::new(),
2131 can_set_role_to: Vec::new(),
2132 },
2133 );
2134
2135 let error = cache.validate_semantics().unwrap_err();
2136 assert!(error.contains("ADMIN option") && error.contains("without membership"));
2137 }
2138
2139 #[test]
2140 fn current_cache_rejects_circular_role_membership() {
2141 let first = ObjectId::new("", "first");
2142 let second = ObjectId::new("", "second");
2143 let mut cache = DbCache::new();
2144 cache.roles.insert(
2145 first.clone(),
2146 RoleState {
2147 id: first.clone(),
2148 can_login: false,
2149 is_superuser: false,
2150 inherits: true,
2151 member_of: vec![second.clone()],
2152 can_administer_membership: Vec::new(),
2153 can_inherit_from: Vec::new(),
2154 can_set_role_to: vec![second.clone()],
2155 },
2156 );
2157 cache.roles.insert(
2158 second.clone(),
2159 RoleState {
2160 id: second,
2161 can_login: false,
2162 is_superuser: false,
2163 inherits: true,
2164 member_of: vec![first.clone()],
2165 can_administer_membership: Vec::new(),
2166 can_inherit_from: Vec::new(),
2167 can_set_role_to: vec![first],
2168 },
2169 );
2170
2171 let error = cache.validate_semantics().unwrap_err();
2172 assert!(error.contains("circular path"));
2173 }
2174
2175 #[test]
2176 fn current_cache_rejects_schema_qualified_role_identity() {
2177 let role = ObjectId::new("public", "app_role");
2178 let mut cache = DbCache::new();
2179 cache.roles.insert(
2180 role.clone(),
2181 RoleState {
2182 id: role,
2183 can_login: false,
2184 is_superuser: false,
2185 inherits: true,
2186 member_of: Vec::new(),
2187 can_administer_membership: Vec::new(),
2188 can_inherit_from: Vec::new(),
2189 can_set_role_to: Vec::new(),
2190 },
2191 );
2192
2193 let error = cache.validate_semantics().unwrap_err();
2194 assert!(error.contains("cluster role namespace"));
2195 }
2196
2197 #[test]
2198 fn current_cache_rejects_schema_qualified_role_membership_target() {
2199 let member = ObjectId::new("", "member");
2200 let parent = ObjectId::new("public", "parent");
2201 let mut cache = DbCache::new();
2202 cache.roles.insert(
2203 member.clone(),
2204 RoleState {
2205 id: member,
2206 can_login: false,
2207 is_superuser: false,
2208 inherits: true,
2209 member_of: vec![parent.clone()],
2210 can_administer_membership: Vec::new(),
2211 can_inherit_from: Vec::new(),
2212 can_set_role_to: vec![parent.clone()],
2213 },
2214 );
2215 cache.roles.insert(
2216 parent.clone(),
2217 RoleState {
2218 id: parent,
2219 can_login: false,
2220 is_superuser: false,
2221 inherits: true,
2222 member_of: Vec::new(),
2223 can_administer_membership: Vec::new(),
2224 can_inherit_from: Vec::new(),
2225 can_set_role_to: Vec::new(),
2226 },
2227 );
2228
2229 let error = cache.validate_semantics().unwrap_err();
2230 assert!(error.contains("cluster role namespace"));
2231 }
2232
2233 #[test]
2234 fn current_cache_rejects_ambiguous_relation_columns() {
2235 let table_id = ObjectId::new("public", "entries");
2236 let mut cache = DbCache::new();
2237 cache.insert_baseline(table_id.clone(), table(table_id, &["id", "id"]));
2238
2239 let error = cache.validate_semantics().unwrap_err();
2240 assert!(error.contains("duplicate column 'id'"));
2241 }
2242
2243 #[test]
2244 fn current_cache_rejects_empty_relation_column_identity() {
2245 let table_id = ObjectId::new("public", "entries");
2246 let mut cache = DbCache::new();
2247 cache.insert_baseline(table_id.clone(), table(table_id, &[""]));
2248
2249 let error = cache.validate_semantics().unwrap_err();
2250 assert!(error.contains("empty identity"));
2251 }
2252
2253 #[test]
2254 fn current_cache_rejects_noncanonical_object_identities() {
2255 let mut quoted_whitespace = DbCache::new();
2256 let id = ObjectId::new("public", " ");
2257 quoted_whitespace
2258 .relations
2259 .insert(id.clone(), table(id, &[" "]));
2260 assert!(quoted_whitespace.validate_semantics().is_ok());
2261
2262 let mut empty_name = DbCache::new();
2263 let id = ObjectId::new("public", "");
2264 empty_name.relations.insert(id.clone(), table(id, &[]));
2265 let error = empty_name.validate_semantics().unwrap_err();
2266 assert!(error.contains("empty object name"));
2267
2268 let mut inferred = DbCache::new();
2269 let mut id = ObjectId::new("public", "items");
2270 id.inferred_schema = true;
2271 inferred.relations.insert(id.clone(), table(id, &[]));
2272 let error = inferred.validate_semantics().unwrap_err();
2273 assert!(error.contains("inferred schema identity"));
2274 }
2275
2276 #[test]
2277 fn current_cache_rejects_ambiguous_schema_scope_and_search_path() {
2278 let mut duplicate_scope = DbCache::new();
2279 duplicate_scope.metadata.schemas = Some(vec!["app".into(), "app".into()]);
2280 let error = duplicate_scope.validate_semantics().unwrap_err();
2281 assert!(error.contains("duplicate schema 'app'"));
2282
2283 let mut duplicate_path = DbCache::new();
2284 duplicate_path.search_path = vec!["public".into(), "public".into()];
2285 let error = duplicate_path.validate_semantics().unwrap_err();
2286 assert!(error.contains("search path contains duplicate schema 'public'"));
2287 }
2288
2289 #[test]
2290 fn current_cache_accepts_explicit_scope_in_caller_order() {
2291 let scope = vec!["public".to_string(), "app".to_string()];
2292 let mut cache = DbCache::new();
2293 cache.metadata.schemas = Some(scope.clone());
2294 cache.coverage = CatalogCoverage::from_sync_scope(Some(&scope));
2295 assert!(cache.validate_semantics().is_ok());
2296 }
2297
2298 #[test]
2299 fn current_cache_rejects_owner_absent_from_role_catalog() {
2300 let table_id = ObjectId::new("public", "items");
2301 let mut cache = DbCache::new();
2302 cache.insert_baseline(table_id.clone(), table(table_id, &[]));
2303 let role_id = ObjectId::new("", "known_owner");
2304 cache.roles.insert(
2305 role_id.clone(),
2306 RoleState {
2307 id: role_id,
2308 can_login: false,
2309 is_superuser: false,
2310 inherits: true,
2311 member_of: Vec::new(),
2312 can_administer_membership: Vec::new(),
2313 can_inherit_from: Vec::new(),
2314 can_set_role_to: Vec::new(),
2315 },
2316 );
2317 let error = cache.validate_semantics().unwrap_err();
2318 assert!(error.contains("relation owner identity 'postgres'"));
2319 }
2320
2321 #[test]
2322 fn current_cache_rejects_empty_provenance_identities() {
2323 let mut cache = DbCache::new();
2324 cache.metadata.source_role = Some(String::new());
2325 let error = cache.validate_semantics().unwrap_err();
2326 assert!(error.contains("empty source role identity"));
2327
2328 let mut cache = DbCache::new();
2329 cache.metadata.source_search_path = Some(vec![String::new()]);
2330 let error = cache.validate_semantics().unwrap_err();
2331 assert!(error.contains("source search path contains an empty"));
2332 }
2333
2334 #[test]
2335 fn current_cache_rejects_malformed_publication_scope_metadata() {
2336 let table = crate::_internal::analysis::facts::PublicationObjectFact::Table {
2337 name: crate::_internal::ast::identifiers::QualifiedName::new(
2338 Some(crate::_internal::ast::identifiers::Ident::new(
2339 "public", false,
2340 )),
2341 crate::_internal::ast::identifiers::Ident::new("items", false),
2342 ),
2343 only: false,
2344 include_partitions: false,
2345 columns: Some(vec!["id".into(), "id".into()]),
2346 row_filter: None,
2347 };
2348 let mut cache = DbCache::new();
2349 cache.publications.insert(
2350 "pub_items".into(),
2351 PublicationState {
2352 name: "pub_items".into(),
2353 owner: None,
2354 scope: crate::_internal::analysis::facts::PublicationScope::Explicit(vec![
2355 table.clone(),
2356 table,
2357 ]),
2358 params: Vec::new(),
2359 generation: 0,
2360 },
2361 );
2362
2363 let error = DbCacheVersioned::V7(Box::new(cache))
2364 .into_cache()
2365 .unwrap_err();
2366 assert!(error.contains("empty or duplicate table column"));
2367 }
2368
2369 #[test]
2370 fn current_cache_rejects_subscription_owner_outside_role_catalog() {
2371 let mut cache = DbCache::new();
2372 cache.roles.insert(
2373 ObjectId::new("", "present_owner"),
2374 RoleState {
2375 id: ObjectId::new("", "present_owner"),
2376 can_login: false,
2377 is_superuser: false,
2378 inherits: true,
2379 member_of: Vec::new(),
2380 can_administer_membership: Vec::new(),
2381 can_inherit_from: Vec::new(),
2382 can_set_role_to: Vec::new(),
2383 },
2384 );
2385 cache.subscriptions.insert(
2386 "sub".into(),
2387 SubscriptionState {
2388 name: "sub".into(),
2389 owner: Some("missing_owner".into()),
2390 connection: crate::_internal::analysis::facts::ConnectionTarget::Redacted,
2391 publications: vec!["pub".into()],
2392 params: None,
2393 enabled: true,
2394 slot_name: None,
2395 generation: 0,
2396 },
2397 );
2398
2399 let error = DbCacheVersioned::V7(Box::new(cache))
2400 .into_cache()
2401 .unwrap_err();
2402 assert!(error.contains("owner 'missing_owner' is absent"));
2403 }
2404
2405 #[test]
2406 fn current_cache_rejects_duplicate_view_dependencies() {
2407 let view = ObjectId::new("public", "active_entries");
2408 let table_id = ObjectId::new("public", "entries");
2409 let mut cache = DbCache::new();
2410 let mut view_state = table(view.clone(), &["id"]);
2411 view_state.kind = RelationKind::View;
2412 cache.insert_baseline(view.clone(), view_state);
2413 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2414 let dependency = ViewDependencyCache {
2415 dependent: view,
2416 referenced: table_id,
2417 referenced_column: Some("id".into()),
2418 };
2419 cache.dependencies = vec![dependency.clone(), dependency];
2420
2421 let error = cache.validate_semantics().unwrap_err();
2422 assert!(error.contains("appears more than once"));
2423 }
2424
2425 #[test]
2426 fn current_cache_rejects_empty_view_dependency_column() {
2427 let view = ObjectId::new("public", "active_entries");
2428 let table_id = ObjectId::new("public", "entries");
2429 let mut cache = DbCache::new();
2430 let mut view_state = table(view.clone(), &["id"]);
2431 view_state.kind = RelationKind::View;
2432 cache.insert_baseline(view.clone(), view_state);
2433 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2434 cache.dependencies.push(ViewDependencyCache {
2435 dependent: view,
2436 referenced: table_id,
2437 referenced_column: Some(String::new()),
2438 });
2439
2440 let error = cache.validate_semantics().unwrap_err();
2441 assert!(error.contains("empty referenced column identity"));
2442 }
2443
2444 #[test]
2445 fn current_cache_rejects_privilege_for_missing_role() {
2446 let table_id = ObjectId::new("public", "entries");
2447 let mut cache = DbCache::new();
2448 let mut relation = table(table_id.clone(), &["id"]);
2449 relation.privileges.grants.insert(
2450 ObjectId::new("", "missing_role"),
2451 [crate::_internal::model::relation::Privilege::Select]
2452 .into_iter()
2453 .collect(),
2454 );
2455 cache.insert_baseline(table_id, relation);
2456
2457 let error = cache.validate_semantics().unwrap_err();
2458 assert!(error.contains("missing grantee role"));
2459 }
2460
2461 #[test]
2462 fn current_cache_allows_public_privilege_grantee() {
2463 let table_id = ObjectId::new("public", "entries");
2464 let mut cache = DbCache::new();
2465 let mut relation = table(table_id.clone(), &["id"]);
2466 relation.privileges.grants.insert(
2467 ObjectId::new("", "public"),
2468 [crate::_internal::model::relation::Privilege::Select]
2469 .into_iter()
2470 .collect(),
2471 );
2472 cache.insert_baseline(table_id, relation);
2473
2474 assert!(cache.validate_semantics().is_ok());
2475 }
2476
2477 #[test]
2478 fn current_cache_rejects_repeated_constraint_key_columns() {
2479 let table_id = ObjectId::new("public", "entries");
2480 let mut cache = DbCache::new();
2481 cache.insert_baseline(
2482 table_id.clone(),
2483 table(table_id.clone(), &["id", "tenant_id"]),
2484 );
2485 cache.constraints.push(ConstraintState {
2486 table_id: table_id.clone(),
2487 name: "entries_key".to_string(),
2488 kind: ConstraintKind::Unique,
2489 validated: true,
2490 backing_index: None,
2491 });
2492 cache.constraint_keys.push(ConstraintKeyCache {
2493 table_id,
2494 constraint_name: "entries_key".to_string(),
2495 columns: vec!["id".to_string(), "id".to_string()],
2496 is_primary: false,
2497 });
2498
2499 let error = cache.validate_semantics().unwrap_err();
2500 assert!(error.contains("constraint key 'public.entries.entries_key' repeats column 'id'"));
2501 }
2502
2503 #[test]
2504 fn scoped_cache_accepts_a_dependency_to_an_omitted_schema() {
2505 let view_id = ObjectId::new("app", "v");
2506 let mut cache = DbCache::new();
2507 cache.metadata.schemas = Some(vec!["app".to_string()]);
2508 cache.coverage = CatalogCoverage::from_sync_scope(cache.metadata.schemas.as_deref());
2509 cache.insert_baseline(
2510 view_id.clone(),
2511 RelationState::new(
2512 view_id.clone(),
2513 ObjectId::new("", "postgres"),
2514 0,
2515 None,
2516 crate::_internal::model::relation::RelationKind::View,
2517 crate::_internal::model::relation::Persistence::Permanent,
2518 0,
2519 ),
2520 );
2521 cache.dependencies.push(ViewDependencyCache {
2522 dependent: view_id,
2523 referenced: ObjectId::new("tenant", "base"),
2524 referenced_column: None,
2525 });
2526
2527 assert!(cache.validate_semantics().is_ok());
2528 }
2529
2530 #[test]
2531 fn current_cache_rejects_view_dependency_on_a_missing_column() {
2532 let table_id = ObjectId::new("public", "entries");
2533 let view_id = ObjectId::new("public", "entry_view");
2534 let mut cache = DbCache::new();
2535 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2536 cache.insert_baseline(
2537 view_id.clone(),
2538 RelationState::new(
2539 view_id.clone(),
2540 ObjectId::new("", "postgres"),
2541 0,
2542 None,
2543 RelationKind::View,
2544 crate::_internal::model::relation::Persistence::Permanent,
2545 0,
2546 ),
2547 );
2548 cache.dependencies.push(ViewDependencyCache {
2549 dependent: view_id,
2550 referenced: table_id,
2551 referenced_column: Some("missing".to_string()),
2552 });
2553
2554 assert!(
2555 cache
2556 .validate_semantics()
2557 .unwrap_err()
2558 .contains("references missing column")
2559 );
2560 }
2561
2562 #[test]
2563 fn current_cache_rejects_view_dependency_with_a_non_view_dependent() {
2564 let table_id = ObjectId::new("public", "entries");
2565 let referenced_id = ObjectId::new("public", "source");
2566 let mut cache = DbCache::new();
2567 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2568 cache.insert_baseline(referenced_id.clone(), table(referenced_id.clone(), &["id"]));
2569 cache.dependencies.push(ViewDependencyCache {
2570 dependent: table_id,
2571 referenced: referenced_id,
2572 referenced_column: Some("id".to_string()),
2573 });
2574
2575 assert!(
2576 cache
2577 .validate_semantics()
2578 .unwrap_err()
2579 .contains("has a non-view dependent")
2580 );
2581 }
2582
2583 #[test]
2584 fn current_cache_rejects_constraint_dependency_on_a_missing_column() {
2585 let table_id = ObjectId::new("public", "entries");
2586 let mut cache = DbCache::new();
2587 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2588 cache
2589 .constraints
2590 .push(crate::_internal::model::constraint::ConstraintState {
2591 table_id: table_id.clone(),
2592 name: "entries_check".to_string(),
2593 kind: crate::_internal::model::constraint::ConstraintKind::Check,
2594 validated: true,
2595 backing_index: None,
2596 });
2597 cache
2598 .constraint_dependencies
2599 .push(ConstraintDependencyCache {
2600 table_id,
2601 constraint_name: "entries_check".to_string(),
2602 columns: vec!["missing".to_string()],
2603 });
2604
2605 assert!(
2606 cache
2607 .validate_semantics()
2608 .unwrap_err()
2609 .contains("constraint dependency")
2610 );
2611 }
2612
2613 #[test]
2614 fn current_cache_rejects_generated_dependency_on_a_missing_source_column() {
2615 let table_id = ObjectId::new("public", "entries");
2616 let mut cache = DbCache::new();
2617 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id", "total"]));
2618 cache
2619 .generated_column_dependencies
2620 .push(GeneratedColumnDependencyCache {
2621 table_id,
2622 column_name: "total".to_string(),
2623 depends_on_column: "missing".to_string(),
2624 });
2625
2626 assert!(
2627 cache
2628 .validate_semantics()
2629 .unwrap_err()
2630 .contains("generated column dependency")
2631 );
2632 }
2633
2634 #[test]
2635 fn current_cache_rejects_self_referencing_generated_dependency() {
2636 let table_id = ObjectId::new("public", "entries");
2637 let mut cache = DbCache::new();
2638 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["total"]));
2639 cache
2640 .generated_column_dependencies
2641 .push(GeneratedColumnDependencyCache {
2642 table_id,
2643 column_name: "total".to_string(),
2644 depends_on_column: "total".to_string(),
2645 });
2646
2647 let error = cache.validate_semantics().unwrap_err();
2648 assert!(error.contains("cannot depend on itself"));
2649 }
2650
2651 #[test]
2652 fn current_cache_rejects_duplicate_generated_dependency_rows() {
2653 let table_id = ObjectId::new("public", "entries");
2654 let mut cache = DbCache::new();
2655 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id", "total"]));
2656 let dependency = GeneratedColumnDependencyCache {
2657 table_id,
2658 column_name: "total".to_string(),
2659 depends_on_column: "id".to_string(),
2660 };
2661 cache.generated_column_dependencies.push(dependency.clone());
2662 cache.generated_column_dependencies.push(dependency);
2663
2664 assert!(cache.validate_semantics().unwrap_err().contains(
2665 "generated column dependency 'public.entries.total -> id' appears more than once"
2666 ));
2667 }
2668 #[test]
2669 fn current_cache_rejects_dangling_index_relationship() {
2670 let mut cache = DbCache::new();
2671 cache.indexes.push(IndexCache {
2672 index_id: ObjectId::new("public", "items_idx"),
2673 table_id: ObjectId::new("public", "items"),
2674 using_method: "btree".into(),
2675 key_columns: vec!["id".into()],
2676 included_columns: Vec::new(),
2677 dependency_columns: vec!["id".into()],
2678 dependency_columns_known: true,
2679 has_expression_keys: false,
2680 has_predicate: false,
2681 is_unique: false,
2682 is_valid: true,
2683 is_ready: true,
2684 is_live: true,
2685 has_default_sort_order: true,
2686 has_default_opclasses: true,
2687 has_default_collations: true,
2688 });
2689
2690 let error = DbCacheVersioned::V7(Box::new(cache))
2691 .into_cache()
2692 .unwrap_err();
2693 assert!(error.contains("references missing relation 'public.items'"));
2694 }
2695
2696 #[test]
2697 fn current_cache_rejects_index_in_a_different_schema_than_relation() {
2698 let table_id = ObjectId::new("public", "items");
2699 let mut cache = DbCache::new();
2700 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2701 cache.indexes.push(IndexCache {
2702 index_id: ObjectId::new("other", "items_idx"),
2703 table_id,
2704 using_method: "btree".into(),
2705 key_columns: vec!["id".into()],
2706 included_columns: Vec::new(),
2707 dependency_columns: vec!["id".into()],
2708 dependency_columns_known: true,
2709 has_expression_keys: false,
2710 has_predicate: false,
2711 is_unique: false,
2712 is_valid: true,
2713 is_ready: true,
2714 is_live: true,
2715 has_default_sort_order: true,
2716 has_default_opclasses: true,
2717 has_default_collations: true,
2718 });
2719
2720 let error = DbCacheVersioned::V7(Box::new(cache))
2721 .into_cache()
2722 .unwrap_err();
2723 assert!(error.contains("must be in the same schema as indexed relation"));
2724 }
2725
2726 #[test]
2727 fn current_cache_rejects_index_colliding_with_relation_namespace_object() {
2728 let table_id = ObjectId::new("public", "items");
2729 let index_id = ObjectId::new("public", "shared_name");
2730 let mut cache = DbCache::new();
2731 cache.insert_baseline(table_id.clone(), table(table_id, &["id"]));
2732 cache.insert_baseline(index_id.clone(), table(index_id.clone(), &["id"]));
2733 cache.indexes.push(IndexCache {
2734 index_id,
2735 table_id: ObjectId::new("public", "items"),
2736 using_method: "btree".into(),
2737 key_columns: vec!["id".into()],
2738 included_columns: Vec::new(),
2739 dependency_columns: vec!["id".into()],
2740 dependency_columns_known: true,
2741 has_expression_keys: false,
2742 has_predicate: false,
2743 is_unique: false,
2744 is_valid: true,
2745 is_ready: true,
2746 is_live: true,
2747 has_default_sort_order: true,
2748 has_default_opclasses: true,
2749 has_default_collations: true,
2750 });
2751
2752 let error = DbCacheVersioned::V7(Box::new(cache))
2753 .into_cache()
2754 .unwrap_err();
2755 assert!(error.contains("collides with another relation-namespace object"));
2756 }
2757
2758 #[test]
2759 fn current_cache_rejects_sequence_colliding_with_relation_namespace_object() {
2760 let table_id = ObjectId::new("public", "items");
2761 let shared_id = ObjectId::new("public", "shared_name");
2762 let mut cache = DbCache::new();
2763 cache.insert_baseline(table_id.clone(), table(table_id, &["id"]));
2764 cache.insert_baseline(shared_id.clone(), table(shared_id.clone(), &["id"]));
2765 cache.sequences.insert(
2766 shared_id.clone(),
2767 SequenceState {
2768 id: shared_id,
2769 owner: ObjectId::new("", "postgres"),
2770 owned_by: None,
2771 kind: crate::_internal::model::sequence::SequenceKind::Owned,
2772 generation: 0,
2773 },
2774 );
2775
2776 let error = DbCacheVersioned::V7(Box::new(cache))
2777 .into_cache()
2778 .unwrap_err();
2779 assert!(error.contains("collides with another relation-namespace object"));
2780 }
2781
2782 #[test]
2783 fn current_cache_rejects_trigger_in_a_different_schema_than_table() {
2784 let table_id = ObjectId::new("public", "items");
2785 let mut cache = DbCache::new();
2786 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2787 cache.triggers.push(TriggerCache {
2788 trigger_id: ObjectId::new("other", "items_trigger"),
2789 table_id,
2790 function_id: ObjectId::new("public", "items_trigger_fn()"),
2791 enabled_mode: TriggerEnableMode::Origin,
2792 });
2793
2794 let error = DbCacheVersioned::V7(Box::new(cache))
2795 .into_cache()
2796 .unwrap_err();
2797 assert!(error.contains("must be in the same schema as trigger table"));
2798 }
2799
2800 #[test]
2801 fn current_cache_rejects_index_without_complete_dependency_evidence() {
2802 let table_id = ObjectId::new("public", "items");
2803 let mut cache = DbCache::new();
2804 cache.insert_baseline(table_id.clone(), table(table_id.clone(), &["id"]));
2805 cache.indexes.push(IndexCache {
2806 index_id: ObjectId::new("public", "items_idx"),
2807 table_id,
2808 using_method: "btree".into(),
2809 key_columns: vec!["id".into()],
2810 included_columns: Vec::new(),
2811 dependency_columns: vec!["id".into()],
2812 dependency_columns_known: false,
2813 has_expression_keys: false,
2814 has_predicate: false,
2815 is_unique: false,
2816 is_valid: true,
2817 is_ready: true,
2818 is_live: true,
2819 has_default_sort_order: true,
2820 has_default_opclasses: true,
2821 has_default_collations: true,
2822 });
2823
2824 assert!(
2825 DbCacheVersioned::V7(Box::new(cache))
2826 .into_cache()
2827 .unwrap_err()
2828 .contains("missing complete dependency-column evidence")
2829 );
2830 }
2831
2832 #[test]
2833 fn current_cache_validates_stable_direct_inheritance() {
2834 let parent = ObjectId::new("public", "parent");
2835 let child = ObjectId::new("public", "child");
2836 let mut cache = DbCache::new();
2837 cache.search_path.clear();
2838 cache.insert_baseline(parent.clone(), table(parent.clone(), &["id"]));
2839 cache.insert_baseline(child.clone(), table(child.clone(), &["id"]));
2840 cache.inheritances.push(InheritanceCache {
2841 child: child.clone(),
2842 parent: parent.clone(),
2843 sequence: 1,
2844 is_partition: false,
2845 detach_pending: false,
2846 });
2847 assert!(
2848 DbCacheVersioned::V7(Box::new(cache.clone()))
2849 .into_cache()
2850 .is_ok()
2851 );
2852
2853 cache.inheritances[0].detach_pending = true;
2854 let error = DbCacheVersioned::V7(Box::new(cache))
2855 .into_cache()
2856 .unwrap_err();
2857 assert!(error.contains("being detached"));
2858 }
2859
2860 #[test]
2861 fn current_cache_rejects_cross_catalog_contradictions() {
2862 let mut missing_search_schema = DbCache::new();
2863 missing_search_schema.schemas.insert(
2864 "app".to_string(),
2865 SchemaState {
2866 name: "app".to_string(),
2867 owner: ObjectId::new("", "postgres"),
2868 generation: 0,
2869 },
2870 );
2871 assert!(
2872 missing_search_schema
2873 .validate_semantics()
2874 .unwrap_err()
2875 .contains("search path references missing schema 'public'")
2876 );
2877
2878 let mut missing_sequence_owner = DbCache::new();
2879 let sequence_id = ObjectId::new("public", "items_id_seq");
2880 missing_sequence_owner.sequences.insert(
2881 sequence_id.clone(),
2882 SequenceState {
2883 id: sequence_id,
2884 owner: ObjectId::new("", "postgres"),
2885 owned_by: Some((ObjectId::new("public", "items"), "id".to_string())),
2886 kind: crate::_internal::model::sequence::SequenceKind::Owned,
2887 generation: 0,
2888 },
2889 );
2890 assert!(
2891 missing_sequence_owner
2892 .validate_semantics()
2893 .unwrap_err()
2894 .contains("ownership references missing relation 'public.items'")
2895 );
2896
2897 let mut cross_schema_sequence_owner = DbCache::new();
2898 let table_id = ObjectId::new("public", "items");
2899 cross_schema_sequence_owner.insert_baseline(table_id.clone(), table(table_id, &["id"]));
2900 cross_schema_sequence_owner.sequences.insert(
2901 ObjectId::new("archive", "items_id_seq"),
2902 SequenceState {
2903 id: ObjectId::new("archive", "items_id_seq"),
2904 owner: ObjectId::new("", "postgres"),
2905 owned_by: Some((ObjectId::new("public", "items"), "id".to_string())),
2906 kind: crate::_internal::model::sequence::SequenceKind::Owned,
2907 generation: 0,
2908 },
2909 );
2910 assert!(
2911 cross_schema_sequence_owner
2912 .validate_semantics()
2913 .unwrap_err()
2914 .contains("must be in the same schema as owning table")
2915 );
2916
2917 let mut missing_membership_role = DbCache::new();
2918 let role_id = ObjectId::new("", "member");
2919 missing_membership_role.roles.insert(
2920 role_id.clone(),
2921 RoleState {
2922 id: role_id,
2923 can_login: true,
2924 is_superuser: false,
2925 inherits: true,
2926 member_of: vec![ObjectId::new("", "missing")],
2927 can_administer_membership: Vec::new(),
2928 can_inherit_from: Vec::new(),
2929 can_set_role_to: Vec::new(),
2930 },
2931 );
2932 assert!(
2933 missing_membership_role
2934 .validate_semantics()
2935 .unwrap_err()
2936 .contains("membership references missing role")
2937 );
2938
2939 let mut invalid_view_dependency = DbCache::new();
2940 invalid_view_dependency
2941 .dependencies
2942 .push(ViewDependencyCache {
2943 dependent: ObjectId::new("public", "view"),
2944 referenced: ObjectId::new("public", "missing"),
2945 referenced_column: None,
2946 });
2947 assert!(
2948 invalid_view_dependency
2949 .validate_semantics()
2950 .unwrap_err()
2951 .contains("references a missing relation")
2952 );
2953 }
2954}