Skip to main content

vespertide_core/
migration.rs

1/// Runtime options controlling how Vespertide tracks applied migrations.
2///
3/// Pass this to the migration runner to configure the version-tracking table name.
4/// The default table name used by the `vespertide_migration!` macro is `"vespertide_migrations"`.
5///
6/// `MigrationOptions` is `#[non_exhaustive]`, so external callers must construct
7/// via `MigrationOptions::new()` or `Default::default()` rather than struct literals.
8///
9/// Construction:
10///
11/// ```rust
12/// use vespertide_core::MigrationOptions;
13///
14/// // Named constructor — preferred when overriding the version table.
15/// let opts = MigrationOptions::new("app_migrations");
16/// assert_eq!(opts.version_table, "app_migrations");
17///
18/// // Default gives the standard tracking table name.
19/// let default_opts = MigrationOptions::default();
20/// assert_eq!(default_opts.version_table, "vespertide_migrations");
21/// ```
22#[derive(Debug, Clone)]
23#[non_exhaustive]
24pub struct MigrationOptions {
25    /// Name of the table used to record which migration versions have been applied.
26    ///
27    /// Defaults to `"vespertide_migrations"`. Override this when multiple Vespertide-managed
28    /// schemas share the same database and need separate version tables.
29    pub version_table: String,
30}
31
32impl MigrationOptions {
33    /// Create a new `MigrationOptions` with the specified version table name.
34    ///
35    /// Construction via `new`:
36    ///
37    /// ```rust
38    /// use vespertide_core::MigrationOptions;
39    ///
40    /// let opts = MigrationOptions::new("tenant_migrations");
41    /// assert_eq!(opts.version_table, "tenant_migrations");
42    /// ```
43    #[must_use]
44    pub fn new(version_table: impl Into<String>) -> Self {
45        Self {
46            version_table: version_table.into(),
47        }
48    }
49}
50
51impl Default for MigrationOptions {
52    fn default() -> Self {
53        Self {
54            version_table: "vespertide_migrations".to_string(),
55        }
56    }
57}
58
59#[derive(thiserror::Error, Debug)]
60pub enum MigrationError {
61    #[error("migration execution is not yet implemented")]
62    NotImplemented,
63    #[error("database error: {0}")]
64    #[deprecated(
65        since = "0.1.62",
66        note = "Use Database { message, source } for proper error source chains"
67    )]
68    DatabaseError(String),
69    #[error("database error: {message}")]
70    Database {
71        message: String,
72        #[source]
73        source: Option<Box<dyn std::error::Error + Send + Sync>>,
74    },
75    #[error(
76        "migration id mismatch for version {version}: expected '{expected}', found '{found}' in database"
77    )]
78    IdMismatch {
79        version: u32,
80        expected: String,
81        found: String,
82    },
83}
84
85impl From<sea_orm::DbErr> for MigrationError {
86    fn from(err: sea_orm::DbErr) -> Self {
87        Self::Database {
88            message: err.to_string(),
89            source: Some(Box::new(err)),
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    //! Coverage-closure tests for `MigrationOptions::new` + `Default` + `MigrationError`.
97    //! Targets `uncovered-detail.json` lines 44, 45 (new) and 50, 51 (Default).
98    use super::*;
99
100    #[test]
101    fn new_constructs_with_override_table_name() {
102        // Covers lines 44, 45 (MigrationOptions::new body).
103        let opts = MigrationOptions::new("tenant_migrations");
104        assert_eq!(opts.version_table, "tenant_migrations");
105    }
106
107    #[test]
108    fn new_accepts_owned_string_via_into() {
109        // Re-exercises the `impl Into<String>` path on lines 44, 45.
110        let owned: String = String::from("audit_migrations");
111        let opts = MigrationOptions::new(owned);
112        assert_eq!(opts.version_table, "audit_migrations");
113    }
114
115    #[test]
116    fn default_uses_canonical_version_table() {
117        // Covers lines 50, 51 (Default::default body).
118        let opts = MigrationOptions::default();
119        assert_eq!(opts.version_table, "vespertide_migrations");
120    }
121
122    #[test]
123    fn migration_error_not_implemented_display() {
124        let err = MigrationError::NotImplemented;
125        assert_eq!(
126            err.to_string(),
127            "migration execution is not yet implemented"
128        );
129    }
130
131    #[test]
132    fn migration_error_database_struct_display() {
133        let err = MigrationError::Database {
134            message: "connection refused".to_string(),
135            source: None,
136        };
137        assert_eq!(err.to_string(), "database error: connection refused");
138    }
139
140    #[test]
141    fn migration_error_id_mismatch_display() {
142        let err = MigrationError::IdMismatch {
143            version: 7,
144            expected: "abc".to_string(),
145            found: "def".to_string(),
146        };
147        assert!(err.to_string().contains("version 7"));
148        assert!(err.to_string().contains("'abc'"));
149        assert!(err.to_string().contains("'def'"));
150    }
151}