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