1use powdb_storage::catalog::{
2 expression_index_file_name, Catalog, IndexKeySource, CATALOG_LSN_FILE, CATALOG_VERSION,
3 LEGACY_CATALOG_VERSION,
4};
5use powdb_storage::wal::WAL_FORMAT_VERSION;
6use powdb_sync::IdentitySnapshot;
7use powdb_sync::RETAINED_SEGMENT_FORMAT_VERSION;
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeSet;
10use std::io;
11use std::path::Path;
12
13fn legacy_catalog_version() -> u16 {
14 LEGACY_CATALOG_VERSION
15}
16
17pub(crate) fn validate_catalog_version(version: u16) -> io::Result<()> {
18 if !(LEGACY_CATALOG_VERSION..=CATALOG_VERSION).contains(&version) {
19 return Err(io::Error::new(
20 io::ErrorKind::InvalidData,
21 format!("unsupported snapshot catalog format {version}"),
22 ));
23 }
24 Ok(())
25}
26
27pub(crate) fn validate_catalog_transition(base: u16, next: u16) -> io::Result<()> {
28 validate_catalog_version(base)?;
29 validate_catalog_version(next)?;
30 if next < base {
31 return Err(io::Error::new(
32 io::ErrorKind::InvalidData,
33 format!("catalog format cannot move backward from {base} to {next}"),
34 ));
35 }
36 Ok(())
37}
38
39pub(crate) fn active_durable_file_names(catalog: &Catalog) -> BTreeSet<String> {
45 let mut names = BTreeSet::from(["catalog.bin".to_string(), CATALOG_LSN_FILE.to_string()]);
46 for table in catalog.list_tables() {
47 names.insert(format!("{table}.heap"));
48 if let Some(indexes) = catalog.index_metadata(table) {
49 for index in indexes {
50 match index.source {
51 IndexKeySource::Column { column } => {
52 names.insert(format!("{table}_{column}.idx"));
53 }
54 IndexKeySource::Expression { index_id, .. } => {
55 names.insert(expression_index_file_name(table, index_id));
56 }
57 }
58 }
59 }
60 }
61 names
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct FileEntry {
66 pub name: String,
67 pub len: u64,
68 pub blake3_hex: String,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct BackupManifest {
73 pub format_version: u32,
74 pub created_unix_secs: u64,
75 pub source_lsn: u64,
77 #[serde(default = "legacy_catalog_version")]
79 pub catalog_version: u16,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub sync: Option<SyncSnapshotMetadata>,
82 pub files: Vec<FileEntry>,
83}
84
85impl BackupManifest {
86 pub const FORMAT_VERSION: u32 = 1;
87 pub const FILE_NAME: &'static str = "manifest.json";
88
89 pub fn validate_version(&self) -> io::Result<()> {
90 if self.format_version != Self::FORMAT_VERSION {
91 return Err(io::Error::other(format!(
92 "unsupported backup format {} (this build understands {})",
93 self.format_version,
94 Self::FORMAT_VERSION
95 )));
96 }
97 validate_catalog_version(self.catalog_version)?;
98 validate_sync_metadata(&self.sync, self.source_lsn, self.catalog_version)?;
99 Ok(())
100 }
101
102 pub fn write(&self, dir: &Path) -> io::Result<()> {
103 let json = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
104 std::fs::write(dir.join(Self::FILE_NAME), json)
105 }
106
107 pub fn read(dir: &Path) -> io::Result<Self> {
108 let bytes = std::fs::read(dir.join(Self::FILE_NAME))?;
109 let m: BackupManifest = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
110 m.validate_version()?;
111 Ok(m)
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub enum ChangedFile {
118 Whole {
120 name: String,
121 len: u64,
122 blake3_hex: String,
123 },
124 Pages {
128 name: String,
129 total_pages: u32,
131 page_indices: Vec<u32>,
133 delta_file: String,
135 delta_len: u64,
136 delta_blake3_hex: String,
137 },
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141pub struct SyncSnapshotMetadata {
142 pub identity: IdentitySnapshot,
143 pub source_lsn: u64,
144 pub catalog_blake3_hex: String,
145 pub wal_format_version: u16,
146 pub catalog_version: u16,
147 pub retained_segment_format_version: u16,
148}
149
150impl SyncSnapshotMetadata {
151 pub fn current(
152 identity: IdentitySnapshot,
153 source_lsn: u64,
154 catalog_blake3_hex: String,
155 catalog_version: u16,
156 ) -> Self {
157 Self {
158 identity,
159 source_lsn,
160 catalog_blake3_hex,
161 wal_format_version: WAL_FORMAT_VERSION,
162 catalog_version,
163 retained_segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
164 }
165 }
166
167 pub fn validate_against_source_lsn(&self, source_lsn: u64) -> io::Result<()> {
168 self.identity.validate()?;
169 if self.source_lsn != source_lsn {
170 return Err(io::Error::new(
171 io::ErrorKind::InvalidData,
172 format!(
173 "sync snapshot source_lsn {} does not match manifest source_lsn {}",
174 self.source_lsn, source_lsn
175 ),
176 ));
177 }
178 if self.catalog_blake3_hex.is_empty() {
179 return Err(io::Error::new(
180 io::ErrorKind::InvalidData,
181 "sync snapshot catalog hash must be non-empty",
182 ));
183 }
184 if self.wal_format_version != WAL_FORMAT_VERSION {
185 return Err(io::Error::new(
186 io::ErrorKind::InvalidData,
187 format!(
188 "unsupported sync snapshot WAL format {}",
189 self.wal_format_version
190 ),
191 ));
192 }
193 validate_catalog_version(self.catalog_version)?;
194 if self.retained_segment_format_version != RETAINED_SEGMENT_FORMAT_VERSION {
195 return Err(io::Error::new(
196 io::ErrorKind::InvalidData,
197 format!(
198 "unsupported retained segment format {}",
199 self.retained_segment_format_version
200 ),
201 ));
202 }
203 Ok(())
204 }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct IncrementManifest {
210 pub format_version: u32,
212 pub created_unix_secs: u64,
213 pub base_source_lsn: u64,
215 pub source_lsn: u64,
217 #[serde(default = "legacy_catalog_version")]
219 pub catalog_version: u16,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub sync: Option<SyncSnapshotMetadata>,
222 pub changed: Vec<ChangedFile>,
223}
224
225impl IncrementManifest {
226 pub const FORMAT_VERSION: u32 = 1;
227 pub const FILE_NAME: &'static str = "increment.json";
228
229 pub fn validate_version(&self) -> io::Result<()> {
230 if self.format_version != Self::FORMAT_VERSION {
231 return Err(io::Error::other(format!(
232 "unsupported increment format {} (this build understands {})",
233 self.format_version,
234 Self::FORMAT_VERSION
235 )));
236 }
237 validate_catalog_version(self.catalog_version)?;
238 validate_sync_metadata(&self.sync, self.source_lsn, self.catalog_version)?;
239 Ok(())
240 }
241
242 pub fn write(&self, dir: &Path) -> io::Result<()> {
243 let json = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
244 std::fs::write(dir.join(Self::FILE_NAME), json)
245 }
246
247 pub fn read(dir: &Path) -> io::Result<Self> {
248 let bytes = std::fs::read(dir.join(Self::FILE_NAME))?;
249 let m: IncrementManifest = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
250 m.validate_version()?;
251 Ok(m)
252 }
253}
254
255pub(crate) fn current_sync_snapshot_metadata(
256 data_dir: &Path,
257 source_lsn: u64,
258 catalog_version: u16,
259) -> io::Result<Option<SyncSnapshotMetadata>> {
260 let Some(identity) = powdb_sync::read_identity_snapshot_if_exists(data_dir)? else {
261 return Ok(None);
262 };
263 let catalog_bytes = std::fs::read(data_dir.join("catalog.bin"))?;
264 let catalog_blake3_hex = blake3::hash(&catalog_bytes).to_hex().to_string();
265 Ok(Some(SyncSnapshotMetadata::current(
266 identity,
267 source_lsn,
268 catalog_blake3_hex,
269 catalog_version,
270 )))
271}
272
273fn validate_sync_metadata(
274 metadata: &Option<SyncSnapshotMetadata>,
275 source_lsn: u64,
276 catalog_version: u16,
277) -> io::Result<()> {
278 if let Some(metadata) = metadata {
279 metadata.validate_against_source_lsn(source_lsn)?;
280 if metadata.catalog_version != catalog_version {
281 return Err(io::Error::new(
282 io::ErrorKind::InvalidData,
283 "sync snapshot catalog format does not match manifest catalog format",
284 ));
285 }
286 }
287 Ok(())
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 #[test]
294 fn manifest_round_trips_and_rejects_bad_version() {
295 let m = BackupManifest {
296 format_version: BackupManifest::FORMAT_VERSION,
297 created_unix_secs: 1_700_000_000,
298 source_lsn: 42,
299 catalog_version: LEGACY_CATALOG_VERSION,
300 sync: None,
301 files: vec![FileEntry {
302 name: "catalog.bin".into(),
303 len: 10,
304 blake3_hex: "ab".into(),
305 }],
306 };
307 let json = serde_json::to_string(&m).unwrap();
308 let back: BackupManifest = serde_json::from_str(&json).unwrap();
309 assert_eq!(back.source_lsn, 42);
310 assert_eq!(back.files.len(), 1);
311
312 let mut bad = m.clone();
313 bad.format_version = 999;
314 assert!(
315 bad.validate_version().is_err(),
316 "unknown format must be rejected"
317 );
318 }
319}