oxisql_core/migrator.rs
1//! [`Migrator`] trait — abstract interface for applying and rolling back
2//! database schema migrations.
3
4use std::collections::HashSet;
5
6use async_trait::async_trait;
7
8use crate::OxiSqlError;
9
10/// The execution status of a single migration version.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum MigrationStatus {
13 /// The migration has been applied to the target database.
14 Applied,
15 /// The migration has not yet been applied.
16 Pending,
17 /// The migration status could not be determined (e.g. checksum mismatch).
18 Unknown,
19}
20
21/// Metadata describing a single migration version.
22#[derive(Debug, Clone)]
23pub struct MigrationInfo {
24 /// The numeric version identifier (e.g. timestamp or sequential number).
25 pub version: u64,
26 /// A human-readable name for the migration.
27 pub name: String,
28 /// The current status of this migration.
29 pub status: MigrationStatus,
30 /// ISO-8601 timestamp at which the migration was applied, if known.
31 pub applied_at: Option<String>,
32 /// Checksum of the migration SQL, if computed.
33 pub checksum: Option<String>,
34}
35
36/// An async trait for managing database schema migrations.
37///
38/// Implementors are expected to be `Send + Sync` so they can be used safely
39/// across async task boundaries.
40///
41/// # Default implementation
42///
43/// [`Migrator::pending`] has a default body that calls [`Migrator::status`]
44/// and returns any version from `all_versions` that is not yet
45/// [`MigrationStatus::Applied`]. Backends may override this with a more
46/// efficient query.
47#[async_trait]
48pub trait Migrator: Send + Sync {
49 /// Apply a migration identified by `version` by executing `sql`.
50 ///
51 /// The implementation is expected to record the successful application
52 /// (e.g. by inserting a row into a migration history table) so that
53 /// [`status`](Migrator::status) reflects the change.
54 async fn apply(&mut self, version: u64, sql: &str) -> Result<(), OxiSqlError>;
55
56 /// Roll back a previously applied migration by executing `down_sql`.
57 ///
58 /// The implementation should remove or update the corresponding history
59 /// record so that [`status`](Migrator::status) no longer reports this
60 /// version as [`MigrationStatus::Applied`].
61 async fn rollback(&mut self, version: u64, down_sql: &str) -> Result<(), OxiSqlError>;
62
63 /// Return metadata about all known migrations and their current status.
64 async fn status(&self) -> Result<Vec<MigrationInfo>, OxiSqlError>;
65
66 /// Return the subset of `all_versions` that have not yet been applied.
67 ///
68 /// The default implementation calls [`status`](Migrator::status) and
69 /// filters out already-applied versions. Backends may override this with
70 /// a cheaper single-query implementation.
71 async fn pending(&self, all_versions: &[u64]) -> Result<Vec<u64>, OxiSqlError> {
72 let applied: HashSet<u64> = self
73 .status()
74 .await?
75 .into_iter()
76 .filter(|m| m.status == MigrationStatus::Applied)
77 .map(|m| m.version)
78 .collect();
79 Ok(all_versions
80 .iter()
81 .filter(|v| !applied.contains(v))
82 .copied()
83 .collect())
84 }
85}