Skip to main content

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