docbox_database/migrations/
mod.rs

1use crate::{
2    DbExecutor, DbResult, DbTransaction,
3    models::{
4        tenant::Tenant,
5        tenant_migration::{CreateTenantMigration, TenantMigration},
6    },
7};
8use chrono::Utc;
9use std::ops::DerefMut;
10
11const TENANT_MIGRATIONS: &[(&str, &str)] = &[
12    (
13        "m1_create_users_table",
14        include_str!("./tenant/m1_create_users_table.sql"),
15    ),
16    (
17        "m2_create_document_box_table",
18        include_str!("./tenant/m2_create_document_box_table.sql"),
19    ),
20    (
21        "m3_create_folders_table",
22        include_str!("./tenant/m3_create_folders_table.sql"),
23    ),
24    (
25        "m4_create_files_table",
26        include_str!("./tenant/m4_create_files_table.sql"),
27    ),
28    (
29        "m5_create_generated_files_table",
30        include_str!("./tenant/m5_create_generated_files_table.sql"),
31    ),
32    (
33        "m6_create_links_table",
34        include_str!("./tenant/m6_create_links_table.sql"),
35    ),
36    (
37        "m7_create_edit_history_table",
38        include_str!("./tenant/m7_create_edit_history_table.sql"),
39    ),
40    (
41        "m8_create_tasks_table",
42        include_str!("./tenant/m8_create_tasks_table.sql"),
43    ),
44    (
45        "m9_create_presigned_upload_tasks_table",
46        include_str!("./tenant/m9_create_presigned_upload_tasks_table.sql"),
47    ),
48    (
49        "m10_add_pinned_column",
50        include_str!("./tenant/m10_add_pinned_column.sql"),
51    ),
52];
53
54/// Get all pending migrations for a tenant that have not been applied yet
55pub async fn get_pending_tenant_migrations(
56    db: impl DbExecutor<'_>,
57    tenant: &Tenant,
58) -> DbResult<Vec<String>> {
59    let migrations = TenantMigration::find_by_tenant(db, tenant.id, &tenant.env).await?;
60
61    let pending = TENANT_MIGRATIONS
62        .iter()
63        .filter(|(migration_name, _migration)| {
64            // Skip already applied migrations
65            !migrations
66                .iter()
67                .any(|migration| migration.name.eq(migration_name))
68        })
69        .map(|(migration_name, _migration)| migration_name.to_string())
70        .collect();
71
72    Ok(pending)
73}
74
75/// Applies migrations to the provided tenant, only applies migrations that
76/// haven't already been applied
77///
78/// Optionally filtered to a specific migration through `target_migration_name`
79pub async fn apply_tenant_migrations(
80    root_t: &mut DbTransaction<'_>,
81    t: &mut DbTransaction<'_>,
82    tenant: &Tenant,
83    target_migration_name: Option<&str>,
84) -> DbResult<()> {
85    let migrations =
86        TenantMigration::find_by_tenant(root_t.deref_mut(), tenant.id, &tenant.env).await?;
87
88    for (migration_name, migration) in TENANT_MIGRATIONS {
89        // If targeting a specific migration only apply the target one
90        if target_migration_name
91            .is_some_and(|target_migration_name| target_migration_name.ne(*migration_name))
92        {
93            continue;
94        }
95
96        // Skip already applied migrations
97        if migrations
98            .iter()
99            .any(|migration| migration.name.eq(migration_name))
100        {
101            continue;
102        }
103
104        // Apply the migration
105        apply_tenant_migration(t, migration_name, migration).await?;
106
107        // Store the applied migration
108        TenantMigration::create(
109            root_t.deref_mut(),
110            CreateTenantMigration {
111                tenant_id: tenant.id,
112                env: tenant.env.clone(),
113                name: migration_name.to_string(),
114                applied_at: Utc::now(),
115            },
116        )
117        .await?;
118    }
119
120    Ok(())
121}
122
123/// Applies migrations without checking if migrations have already been applied
124///
125/// Should only be used for integration tests where you aren't setting up the root database
126pub async fn force_apply_tenant_migrations(
127    t: &mut DbTransaction<'_>,
128    target_migration_name: Option<&str>,
129) -> DbResult<()> {
130    for (migration_name, migration) in TENANT_MIGRATIONS {
131        // If targeting a specific migration only apply the target one
132        if target_migration_name
133            .is_some_and(|target_migration_name| target_migration_name.ne(*migration_name))
134        {
135            continue;
136        }
137
138        apply_tenant_migration(t, migration_name, migration).await?;
139    }
140
141    Ok(())
142}
143
144/// Apply a migration to the specific tenant database
145pub async fn apply_tenant_migration(
146    db: &mut DbTransaction<'_>,
147    migration_name: &str,
148    migration: &str,
149) -> DbResult<()> {
150    // Split the SQL queries into multiple queries
151    let queries = migration
152        .split(';')
153        .map(|query| query.trim())
154        .filter(|query| !query.is_empty());
155
156    for query in queries {
157        let result = sqlx::query(query)
158            .execute(db.deref_mut())
159            .await
160            .inspect_err(|error| {
161                tracing::error!(?error, ?migration_name, "failed to perform migration")
162            })?;
163        let rows_affected = result.rows_affected();
164
165        tracing::debug!(?migration_name, ?rows_affected, "applied migration query");
166    }
167
168    Ok(())
169}