Skip to main content

ferro_deployments/
migration.rs

1//! `CreateDeploymentsTable` + `CreateDeploymentPointersTable` —
2//! SeaORM migrations creating the `deployments` and `deployment_pointers`
3//! tables. Portable across SQLite + Postgres: no backend-specific SQL,
4//! only the SchemaManager DDL builder.
5//!
6//! Consumers register both in their own `Migrator` in order:
7//! ```rust,ignore
8//! impl MigratorTrait for Migrator {
9//!     fn migrations() -> Vec<Box<dyn MigrationTrait>> {
10//!         vec![
11//!             Box::new(ferro_deployments::CreateDeploymentsTable),
12//!             Box::new(ferro_deployments::CreateDeploymentPointersTable),
13//!             // ... your app migrations
14//!         ]
15//!     }
16//! }
17//! ```
18
19use sea_orm_migration::prelude::*;
20
21/// Migration that creates the `deployments` table.
22pub struct CreateDeploymentsTable;
23
24impl sea_orm_migration::MigrationName for CreateDeploymentsTable {
25    fn name(&self) -> &str {
26        "m_create_deployments_table"
27    }
28}
29
30#[async_trait::async_trait]
31impl MigrationTrait for CreateDeploymentsTable {
32    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
33        manager
34            .create_table(
35                Table::create()
36                    .table(Deployments::Table)
37                    .if_not_exists()
38                    .col(
39                        ColumnDef::new(Deployments::Id)
40                            .big_integer()
41                            .not_null()
42                            .auto_increment()
43                            .primary_key(),
44                    )
45                    .col(
46                        ColumnDef::new(Deployments::Identifier)
47                            .string()
48                            .not_null()
49                            .unique_key(),
50                    )
51                    .col(ColumnDef::new(Deployments::OwnerKey).string().not_null())
52                    .col(ColumnDef::new(Deployments::SourceRef).string().null())
53                    .col(
54                        ColumnDef::new(Deployments::ArtifactLocation)
55                            .string()
56                            .null(),
57                    )
58                    .col(ColumnDef::new(Deployments::ByteSize).big_integer().null())
59                    .col(
60                        ColumnDef::new(Deployments::Status)
61                            .string()
62                            .not_null()
63                            .default("building"),
64                    )
65                    .col(
66                        ColumnDef::new(Deployments::ArtifactDeletedAt)
67                            .timestamp_with_time_zone()
68                            .null(),
69                    )
70                    .col(
71                        ColumnDef::new(Deployments::TerminatedAt)
72                            .timestamp_with_time_zone()
73                            .null(),
74                    )
75                    .col(
76                        ColumnDef::new(Deployments::CreatedAt)
77                            .timestamp_with_time_zone()
78                            .not_null(),
79                    )
80                    .to_owned(),
81            )
82            .await
83    }
84
85    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
86        manager
87            .drop_table(Table::drop().table(Deployments::Table).to_owned())
88            .await
89    }
90}
91
92#[derive(DeriveIden)]
93pub(crate) enum Deployments {
94    Table,
95    Id,
96    Identifier,
97    OwnerKey,
98    SourceRef,
99    ArtifactLocation,
100    ByteSize,
101    Status,
102    ArtifactDeletedAt,
103    TerminatedAt,
104    CreatedAt,
105}
106
107/// Migration that creates the `deployment_pointers` table.
108pub struct CreateDeploymentPointersTable;
109
110impl sea_orm_migration::MigrationName for CreateDeploymentPointersTable {
111    fn name(&self) -> &str {
112        "m_create_deployment_pointers_table"
113    }
114}
115
116#[async_trait::async_trait]
117impl MigrationTrait for CreateDeploymentPointersTable {
118    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
119        manager
120            .create_table(
121                Table::create()
122                    .table(DeploymentPointers::Table)
123                    .if_not_exists()
124                    .col(
125                        ColumnDef::new(DeploymentPointers::OwnerKey)
126                            .string()
127                            .not_null()
128                            .primary_key(),
129                    )
130                    .col(
131                        ColumnDef::new(DeploymentPointers::DeploymentId)
132                            .big_integer()
133                            .not_null(),
134                    )
135                    .col(
136                        ColumnDef::new(DeploymentPointers::PreviousDeploymentId)
137                            .big_integer()
138                            .null(),
139                    )
140                    .col(
141                        ColumnDef::new(DeploymentPointers::UpdatedAt)
142                            .timestamp_with_time_zone()
143                            .not_null(),
144                    )
145                    .to_owned(),
146            )
147            .await
148    }
149
150    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
151        manager
152            .drop_table(Table::drop().table(DeploymentPointers::Table).to_owned())
153            .await
154    }
155}
156
157#[derive(DeriveIden)]
158pub(crate) enum DeploymentPointers {
159    Table,
160    OwnerKey,
161    DeploymentId,
162    PreviousDeploymentId,
163    UpdatedAt,
164}
165
166#[cfg(test)]
167mod tests {
168    use sea_orm::{ConnectionTrait, Database, DatabaseBackend, Statement};
169    use sea_orm_migration::MigratorTrait;
170
171    struct TestMigrator;
172
173    #[async_trait::async_trait]
174    impl MigratorTrait for TestMigrator {
175        fn migrations() -> Vec<Box<dyn sea_orm_migration::MigrationTrait>> {
176            vec![
177                Box::new(super::CreateDeploymentsTable),
178                Box::new(super::CreateDeploymentPointersTable),
179            ]
180        }
181    }
182
183    #[tokio::test]
184    async fn migration_creates_deployments_table() {
185        let conn = Database::connect("sqlite::memory:")
186            .await
187            .expect("connect to in-memory sqlite");
188
189        TestMigrator::up(&conn, None)
190            .await
191            .expect("run migration up");
192
193        // Verify the deployments table exists.
194        let table_row = conn
195            .query_one(Statement::from_string(
196                DatabaseBackend::Sqlite,
197                "SELECT name FROM sqlite_master WHERE type='table' AND name='deployments'"
198                    .to_string(),
199            ))
200            .await
201            .expect("query sqlite_master for deployments table");
202        assert!(
203            table_row.is_some(),
204            "deployments table not created by migration"
205        );
206
207        // Verify the deployment_pointers table exists.
208        let pointers_row = conn
209            .query_one(Statement::from_string(
210                DatabaseBackend::Sqlite,
211                "SELECT name FROM sqlite_master WHERE type='table' AND name='deployment_pointers'"
212                    .to_string(),
213            ))
214            .await
215            .expect("query sqlite_master for deployment_pointers table");
216        assert!(
217            pointers_row.is_some(),
218            "deployment_pointers table not created by migration"
219        );
220
221        // Verify artifact_deleted_at column is present in deployments table.
222        let pragma_rows = conn
223            .query_all(Statement::from_string(
224                DatabaseBackend::Sqlite,
225                "PRAGMA table_info(deployments)".to_string(),
226            ))
227            .await
228            .expect("PRAGMA table_info(deployments)");
229        let col_names: Vec<String> = pragma_rows
230            .iter()
231            .filter_map(|row| row.try_get_by::<String, _>("name").ok())
232            .collect();
233        assert!(
234            col_names.iter().any(|c| c == "artifact_deleted_at"),
235            "artifact_deleted_at column not found in deployments table; columns: {col_names:?}"
236        );
237
238        // Verify down() drops both tables.
239        TestMigrator::down(&conn, None)
240            .await
241            .expect("run migration down");
242
243        let deployments_after_down = conn
244            .query_one(Statement::from_string(
245                DatabaseBackend::Sqlite,
246                "SELECT name FROM sqlite_master WHERE type='table' AND name='deployments'"
247                    .to_string(),
248            ))
249            .await
250            .expect("query sqlite_master after down");
251        assert!(
252            deployments_after_down.is_none(),
253            "deployments table should be dropped by down()"
254        );
255
256        let pointers_after_down = conn
257            .query_one(Statement::from_string(
258                DatabaseBackend::Sqlite,
259                "SELECT name FROM sqlite_master WHERE type='table' AND name='deployment_pointers'"
260                    .to_string(),
261            ))
262            .await
263            .expect("query sqlite_master for deployment_pointers after down");
264        assert!(
265            pointers_after_down.is_none(),
266            "deployment_pointers table should be dropped by down()"
267        );
268    }
269}