Skip to main content

safe_migrate/db/
cache.rs

1use crate::ast::identifiers::ObjectId;
2use crate::model::constraint::ConstraintState;
3use crate::model::function::FunctionState;
4use crate::model::relation::RelationState;
5use crate::model::replication::{PublicationState, SubscriptionState};
6use crate::model::role::RoleState;
7use crate::model::schema::SchemaState;
8use crate::model::sequence::SequenceState;
9use crate::model::trigger::TriggerEnableMode;
10use crate::model::types::TypeState;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct ForeignKeyCache {
16    pub constraint_name: String,
17    pub from_table: ObjectId,
18    pub to_table: ObjectId,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct IndexCache {
23    pub index_id: ObjectId,
24    pub table_id: ObjectId,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct TriggerCache {
29    pub trigger_id: ObjectId,
30    pub table_id: ObjectId,
31    pub function_id: ObjectId,
32    pub enabled_mode: TriggerEnableMode,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct DependencyCache {
37    pub classid: u32,
38    pub objid: u32,
39    pub objsubid: i32,
40    pub refclassid: u32,
41    pub refobjid: u32,
42    pub refobjsubid: i32,
43    pub deptype: String,
44    pub obj_schema: Option<String>,
45    pub obj_name: Option<String>,
46    pub ref_schema: Option<String>,
47    pub ref_name: Option<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, Default)]
51pub struct CacheMetadata {
52    /// Seconds since the Unix epoch when `safe-migrate sync` assembled this
53    /// baseline. `None` represents a cache written before provenance support.
54    pub created_at_unix_secs: Option<u64>,
55    /// PostgreSQL database name only; connection credentials and host details
56    /// are deliberately never stored in a cache.
57    pub source_database: Option<String>,
58    /// Session role used when the cache was synchronized. This is needed to
59    /// resolve PostgreSQL's special `$user` search-path entry.
60    pub source_role: Option<String>,
61    /// `SESSION_USER` at synchronization time. This remains distinct from
62    /// `source_role` when the connection has selected another effective role.
63    pub source_session_role: Option<String>,
64    /// Parsed `search_path` setting before PostgreSQL expands `$user`.
65    pub source_search_path: Option<Vec<String>>,
66    /// Effective `lock_timeout` observed on the fresh synchronization
67    /// connection, normalized to milliseconds. PostgreSQL uses zero to mean
68    /// that the timeout is disabled.
69    pub source_lock_timeout_ms: u64,
70    /// Effective `statement_timeout` observed on the fresh synchronization
71    /// connection, normalized to milliseconds. PostgreSQL uses zero to mean
72    /// that the timeout is disabled.
73    pub source_statement_timeout_ms: u64,
74    /// Explicit schema scope passed to sync. `None` means all non-system
75    /// schemas were requested.
76    pub schemas: Option<Vec<String>>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct DbCache {
81    pub pg_version_num: Option<u32>,
82    pub metadata: CacheMetadata,
83    pub search_path: Vec<String>,
84    pub relations: HashMap<ObjectId, RelationState>,
85    pub foreign_keys: Vec<ForeignKeyCache>,
86    pub indexes: Vec<IndexCache>,
87    pub constraints: Vec<ConstraintState>,
88    pub triggers: Vec<TriggerCache>,
89    pub functions: HashMap<ObjectId, FunctionState>,
90    pub types: HashMap<ObjectId, TypeState>,
91    pub roles: HashMap<ObjectId, RoleState>,
92    pub schemas: HashMap<String, SchemaState>,
93    pub sequences: HashMap<ObjectId, SequenceState>,
94    pub dependencies: Vec<DependencyCache>,
95    pub publications: HashMap<String, PublicationState>,
96    pub subscriptions: HashMap<String, SubscriptionState>,
97}
98
99pub const CACHE_FORMAT_VERSION: u32 = 6;
100
101pub const CACHE_V6_MAGIC: &[u8] = b"SMCACHE06";
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub enum DbCacheVersioned {
105    // Unit variants reserve the historic bincode discriminants. The reader
106    // rejects non-V6 headers before decoding, so legacy layouts are not part
107    // of the production model and cannot be converted accidentally.
108    V1,
109    V2,
110    V3,
111    V4,
112    V5(Box<DbCache>),
113    V6(Box<DbCache>),
114}
115
116impl DbCacheVersioned {
117    pub fn format_version(&self) -> u32 {
118        match self {
119            DbCacheVersioned::V1 => 1,
120            DbCacheVersioned::V2 => 2,
121            DbCacheVersioned::V3 => 3,
122            DbCacheVersioned::V4 => 4,
123            DbCacheVersioned::V5(_) => 5,
124            DbCacheVersioned::V6(_) => 6,
125        }
126    }
127
128    pub fn into_cache(self) -> Result<DbCache, String> {
129        match self {
130            DbCacheVersioned::V1
131            | DbCacheVersioned::V2
132            | DbCacheVersioned::V3
133            | DbCacheVersioned::V4
134            | DbCacheVersioned::V5(_) => Err(
135                "This cache format is unsupported. Run `safe-migrate sync` to rebuild it."
136                    .to_string(),
137            ),
138            DbCacheVersioned::V6(c) => Ok(*c),
139        }
140    }
141}
142
143impl Default for DbCache {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl DbCache {
150    pub fn new() -> Self {
151        Self {
152            pg_version_num: None,
153            metadata: CacheMetadata::default(),
154            search_path: vec!["public".to_string()],
155            relations: HashMap::new(),
156            foreign_keys: Vec::new(),
157            indexes: Vec::new(),
158            constraints: Vec::new(),
159            triggers: Vec::new(),
160            functions: HashMap::new(),
161            types: HashMap::new(),
162            roles: HashMap::new(),
163            schemas: HashMap::new(),
164            sequences: HashMap::new(),
165            dependencies: Vec::new(),
166            publications: HashMap::new(),
167            subscriptions: HashMap::new(),
168        }
169    }
170
171    pub fn insert_baseline(&mut self, id: ObjectId, state: RelationState) {
172        self.relations.insert(id, state);
173    }
174
175    pub fn baseline_relations(&self) -> impl Iterator<Item = (&ObjectId, &RelationState)> {
176        self.relations.iter()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn every_legacy_cache_variant_is_rejected_generically() {
186        for (versioned, expected_version) in [
187            (DbCacheVersioned::V1, 1),
188            (DbCacheVersioned::V2, 2),
189            (DbCacheVersioned::V3, 3),
190            (DbCacheVersioned::V4, 4),
191        ] {
192            assert_eq!(versioned.format_version(), expected_version);
193            assert_eq!(
194                versioned.into_cache().unwrap_err(),
195                "This cache format is unsupported. Run `safe-migrate sync` to rebuild it."
196            );
197        }
198        let v5 = DbCacheVersioned::V5(Box::default());
199        assert_eq!(v5.format_version(), 5);
200        assert_eq!(
201            v5.into_cache().unwrap_err(),
202            "This cache format is unsupported. Run `safe-migrate sync` to rebuild it."
203        );
204    }
205
206    #[test]
207    fn current_cache_format_is_v6() {
208        assert_eq!(CACHE_FORMAT_VERSION, 6);
209        assert_eq!(DbCacheVersioned::V6(Box::default()).format_version(), 6);
210        assert_eq!(CACHE_V6_MAGIC, b"SMCACHE06");
211    }
212}