docbox_database/migrations/
mod.rs

1use crate::{
2    DbExecutor, DbResult, DbTransaction,
3    models::{
4        root_migration::{CreateRootMigration, RootMigration},
5        tenant::Tenant,
6        tenant_migration::{CreateTenantMigration, TenantMigration},
7    },
8};
9use chrono::Utc;
10use std::ops::DerefMut;
11
12pub const ROOT_MIGRATIONS: &[(&str, &str)] = &[
13    (
14        "m1_create_tenants_table",
15        include_str!("./root/m1_create_tenants_table.sql"),
16    ),
17    (
18        "m2_create_tenant_migrations_table",
19        include_str!("./root/m2_create_tenant_migrations_table.sql"),
20    ),
21    (
22        "m3_create_storage_bucket_index",
23        include_str!("./root/m3_create_storage_bucket_index.sql"),
24    ),
25    (
26        "m4_tenant_migrations_update_constraint",
27        include_str!("./root/m4_tenant_migrations_update_constraint.sql"),
28    ),
29];
30
31pub const TENANT_MIGRATIONS: &[(&str, &str)] = &[
32    (
33        "m1_create_users_table",
34        include_str!("./tenant/m1_create_users_table.sql"),
35    ),
36    (
37        "m2_create_document_box_table",
38        include_str!("./tenant/m2_create_document_box_table.sql"),
39    ),
40    (
41        "m3_create_folders_table",
42        include_str!("./tenant/m3_create_folders_table.sql"),
43    ),
44    (
45        "m4_create_files_table",
46        include_str!("./tenant/m4_create_files_table.sql"),
47    ),
48    (
49        "m5_create_generated_files_table",
50        include_str!("./tenant/m5_create_generated_files_table.sql"),
51    ),
52    (
53        "m6_create_links_table",
54        include_str!("./tenant/m6_create_links_table.sql"),
55    ),
56    (
57        "m7_create_edit_history_table",
58        include_str!("./tenant/m7_create_edit_history_table.sql"),
59    ),
60    (
61        "m8_create_tasks_table",
62        include_str!("./tenant/m8_create_tasks_table.sql"),
63    ),
64    (
65        "m9_create_presigned_upload_tasks_table",
66        include_str!("./tenant/m9_create_presigned_upload_tasks_table.sql"),
67    ),
68    (
69        "m10_add_pinned_column",
70        include_str!("./tenant/m10_add_pinned_column.sql"),
71    ),
72    (
73        "m11_create_links_resolved_metadata_table",
74        include_str!("./tenant/m11_create_links_resolved_metadata_table.sql"),
75    ),
76    (
77        "m12_create_composite_types_and_views",
78        include_str!("./tenant/m12_create_composite_types_and_views.sql"),
79    ),
80    (
81        "m13_create_folder_functions",
82        include_str!("./tenant/m13_create_folder_functions.sql"),
83    ),
84    (
85        "m14_create_link_functions",
86        include_str!("./tenant/m14_create_link_functions.sql"),
87    ),
88    (
89        "m15_create_file_functions",
90        include_str!("./tenant/m15_create_file_functions.sql"),
91    ),
92    (
93        "m16_docbox_tasks_constraint",
94        include_str!("./tenant/m16_docbox_tasks_constraint.sql"),
95    ),
96];
97
98/// Initialize the table used for root migration tracking
99///
100/// (Must be performed before normal migrations can happen otherwise tracking will fail)
101pub async fn initialize_root_migrations(db: impl DbExecutor<'_>) -> DbResult<()> {
102    sqlx::raw_sql(include_str!("./root/m0_create_root_migrations_table.sql"))
103        .execute(db)
104        .await?;
105
106    Ok(())
107}
108
109/// Get all pending migrations for a tenant that have not been applied yet
110pub async fn get_pending_tenant_migrations(
111    db: impl DbExecutor<'_>,
112    tenant: &Tenant,
113) -> DbResult<Vec<String>> {
114    let migrations = TenantMigration::find_by_tenant(db, tenant.id, &tenant.env).await?;
115
116    let pending = TENANT_MIGRATIONS
117        .iter()
118        .filter(|(migration_name, _migration)| {
119            // Skip already applied migrations
120            !migrations
121                .iter()
122                .any(|migration| migration.name.eq(migration_name))
123        })
124        .map(|(migration_name, _migration)| migration_name.to_string())
125        .collect();
126
127    Ok(pending)
128}
129
130/// Get all pending migrations for the root that have not been applied yet
131pub async fn get_pending_root_migrations(db: impl DbExecutor<'_>) -> DbResult<Vec<String>> {
132    let migrations = RootMigration::all(db).await?;
133
134    let pending = ROOT_MIGRATIONS
135        .iter()
136        .filter(|(migration_name, _migration)| {
137            // Skip already applied migrations
138            !migrations
139                .iter()
140                .any(|migration| migration.name.eq(migration_name))
141        })
142        .map(|(migration_name, _migration)| migration_name.to_string())
143        .collect();
144
145    Ok(pending)
146}
147
148/// Applies migrations to the provided tenant, only applies migrations that
149/// haven't already been applied
150///
151/// Optionally filtered to a specific migration through `target_migration_name`
152pub async fn apply_tenant_migrations(
153    root_t: &mut DbTransaction<'_>,
154    t: &mut DbTransaction<'_>,
155    tenant: &Tenant,
156    target_migration_name: Option<&str>,
157) -> DbResult<()> {
158    let migrations =
159        TenantMigration::find_by_tenant(root_t.deref_mut(), tenant.id, &tenant.env).await?;
160
161    for (migration_name, migration) in TENANT_MIGRATIONS {
162        // If targeting a specific migration only apply the target one
163        if target_migration_name
164            .is_some_and(|target_migration_name| target_migration_name.ne(*migration_name))
165        {
166            continue;
167        }
168
169        // Skip already applied migrations
170        if migrations
171            .iter()
172            .any(|migration| migration.name.eq(migration_name))
173        {
174            continue;
175        }
176
177        // Apply the migration
178        apply_migration(t, migration_name, migration).await?;
179
180        // Store the applied migration
181        TenantMigration::create(
182            root_t.deref_mut(),
183            CreateTenantMigration {
184                tenant_id: tenant.id,
185                env: tenant.env.clone(),
186                name: migration_name.to_string(),
187                applied_at: Utc::now(),
188            },
189        )
190        .await?;
191    }
192
193    Ok(())
194}
195
196/// Applies migrations to the root, only applies migrations that
197/// haven't already been applied
198///
199/// Optionally filtered to a specific migration through `target_migration_name`
200pub async fn apply_root_migrations(
201    root_t: &mut DbTransaction<'_>,
202    target_migration_name: Option<&str>,
203) -> DbResult<()> {
204    let migrations = RootMigration::all(root_t.deref_mut()).await?;
205
206    for (migration_name, migration) in ROOT_MIGRATIONS {
207        // If targeting a specific migration only apply the target one
208        if target_migration_name
209            .is_some_and(|target_migration_name| target_migration_name.ne(*migration_name))
210        {
211            continue;
212        }
213
214        // Skip already applied migrations
215        if migrations
216            .iter()
217            .any(|migration| migration.name.eq(migration_name))
218        {
219            continue;
220        }
221
222        // Apply the migration
223        apply_migration(root_t, migration_name, migration).await?;
224
225        // Store the applied migration
226        RootMigration::create(
227            root_t.deref_mut(),
228            CreateRootMigration {
229                name: migration_name.to_string(),
230                applied_at: Utc::now(),
231            },
232        )
233        .await?;
234    }
235
236    Ok(())
237}
238
239/// Applies migrations without checking if migrations have already been applied
240///
241/// Should only be used for integration tests where you aren't setting up the root database
242pub async fn force_apply_tenant_migrations(
243    t: &mut DbTransaction<'_>,
244    target_migration_name: Option<&str>,
245) -> DbResult<()> {
246    for (migration_name, migration) in TENANT_MIGRATIONS {
247        // If targeting a specific migration only apply the target one
248        if target_migration_name
249            .is_some_and(|target_migration_name| target_migration_name.ne(*migration_name))
250        {
251            continue;
252        }
253
254        apply_migration(t, migration_name, migration).await?;
255    }
256
257    Ok(())
258}
259
260/// Apply a migration to the specific database
261pub async fn apply_migration(
262    db: &mut DbTransaction<'_>,
263    migration_name: &str,
264    migration: &str,
265) -> DbResult<()> {
266    // Split the SQL queries into multiple queries
267    let queries = migration
268        .split(';')
269        .map(|query| query.trim())
270        .filter(|query| !query.is_empty());
271
272    for query in queries {
273        let result = sqlx::query(query)
274            .execute(db.deref_mut())
275            .await
276            .inspect_err(|error| {
277                tracing::error!(?error, ?migration_name, "failed to perform migration")
278            })?;
279        let rows_affected = result.rows_affected();
280
281        tracing::debug!(?migration_name, ?rows_affected, "applied migration query");
282    }
283
284    Ok(())
285}