Skip to main content

mongreldb_kit/
migrate.rs

1//! Migration runner for `mongreldb-kit`.
2
3use crate::db::internal_bytes;
4use crate::error::{KitError, Result};
5use crate::internal::{
6    cols, ensure_internal_tables, iso_now, MIGRATIONS_TABLE, ROW_GUARDS, UNIQUE_KEYS,
7};
8use crate::schema::{core_row_to_json, to_core_schema, Row as KitRow};
9use crate::txn::{encoded_pk_for, fk_values_null, parent_pk_components, unique_key};
10use mongreldb_core::memtable::{Row as CoreRow, Value as CoreValue};
11use mongreldb_core::{AlterColumn, Database as CoreDatabase};
12use mongreldb_kit_core::keys::{encode_pk, encode_row_guard_key, KeyComponent};
13use mongreldb_kit_core::migrations::{plan_migrations, Migration, MigrationOp};
14use mongreldb_kit_core::schema::{Schema as KitSchema, Table as KitTable};
15use std::collections::{HashMap, HashSet};
16
17/// Run pending migrations against `db`.
18///
19/// Creates internal tables if missing, applies each pending migration in
20/// version order, and records it in `__kit_schema_migrations`.
21pub fn migrate(db: &mut crate::db::Database, migrations: &[Migration]) -> Result<()> {
22    let core = db.core_db();
23
24    // Ensure internal tables exist (idempotent on an already-created DB).
25    ensure_internal_tables(core)?;
26
27    let applied = load_applied_migrations(core)?;
28    let pending = plan_migrations(&applied, migrations);
29
30    if pending.is_empty() {
31        return Ok(());
32    }
33
34    // Apply DDL ops directly. Each op is individually durable in core; the
35    // migration record transaction below makes the applied versions atomic.
36    for migration in &pending {
37        apply_migration_ops(core, migration, &db.schema)?;
38    }
39
40    // Record all newly-applied migrations in one transaction.
41    let mut txn = core.begin();
42    for migration in &pending {
43        record_migration(&mut txn, migration)?;
44    }
45    txn.commit().map_err(KitError::from)?;
46
47    // Persist the updated schema and reload it.
48    crate::db::persist_schema(db, &db.schema)?;
49    let fresh = crate::db::load_schema(db.root())?;
50    db.set_schema(fresh);
51    Ok(())
52}
53
54pub fn load_applied_migrations(core: &CoreDatabase) -> Result<Vec<Migration>> {
55    let handle = core.table(MIGRATIONS_TABLE).map_err(KitError::from)?;
56    let guard = handle.lock();
57    let snapshot = guard.snapshot();
58    let rows = guard.visible_rows(snapshot).map_err(KitError::from)?;
59
60    let mut out: Vec<Migration> = rows
61        .into_iter()
62        .filter_map(|r| migration_from_row(&r).ok())
63        .collect();
64    out.sort_by_key(|m| m.version);
65    Ok(out)
66}
67
68fn migration_from_row(row: &CoreRow) -> Result<Migration> {
69    let version = match row.columns.get(&1).cloned().unwrap_or(CoreValue::Null) {
70        CoreValue::Int64(i) => i,
71        _ => return Err(KitError::Integrity("migration version missing".into())),
72    };
73    let name = bytes_string(row.columns.get(&2))?.unwrap_or_default();
74    Ok(Migration {
75        version,
76        name,
77        ops: Vec::new(),
78    })
79}
80
81fn bytes_string(value: Option<&CoreValue>) -> Result<Option<String>> {
82    match value {
83        Some(CoreValue::Bytes(b)) => Ok(Some(
84            String::from_utf8(b.clone()).map_err(|e| KitError::Integrity(e.to_string()))?,
85        )),
86        Some(CoreValue::Null) | None => Ok(None),
87        _ => Err(KitError::Integrity("expected bytes value".into())),
88    }
89}
90
91fn apply_migration_ops(
92    core: &CoreDatabase,
93    migration: &Migration,
94    schema: &KitSchema,
95) -> Result<()> {
96    for op in &migration.ops {
97        match op {
98            MigrationOp::CreateTable { name } => {
99                if let Some(table) = schema.table(name) {
100                    if core.table_id(name).is_err() {
101                        core.create_table(name, to_core_schema(table))
102                            .map_err(KitError::from)?;
103                    }
104                }
105            }
106            MigrationOp::DropTable { name } => {
107                // Drop is best-effort (idempotent), then forget the table's
108                // unique-key and row guards.
109                let _ = core.drop_table(name);
110                clean_table_guards(core, name)?;
111            }
112            MigrationOp::AddColumn { table, column } => {
113                if let Some(t) = schema.table(table) {
114                    if let Some(col) = t.column(column) {
115                        let handle = core.table(table).map_err(KitError::from)?;
116                        let mut guard = handle.lock();
117                        guard
118                            .add_column(
119                                column,
120                                crate::schema::to_core_type(col.storage_type),
121                                crate::schema::to_core_flags(t, col),
122                            )
123                            .map_err(KitError::from)?;
124                    }
125                }
126            }
127            MigrationOp::AddUnique { table, constraint } => {
128                backfill_unique(core, schema, table, constraint)?;
129            }
130            MigrationOp::DropUnique { table, constraint } => {
131                drop_unique_guards(core, table, constraint)?;
132            }
133            MigrationOp::AddForeignKey { table, constraint } => {
134                backfill_foreign_key(core, schema, table, constraint)?;
135            }
136            MigrationOp::AlterColumn { table, column } => {
137                alter_column(core, schema, table, column)?;
138            }
139            MigrationOp::AddCheck { .. }
140            | MigrationOp::DropCheck { .. }
141            | MigrationOp::DropForeignKey { .. } => {
142                // Metadata-only: check evaluation, foreign-key enforcement, and
143                // dropped foreign-key definitions are driven by the re-persisted
144                // schema, so there is no catalog or guard mutation to perform
145                // here.
146            }
147            MigrationOp::CreateProcedure { procedure, .. } => {
148                let parsed: mongreldb_core::StoredProcedure =
149                    serde_json::from_value(procedure.json.clone()).map_err(KitError::from)?;
150                let procedure = mongreldb_core::StoredProcedure::new(
151                    parsed.name,
152                    parsed.mode,
153                    parsed.params,
154                    parsed.body,
155                    0,
156                )
157                .map_err(KitError::from)?;
158                core.create_procedure(procedure).map_err(KitError::from)?;
159            }
160            MigrationOp::ReplaceProcedure { name: _, procedure } => {
161                let parsed: mongreldb_core::StoredProcedure =
162                    serde_json::from_value(procedure.json.clone()).map_err(KitError::from)?;
163                let procedure = mongreldb_core::StoredProcedure::new(
164                    parsed.name,
165                    parsed.mode,
166                    parsed.params,
167                    parsed.body,
168                    0,
169                )
170                .map_err(KitError::from)?;
171                core.create_or_replace_procedure(procedure)
172                    .map_err(KitError::from)?;
173            }
174            MigrationOp::DropProcedure { name } => {
175                let _ = core.drop_procedure(name);
176            }
177            MigrationOp::DropColumn { table, column } => {
178                let target = schema.table(table).ok_or_else(|| {
179                    KitError::Migration(format!("drop_column: table {table} not found in schema"))
180                })?;
181                if target.column(column).is_some() {
182                    return Err(KitError::Migration(format!(
183                        "drop_column: target schema still contains {table}.{column}"
184                    )));
185                }
186                rebuild_table(core, target)?;
187                drop_stale_unique_guards(core, target)?;
188            }
189            MigrationOp::AddIndex { table, index } => {
190                let target = schema.table(table).ok_or_else(|| {
191                    KitError::Migration(format!("add_index: table {table} not found in schema"))
192                })?;
193                let idx = target
194                    .indexes
195                    .iter()
196                    .find(|idx| idx.name == *index)
197                    .ok_or_else(|| {
198                        KitError::Migration(format!(
199                            "add_index: index {index} not found on table {table} in schema"
200                        ))
201                    })?;
202                if idx.unique {
203                    backfill_unique(core, schema, table, index)?;
204                }
205                rebuild_table(core, target)?;
206            }
207            MigrationOp::DropIndex { table, index } => {
208                let target = schema.table(table).ok_or_else(|| {
209                    KitError::Migration(format!("drop_index: table {table} not found in schema"))
210                })?;
211                if target.indexes.iter().any(|idx| idx.name == *index) {
212                    return Err(KitError::Migration(format!(
213                        "drop_index: target schema still contains index {index} on {table}"
214                    )));
215                }
216                rebuild_table(core, target)?;
217                drop_stale_unique_guards(core, target)?;
218            }
219            MigrationOp::RawSql(sql) => {
220                return Err(KitError::Migration(format!(
221                    "RawSql migration op is not supported: {sql}"
222                )));
223            }
224        }
225    }
226    Ok(())
227}
228
229fn alter_column(
230    core: &CoreDatabase,
231    schema: &KitSchema,
232    table_name: &str,
233    column_name: &str,
234) -> Result<()> {
235    let table = schema
236        .table(table_name)
237        .ok_or_else(|| KitError::Migration(format!("table {table_name:?} not found")))?;
238
239    let handle = core.table(table_name).map_err(KitError::from)?;
240    let guard = handle.lock();
241    let current_columns = guard.schema().columns.clone();
242    drop(guard);
243
244    let target = match table.column(column_name) {
245        Some(col) => col,
246        None => {
247            let current = current_columns
248                .iter()
249                .find(|col| col.name == column_name)
250                .ok_or_else(|| {
251                    KitError::Migration(format!(
252                        "column {table_name}.{column_name} not found in current or target schema"
253                    ))
254                })?;
255            table
256                .columns
257                .iter()
258                .find(|col| col.id as u16 == current.id)
259                .ok_or_else(|| {
260                    KitError::Migration(format!(
261                        "target column for {table_name}.{column_name} with id {} not found",
262                        current.id
263                    ))
264                })?
265        }
266    };
267
268    let source_name = current_columns
269        .iter()
270        .find(|col| col.id == target.id as u16)
271        .map(|col| col.name.as_str())
272        .unwrap_or(column_name);
273
274    core.alter_column(
275        table_name,
276        source_name,
277        AlterColumn {
278            name: Some(target.name.clone()),
279            ty: Some(crate::schema::to_core_type(target.storage_type)),
280            flags: Some(crate::schema::to_core_flags(table, target)),
281        },
282    )
283    .map_err(KitError::from)?;
284
285    Ok(())
286}
287
288/// All visible rows of an internal `__kit_*` table as raw core rows.
289fn visible_internal_rows(core: &CoreDatabase, name: &str) -> Result<Vec<CoreRow>> {
290    let handle = core.table(name).map_err(KitError::from)?;
291    let guard = handle.lock();
292    let snapshot = guard.snapshot();
293    guard.visible_rows(snapshot).map_err(KitError::from)
294}
295
296/// All visible rows of an application table as kit JSON rows.
297fn visible_app_rows(core: &CoreDatabase, table: &KitTable) -> Result<Vec<KitRow>> {
298    visible_internal_rows(core, &table.name)?
299        .iter()
300        .map(|r| core_row_to_json(r, table))
301        .collect()
302}
303
304fn copy_rows_to_table(
305    core: &CoreDatabase,
306    table_name: &str,
307    table: &KitTable,
308    rows: &[CoreRow],
309) -> Result<()> {
310    let mut txn = core.begin();
311    for row in rows {
312        let cells: Vec<(u16, CoreValue)> = table
313            .columns
314            .iter()
315            .map(|col| {
316                let id = col.id as u16;
317                (id, row.columns.get(&id).cloned().unwrap_or(CoreValue::Null))
318            })
319            .collect();
320        txn.put(table_name, cells).map_err(KitError::from)?;
321    }
322    txn.commit().map_err(KitError::from)?;
323    Ok(())
324}
325
326fn temp_rebuild_name(core: &CoreDatabase, table_name: &str) -> String {
327    for attempt in 0.. {
328        let name = format!("__kit_tmp_rebuild_{table_name}_{attempt}");
329        if core.table_id(&name).is_err() {
330            return name;
331        }
332    }
333    unreachable!("unbounded rebuild temp name search must return")
334}
335
336fn rebuild_table(core: &CoreDatabase, target: &KitTable) -> Result<()> {
337    let rows = visible_internal_rows(core, &target.name)?;
338    let target_schema = to_core_schema(target);
339    let temp_name = temp_rebuild_name(core, &target.name);
340
341    core.create_table(&temp_name, target_schema.clone())
342        .map_err(KitError::from)?;
343    let result = (|| -> Result<()> {
344        copy_rows_to_table(core, &temp_name, target, &rows)?;
345        core.drop_table(&target.name).map_err(KitError::from)?;
346        core.create_table(&target.name, target_schema)
347            .map_err(KitError::from)?;
348        copy_rows_to_table(core, &target.name, target, &rows)?;
349        Ok(())
350    })();
351
352    let cleanup = core.drop_table(&temp_name).map_err(KitError::from);
353    match (result, cleanup) {
354        (Ok(()), Ok(())) => Ok(()),
355        (Err(err), _) => Err(err),
356        (Ok(()), Err(err)) => Err(err),
357    }
358}
359
360fn drop_stale_unique_guards(core: &CoreDatabase, target: &KitTable) -> Result<()> {
361    let live_constraints: HashSet<&str> = target
362        .unique_constraints
363        .iter()
364        .map(|constraint| constraint.name.as_str())
365        .collect();
366    let existing = visible_internal_rows(core, UNIQUE_KEYS)?;
367    let mut txn = core.begin();
368    for guard in &existing {
369        let guard_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
370        if guard_table != target.name {
371            continue;
372        }
373        let guard_constraint = internal_bytes(guard, cols::UQ_CONSTRAINT).unwrap_or_default();
374        if !live_constraints.contains(guard_constraint.as_str()) {
375            txn.delete(UNIQUE_KEYS, guard.row_id)
376                .map_err(KitError::from)?;
377        }
378    }
379    txn.commit().map_err(KitError::from)?;
380    Ok(())
381}
382
383/// Backfill `__kit_unique_keys` guards for an added unique constraint (PLAN
384/// "Migrations"). Rows whose unique columns are all non-null reserve a guard;
385/// two rows producing the same key means existing data already violates the
386/// constraint, which rejects the migration. Existing guards are left untouched,
387/// so re-running is idempotent.
388fn backfill_unique(
389    core: &CoreDatabase,
390    schema: &KitSchema,
391    table_name: &str,
392    constraint: &str,
393) -> Result<()> {
394    let table = schema.table(table_name).ok_or_else(|| {
395        KitError::Migration(format!(
396            "add_unique: table {table_name} not found in schema"
397        ))
398    })?;
399    let uq = table
400        .unique_constraints
401        .iter()
402        .find(|u| u.name == constraint)
403        .ok_or_else(|| {
404            KitError::Migration(format!(
405                "add_unique: unique constraint {constraint} not found on table {table_name}"
406            ))
407        })?;
408
409    let rows = visible_app_rows(core, table)?;
410    let mut seen: HashMap<String, String> = HashMap::new();
411    let mut to_insert: Vec<(String, String)> = Vec::new();
412    for row in &rows {
413        let Some(key) = unique_key(table, uq, &row.values) else {
414            continue;
415        };
416        let owner_pk = encoded_pk_for(table, &row.values);
417        match seen.get(&key) {
418            Some(existing) if existing != &owner_pk => {
419                return Err(KitError::Migration(format!(
420                    "cannot add unique constraint {constraint} on {table_name}: \
421                     existing rows violate it"
422                )));
423            }
424            Some(_) => {}
425            None => {
426                seen.insert(key.clone(), owner_pk.clone());
427                to_insert.push((key, owner_pk));
428            }
429        }
430    }
431
432    let existing_keys: HashSet<String> = visible_internal_rows(core, UNIQUE_KEYS)?
433        .iter()
434        .filter_map(|g| internal_bytes(g, cols::UQ_ENCODED))
435        .collect();
436
437    let now = iso_now();
438    let mut txn = core.begin();
439    for (key, owner_pk) in to_insert {
440        if existing_keys.contains(&key) {
441            continue;
442        }
443        txn.put(
444            UNIQUE_KEYS,
445            vec![
446                (cols::UQ_ENCODED, CoreValue::Bytes(key.into_bytes())),
447                (
448                    cols::UQ_CONSTRAINT,
449                    CoreValue::Bytes(constraint.as_bytes().to_vec()),
450                ),
451                (
452                    cols::UQ_OWNER_TABLE,
453                    CoreValue::Bytes(table_name.as_bytes().to_vec()),
454                ),
455                (cols::UQ_OWNER_PK, CoreValue::Bytes(owner_pk.into_bytes())),
456                (cols::UQ_CREATED, CoreValue::Bytes(now.clone().into_bytes())),
457            ],
458        )
459        .map_err(KitError::from)?;
460    }
461    txn.commit().map_err(KitError::from)?;
462    Ok(())
463}
464
465/// Delete every `__kit_unique_keys` guard for a dropped unique constraint.
466fn drop_unique_guards(core: &CoreDatabase, table_name: &str, constraint: &str) -> Result<()> {
467    let existing = visible_internal_rows(core, UNIQUE_KEYS)?;
468    let mut txn = core.begin();
469    for g in &existing {
470        let g_table = internal_bytes(g, cols::UQ_OWNER_TABLE).unwrap_or_default();
471        let g_constraint = internal_bytes(g, cols::UQ_CONSTRAINT).unwrap_or_default();
472        if g_table == table_name && g_constraint == constraint {
473            txn.delete(UNIQUE_KEYS, g.row_id).map_err(KitError::from)?;
474        }
475    }
476    txn.commit().map_err(KitError::from)?;
477    Ok(())
478}
479
480/// Backfill parent `__kit_row_guards` for an added foreign key (PLAN
481/// "Migrations"). Every existing child row with a non-null FK must reference an
482/// existing parent; a missing parent rejects the migration. The referenced
483/// parent's row guard is touched so a later concurrent parent delete conflicts.
484fn backfill_foreign_key(
485    core: &CoreDatabase,
486    schema: &KitSchema,
487    table_name: &str,
488    constraint: &str,
489) -> Result<()> {
490    let table = schema.table(table_name).ok_or_else(|| {
491        KitError::Migration(format!(
492            "add_foreign_key: table {table_name} not found in schema"
493        ))
494    })?;
495    let fk = table
496        .foreign_keys
497        .iter()
498        .find(|f| f.name == constraint)
499        .ok_or_else(|| {
500            KitError::Migration(format!(
501                "add_foreign_key: foreign key {constraint} not found on table {table_name}"
502            ))
503        })?;
504    let parent = schema.table(&fk.references_table).ok_or_else(|| {
505        KitError::Migration(format!(
506            "add_foreign_key: referenced table {} not found in schema",
507            fk.references_table
508        ))
509    })?;
510
511    let child_rows = visible_app_rows(core, table)?;
512    let parent_pks: HashSet<String> = visible_app_rows(core, parent)?
513        .iter()
514        .map(|p| encoded_pk_for(parent, &p.values))
515        .collect();
516
517    let mut to_touch: Vec<Vec<KeyComponent>> = Vec::new();
518    let mut seen: HashSet<String> = HashSet::new();
519    for child in &child_rows {
520        if fk_values_null(fk, &child.values) {
521            continue;
522        }
523        let comps = parent_pk_components(&child.values, fk, parent);
524        let encoded = encode_pk(&comps);
525        if !parent_pks.contains(&encoded) {
526            return Err(KitError::ForeignKey(format!(
527                "{} references missing parent {}({})",
528                fk.name, fk.references_table, encoded
529            )));
530        }
531        if seen.insert(encoded) {
532            to_touch.push(comps);
533        }
534    }
535
536    let existing = visible_internal_rows(core, ROW_GUARDS)?;
537    let now = iso_now();
538    let mut txn = core.begin();
539    for comps in &to_touch {
540        let encoded_pk = encode_pk(comps);
541        let guard_key = encode_row_guard_key(&parent.name, &encoded_pk);
542        let mut version = 1i64;
543        for g in &existing {
544            if internal_bytes(g, cols::RG_ENCODED).as_deref() == Some(guard_key.as_str()) {
545                if let Some(CoreValue::Int64(v)) = g.columns.get(&cols::RG_VERSION) {
546                    version = v + 1;
547                }
548                txn.delete(ROW_GUARDS, g.row_id).map_err(KitError::from)?;
549            }
550        }
551        txn.put(
552            ROW_GUARDS,
553            vec![
554                (cols::RG_ENCODED, CoreValue::Bytes(guard_key.into_bytes())),
555                (
556                    cols::RG_TABLE,
557                    CoreValue::Bytes(parent.name.as_bytes().to_vec()),
558                ),
559                (cols::RG_PK, CoreValue::Bytes(encoded_pk.into_bytes())),
560                (cols::RG_VERSION, CoreValue::Int64(version)),
561                (cols::RG_UPDATED, CoreValue::Bytes(now.clone().into_bytes())),
562            ],
563        )
564        .map_err(KitError::from)?;
565    }
566    txn.commit().map_err(KitError::from)?;
567    Ok(())
568}
569
570/// Delete every unique-key and row guard owned by a dropped table.
571fn clean_table_guards(core: &CoreDatabase, table_name: &str) -> Result<()> {
572    let unique = visible_internal_rows(core, UNIQUE_KEYS)?;
573    let guards = visible_internal_rows(core, ROW_GUARDS)?;
574    let mut txn = core.begin();
575    for g in &unique {
576        if internal_bytes(g, cols::UQ_OWNER_TABLE).as_deref() == Some(table_name) {
577            txn.delete(UNIQUE_KEYS, g.row_id).map_err(KitError::from)?;
578        }
579    }
580    for g in &guards {
581        if internal_bytes(g, cols::RG_TABLE).as_deref() == Some(table_name) {
582            txn.delete(ROW_GUARDS, g.row_id).map_err(KitError::from)?;
583        }
584    }
585    txn.commit().map_err(KitError::from)?;
586    Ok(())
587}
588
589fn record_migration(
590    txn: &mut mongreldb_core::txn::Transaction<'_>,
591    migration: &Migration,
592) -> Result<()> {
593    let now = crate::internal::iso_now();
594    let cells = vec![
595        (cols::MIG_VERSION, CoreValue::Int64(migration.version)),
596        (
597            cols::MIG_NAME,
598            CoreValue::Bytes(migration.name.clone().into_bytes()),
599        ),
600        (
601            cols::MIG_CHECKSUM,
602            CoreValue::Bytes(migration.checksum().into_bytes()),
603        ),
604        (cols::MIG_APPLIED, CoreValue::Bytes(now.into_bytes())),
605        (
606            cols::MIG_KIT_VERSION,
607            CoreValue::Bytes(env!("CARGO_PKG_VERSION").as_bytes().to_vec()),
608        ),
609        (cols::MIG_STATUS, CoreValue::Bytes(b"applied".to_vec())),
610    ];
611    txn.put(MIGRATIONS_TABLE, cells).map_err(KitError::from)?;
612    Ok(())
613}