Skip to main content

safe_migrate/db/
cache.rs

1// FILE: src/db/cache.rs
2use crate::ast::identifiers::ObjectId;
3use crate::model::constraint::ConstraintState;
4use crate::model::function::FunctionState;
5use crate::model::relation::RelationState;
6use crate::model::trigger::TriggerEnableMode;
7use crate::model::types::TypeState;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ForeignKeyCache {
13    pub constraint_name: String,
14    pub from_table: ObjectId,
15    pub to_table: ObjectId,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct IndexCache {
20    pub index_id: ObjectId,
21    pub table_id: ObjectId,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TriggerCache {
26    pub trigger_id: ObjectId,
27    pub table_id: ObjectId,
28    pub function_id: ObjectId,
29    pub enabled_mode: TriggerEnableMode,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct LegacyTriggerCache {
34    pub trigger_id: ObjectId,
35    pub table_id: ObjectId,
36    pub function_id: ObjectId,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct DependencyCache {
41    pub classid: u32,
42    pub objid: u32,
43    pub objsubid: i32,
44    pub refclassid: u32,
45    pub refobjid: u32,
46    pub refobjsubid: i32,
47    pub deptype: String,
48    pub obj_schema: Option<String>,
49    pub obj_name: Option<String>,
50    pub ref_schema: Option<String>,
51    pub ref_name: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
55pub struct CacheMetadata {
56    /// Seconds since the Unix epoch when `safe-migrate sync` assembled this
57    /// baseline. `None` represents a cache written before provenance support.
58    pub created_at_unix_secs: Option<u64>,
59    /// PostgreSQL database name only; connection credentials and host details
60    /// are deliberately never stored in a cache.
61    pub source_database: Option<String>,
62    /// Explicit schema scope passed to sync. `None` means all non-system
63    /// schemas were requested.
64    pub schemas: Option<Vec<String>>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct DbCacheV1 {
69    pub pg_version_num: Option<u32>,
70    pub relations: HashMap<ObjectId, RelationState>,
71    #[serde(default)]
72    pub foreign_keys: Vec<ForeignKeyCache>,
73    #[serde(default)]
74    pub indexes: Vec<IndexCache>,
75    #[serde(default)]
76    pub triggers: Vec<LegacyTriggerCache>,
77    #[serde(default)]
78    pub functions: HashMap<ObjectId, FunctionState>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct DbCacheV2 {
83    pub pg_version_num: Option<u32>,
84    pub relations: HashMap<ObjectId, RelationState>,
85    pub foreign_keys: Vec<ForeignKeyCache>,
86    pub indexes: Vec<IndexCache>,
87    pub triggers: Vec<LegacyTriggerCache>,
88    pub functions: HashMap<ObjectId, FunctionState>,
89    pub dependencies: Vec<DependencyCache>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DbCache {
94    pub pg_version_num: Option<u32>,
95    pub metadata: CacheMetadata,
96    pub search_path: Vec<String>,
97    pub relations: HashMap<ObjectId, RelationState>,
98    pub foreign_keys: Vec<ForeignKeyCache>,
99    pub indexes: Vec<IndexCache>,
100    pub constraints: Vec<ConstraintState>,
101    pub triggers: Vec<TriggerCache>,
102    pub functions: HashMap<ObjectId, FunctionState>,
103    pub types: HashMap<ObjectId, TypeState>,
104    pub dependencies: Vec<DependencyCache>,
105}
106
107pub const CACHE_FORMAT_VERSION: u32 = 3;
108
109/// Prefixes every V3 payload after zstd decompression. Older caches did not
110/// have a payload header, so this prevents their bincode V3 discriminator from
111/// being mistaken for the redesigned V3 schema.
112pub const CACHE_V3_MAGIC: &[u8] = b"SMCACHE03";
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub enum DbCacheVersioned {
116    V1(DbCacheV1),
117    V2(DbCacheV2),
118    V3(DbCache),
119}
120
121impl DbCacheVersioned {
122    pub fn format_version(&self) -> u32 {
123        match self {
124            DbCacheVersioned::V1(_) => 1,
125            DbCacheVersioned::V2(_) => 2,
126            DbCacheVersioned::V3(_) => 3,
127        }
128    }
129
130    pub fn into_cache(self) -> Result<DbCache, String> {
131        match self {
132            DbCacheVersioned::V1(c) => Ok(DbCache {
133                pg_version_num: c.pg_version_num,
134                metadata: CacheMetadata::default(),
135                relations: c.relations,
136                foreign_keys: c.foreign_keys,
137                indexes: c.indexes,
138                constraints: Vec::new(),
139                triggers: upgrade_legacy_triggers(c.triggers),
140                functions: c.functions,
141                types: HashMap::new(),
142                dependencies: Vec::new(),
143                search_path: vec!["public".to_string()],
144            }),
145            DbCacheVersioned::V2(c) => Ok(DbCache {
146                pg_version_num: c.pg_version_num,
147                metadata: CacheMetadata::default(),
148                search_path: vec!["public".to_string()],
149                relations: c.relations,
150                foreign_keys: c.foreign_keys,
151                indexes: c.indexes,
152                constraints: Vec::new(),
153                triggers: upgrade_legacy_triggers(c.triggers),
154                functions: c.functions,
155                types: HashMap::new(),
156                dependencies: c.dependencies,
157            }),
158            DbCacheVersioned::V3(c) => Ok(c),
159        }
160    }
161}
162
163/// Returns the historical enum version encoded at the beginning of an
164/// unheadered bincode cache. The configured bincode format uses a single-byte
165/// variable integer for the historical versions. Versions 3 and above are
166/// intentionally not compatible with v0.4.3 and must be rebuilt by
167/// `safe-migrate sync`.
168pub fn legacy_cache_format_version(payload: &[u8]) -> Option<u32> {
169    payload.first().map(|tag| u32::from(*tag) + 1)
170}
171
172fn upgrade_legacy_triggers(triggers: Vec<LegacyTriggerCache>) -> Vec<TriggerCache> {
173    triggers
174        .into_iter()
175        .map(|trigger| TriggerCache {
176            trigger_id: trigger.trigger_id,
177            table_id: trigger.table_id,
178            function_id: trigger.function_id,
179            enabled_mode: TriggerEnableMode::Origin,
180        })
181        .collect()
182}
183
184impl Default for DbCache {
185    fn default() -> Self {
186        Self::new()
187    }
188}
189
190impl DbCache {
191    pub fn new() -> Self {
192        Self {
193            pg_version_num: None,
194            metadata: CacheMetadata::default(),
195            search_path: vec!["public".to_string()],
196            relations: HashMap::new(),
197            foreign_keys: Vec::new(),
198            indexes: Vec::new(),
199            constraints: Vec::new(),
200            triggers: Vec::new(),
201            functions: HashMap::new(),
202            types: HashMap::new(),
203            dependencies: Vec::new(),
204        }
205    }
206
207    pub fn insert_baseline(&mut self, id: ObjectId, state: RelationState) {
208        self.relations.insert(id, state);
209    }
210
211    pub fn baseline_relations(&self) -> impl Iterator<Item = (&ObjectId, &RelationState)> {
212        self.relations.iter()
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn legacy_v1_cache_upgrades_to_current_model() {
222        let cache = DbCacheV1 {
223            pg_version_num: None,
224            relations: HashMap::new(),
225            foreign_keys: Vec::new(),
226            indexes: Vec::new(),
227            triggers: Vec::new(),
228            functions: HashMap::new(),
229        };
230        let versioned = DbCacheVersioned::V1(cache);
231        assert_eq!(versioned.format_version(), 1);
232        let result = versioned.into_cache();
233        assert!(
234            result.is_ok(),
235            "into_cache() should succeed for V1: {:?}",
236            result
237        );
238    }
239
240    #[test]
241    fn current_cache_format_is_v3() {
242        assert_eq!(CACHE_FORMAT_VERSION, 3);
243        assert_eq!(DbCacheVersioned::V3(DbCache::new()).format_version(), 3);
244        assert_eq!(CACHE_V3_MAGIC, b"SMCACHE03");
245    }
246}