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