Skip to main content

microsandbox_migration/
schema_metadata.rs

1//! Static metadata for downgrade planning.
2//!
3//! `Migrator::migrations()` owns the executable migration order. This module
4//! keeps the user-facing downgrade metadata in the same crate so release checks
5//! can ensure every migration has an explicit reversibility and cache-impact
6//! decision before a new binary ships.
7
8use std::collections::HashSet;
9
10//--------------------------------------------------------------------------------------------------
11// Constants
12//--------------------------------------------------------------------------------------------------
13
14/// Version of the hidden schema-baseline JSON shape emitted by the CLI.
15pub const SCHEMA_BASELINE_FORMAT_VERSION: u32 = 1;
16
17/// Oldest release supported by the downgrade flow.
18pub const DOWNGRADE_FLOOR: &str = "0.6.0";
19
20/// Migration that introduced the DB-backed maintenance lease table.
21pub const MAINTENANCE_LEASE_MIGRATION_ID: &str = "m20260621_000002_create_maintenance_lease";
22
23/// Migration that introduced desired-vs-active sandbox config tracking.
24pub const ACTIVE_CONFIG_MIGRATION_ID: &str = "m20260703_000001_add_sandbox_active_config";
25
26/// Migration that adds payload scope metadata to the snapshot index.
27pub const SNAPSHOT_SCOPE_MIGRATION_ID: &str = "m20260714_000001_add_snapshot_scope";
28
29/// Migration that projects final snapshot state and journals legacy conversion.
30pub const SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID: &str =
31    "m20260723_000001_snapshot_artifact_transition";
32
33/// Migration that introduces cooperative host CPU allocation state.
34pub const CPU_ALLOCATION_MIGRATION_ID: &str = "m20260719_000001_create_cpu_allocations";
35
36/// Migration that introduces host-global writeback dirty-credit reservations.
37pub const WRITEBACK_ALLOCATION_MIGRATION_ID: &str = "m20260803_000001_create_writeback_allocations";
38
39/// Migration that records per-NUMA-node guest memory promises for CPU allocations.
40pub const MEMORY_ALLOCATION_NODES_MIGRATION_ID: &str =
41    "m20260808_000001_create_memory_allocation_nodes";
42
43/// Migration that rebuilds the sandbox label index from persisted configs.
44pub const SANDBOX_LABEL_REBUILD_MIGRATION_ID: &str = "m20260810_000001_rebuild_sandbox_labels";
45
46/// Migration that permits several managed vCPUs to share one host logical processor.
47pub const SHARED_CPU_ALLOCATION_MIGRATION_ID: &str = "m20260813_000001_share_cpu_allocations";
48
49/// Frozen migration baseline for the transitional 0.6.0 release.
50///
51/// The released 0.6.0 binary predates `msb __schema-baseline --json`, so
52/// downgrade uses this fixture when inspecting that exact target. Do not extend
53/// this list when adding later migrations; future targets should answer with
54/// their own hidden baseline command.
55pub const BASELINE_0_6_0_MIGRATIONS: &[&str] = &[
56    "m20260305_000001_create_image_tables",
57    "m20260305_000002_create_sandbox_tables",
58    "m20260305_000003_create_storage_tables",
59    "m20260305_000004_create_sandbox_images_table",
60    "m20260410_000001_erofs_image_schema",
61    "m20260501_000001_create_snapshot_index",
62    "m20260517_000001_drop_sandbox_metric",
63    "m20260527_000001_migrate_oci_rootfs_source",
64    "m20260531_000001_create_sandbox_labels",
65    "m20260531_000002_index_sandbox_labels_key_value",
66    "m20260606_000001_named_volume_kinds",
67    "m20260621_000001_add_sandbox_ephemeral",
68    MAINTENANCE_LEASE_MIGRATION_ID,
69];
70
71/// Metadata for every migration in `Migrator::migrations()` order.
72pub const MIGRATION_METADATA: &[MigrationMetadata] = &[
73    MigrationMetadata {
74        id: "m20260305_000001_create_image_tables",
75        reversible: true,
76        affects_cache: true,
77        affects_user_data: false,
78        summary: "remove legacy OCI image catalog tables",
79    },
80    MigrationMetadata {
81        id: "m20260305_000002_create_sandbox_tables",
82        reversible: true,
83        affects_cache: false,
84        affects_user_data: false,
85        summary: "remove sandbox and run tables",
86    },
87    MigrationMetadata {
88        id: "m20260305_000003_create_storage_tables",
89        reversible: true,
90        affects_cache: false,
91        affects_user_data: false,
92        summary: "remove volume and snapshot storage tables",
93    },
94    MigrationMetadata {
95        id: "m20260305_000004_create_sandbox_images_table",
96        reversible: true,
97        affects_cache: true,
98        affects_user_data: false,
99        summary: "remove sandbox image references",
100    },
101    MigrationMetadata {
102        id: "m20260410_000001_erofs_image_schema",
103        reversible: true,
104        affects_cache: true,
105        affects_user_data: false,
106        summary: "remove EROFS rootfs catalog tables",
107    },
108    MigrationMetadata {
109        id: "m20260501_000001_create_snapshot_index",
110        reversible: true,
111        affects_cache: false,
112        affects_user_data: false,
113        summary: "remove snapshot index table",
114    },
115    MigrationMetadata {
116        id: "m20260517_000001_drop_sandbox_metric",
117        reversible: false,
118        affects_cache: false,
119        affects_user_data: false,
120        summary: "restore legacy sandbox metrics table",
121    },
122    MigrationMetadata {
123        id: "m20260527_000001_migrate_oci_rootfs_source",
124        reversible: false,
125        affects_cache: false,
126        affects_user_data: false,
127        summary: "rewrite OCI rootfs config back to the legacy string shape",
128    },
129    MigrationMetadata {
130        id: "m20260531_000001_create_sandbox_labels",
131        reversible: true,
132        affects_cache: false,
133        affects_user_data: false,
134        summary: "remove sandbox labels table",
135    },
136    MigrationMetadata {
137        id: "m20260531_000002_index_sandbox_labels_key_value",
138        reversible: true,
139        affects_cache: false,
140        affects_user_data: false,
141        summary: "remove sandbox label key/value index",
142    },
143    MigrationMetadata {
144        id: "m20260606_000001_named_volume_kinds",
145        reversible: true,
146        affects_cache: false,
147        affects_user_data: false,
148        summary: "remove named volume kind columns and attachments",
149    },
150    MigrationMetadata {
151        id: "m20260621_000001_add_sandbox_ephemeral",
152        reversible: true,
153        affects_cache: false,
154        affects_user_data: false,
155        summary: "remove sandbox ephemeral flag",
156    },
157    MigrationMetadata {
158        id: MAINTENANCE_LEASE_MIGRATION_ID,
159        reversible: true,
160        affects_cache: false,
161        affects_user_data: false,
162        summary: "remove maintenance lease table",
163    },
164    MigrationMetadata {
165        id: ACTIVE_CONFIG_MIGRATION_ID,
166        reversible: true,
167        affects_cache: false,
168        affects_user_data: false,
169        summary: "remove active sandbox config snapshots",
170    },
171    MigrationMetadata {
172        id: "m20260708_000001_migrate_bind_rootfs_source",
173        reversible: true,
174        affects_cache: false,
175        affects_user_data: true,
176        summary: "rewrite bind rootfs config back to the legacy string shape",
177    },
178    MigrationMetadata {
179        id: "m20260710_000001_migrate_root_disk",
180        reversible: true,
181        affects_cache: false,
182        affects_user_data: true,
183        summary: "rewrite root disk config back to the upper size shape",
184    },
185    MigrationMetadata {
186        id: SNAPSHOT_SCOPE_MIGRATION_ID,
187        reversible: true,
188        affects_cache: false,
189        affects_user_data: false,
190        summary: "remove snapshot scope index metadata",
191    },
192    MigrationMetadata {
193        id: SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID,
194        reversible: true,
195        affects_cache: false,
196        affects_user_data: true,
197        summary: "reverse final snapshot descriptors before removing migration state",
198    },
199    MigrationMetadata {
200        id: CPU_ALLOCATION_MIGRATION_ID,
201        reversible: true,
202        affects_cache: false,
203        affects_user_data: false,
204        summary: "remove cooperative host CPU allocation tables",
205    },
206    MigrationMetadata {
207        id: WRITEBACK_ALLOCATION_MIGRATION_ID,
208        reversible: true,
209        affects_cache: false,
210        affects_user_data: false,
211        summary: "remove host-global writeback allocation state",
212    },
213    MigrationMetadata {
214        id: MEMORY_ALLOCATION_NODES_MIGRATION_ID,
215        reversible: true,
216        affects_cache: false,
217        affects_user_data: false,
218        summary: "remove cooperative NUMA memory allocation state",
219    },
220    MigrationMetadata {
221        id: SANDBOX_LABEL_REBUILD_MIGRATION_ID,
222        reversible: true,
223        affects_cache: false,
224        affects_user_data: false,
225        summary: "retain the rebuilt sandbox label index",
226    },
227    MigrationMetadata {
228        id: SHARED_CPU_ALLOCATION_MIGRATION_ID,
229        reversible: true,
230        affects_cache: false,
231        affects_user_data: false,
232        summary: "restore exclusive logical CPU allocation rows",
233    },
234];
235
236//--------------------------------------------------------------------------------------------------
237// Types
238//--------------------------------------------------------------------------------------------------
239
240/// Downgrade metadata for one migration.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub struct MigrationMetadata {
243    /// Migration identifier returned by `MigrationName::name()`.
244    pub id: &'static str,
245
246    /// Whether `down()` actually restores a target-compatible schema/state.
247    pub reversible: bool,
248
249    /// Whether rolling this migration back invalidates re-pullable image cache
250    /// contents on disk.
251    pub affects_cache: bool,
252
253    /// Whether rolling this migration back may leave snapshots or disk-backed
254    /// named volumes in a format the target release cannot read.
255    pub affects_user_data: bool,
256
257    /// Short human-readable summary used in destructive downgrade prompts.
258    pub summary: &'static str,
259}
260
261//--------------------------------------------------------------------------------------------------
262// Functions
263//--------------------------------------------------------------------------------------------------
264
265/// Return all migration identifiers in schema order.
266pub fn migration_ids() -> impl Iterator<Item = &'static str> {
267    MIGRATION_METADATA.iter().map(|metadata| metadata.id)
268}
269
270/// Resolve an unordered collection of applied migration identifiers to its canonical prefix.
271///
272/// SeaORM records migration timestamps with insufficient precision to recover execution order
273/// when several migrations run together. Treat the migration table as a set and use this binary's
274/// append-only metadata as the only source of ordering instead.
275pub fn canonical_applied_prefix<'a>(
276    applied_ids: impl IntoIterator<Item = &'a str>,
277) -> Option<&'static [MigrationMetadata]> {
278    let mut applied_count = 0;
279    let applied_ids: HashSet<_> = applied_ids
280        .into_iter()
281        .inspect(|_| applied_count += 1)
282        .collect();
283
284    // Duplicate migration rows are invalid even if their distinct identifiers resemble a prefix.
285    if applied_ids.len() != applied_count {
286        return None;
287    }
288
289    let prefix = MIGRATION_METADATA.get(..applied_count)?;
290    prefix
291        .iter()
292        .all(|metadata| applied_ids.contains(metadata.id))
293        .then_some(prefix)
294}
295
296//--------------------------------------------------------------------------------------------------
297// Tests
298//--------------------------------------------------------------------------------------------------
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use crate::{Migrator, MigratorTrait};
304
305    #[test]
306    fn metadata_matches_migrator_order() {
307        let migrations = Migrator::migrations();
308        let migrator_ids: Vec<_> = migrations
309            .iter()
310            .map(|migration| migration.name().to_string())
311            .collect();
312        let metadata_ids: Vec<_> = migration_ids().map(str::to_string).collect();
313
314        assert_eq!(metadata_ids, migrator_ids);
315    }
316
317    #[test]
318    fn canonical_applied_prefix_uses_metadata_order() {
319        let applied = [
320            SHARED_CPU_ALLOCATION_MIGRATION_ID,
321            SANDBOX_LABEL_REBUILD_MIGRATION_ID,
322            MEMORY_ALLOCATION_NODES_MIGRATION_ID,
323            WRITEBACK_ALLOCATION_MIGRATION_ID,
324            SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID,
325            CPU_ALLOCATION_MIGRATION_ID,
326        ];
327        let prefix_len = MIGRATION_METADATA.len();
328        let mut all_applied: Vec<_> = MIGRATION_METADATA[..prefix_len - applied.len()]
329            .iter()
330            .map(|metadata| metadata.id)
331            .collect();
332        all_applied.extend(applied);
333
334        let prefix = canonical_applied_prefix(all_applied).expect("valid unordered prefix");
335        assert_eq!(prefix, MIGRATION_METADATA);
336    }
337
338    #[test]
339    fn canonical_applied_prefix_rejects_gaps_and_unknown_migrations() {
340        let without_first = MIGRATION_METADATA
341            .iter()
342            .skip(1)
343            .map(|metadata| metadata.id);
344        assert!(canonical_applied_prefix(without_first).is_none());
345
346        let with_unknown = MIGRATION_METADATA
347            .iter()
348            .map(|metadata| metadata.id)
349            .chain(["m20990101_000001_future"]);
350        assert!(canonical_applied_prefix(with_unknown).is_none());
351    }
352
353    #[test]
354    fn frozen_0_6_0_baseline_is_current_prefix() {
355        let metadata_ids: Vec<_> = migration_ids().collect();
356        assert!(metadata_ids.starts_with(BASELINE_0_6_0_MIGRATIONS));
357    }
358}