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(db, 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    db: &crate::db::Database,
93    migration: &Migration,
94    schema: &KitSchema,
95) -> Result<()> {
96    let core = db.core_db();
97    for op in &migration.ops {
98        match op {
99            MigrationOp::CreateTable { name } => {
100                if let Some(table) = schema.table(name) {
101                    if core.table_id(name).is_err() {
102                        core.create_table(name, to_core_schema(table))
103                            .map_err(KitError::from)?;
104                    }
105                }
106            }
107            MigrationOp::DropTable { name } => {
108                // Drop is best-effort (idempotent), then forget the table's
109                // unique-key and row guards.
110                let _ = core.drop_table(name);
111                clean_table_guards(core, name)?;
112            }
113            MigrationOp::AddColumn { table, column } => {
114                if let Some(t) = schema.table(table) {
115                    if let Some(col) = t.column(column) {
116                        let handle = core.table(table).map_err(KitError::from)?;
117                        let mut guard = handle.lock();
118                        guard
119                            .add_column(
120                                column,
121                                crate::schema::to_core_type(col.storage_type),
122                                crate::schema::to_core_flags(t, col),
123                                None,
124                            )
125                            .map_err(KitError::from)?;
126                    }
127                }
128            }
129            MigrationOp::AddUnique { table, constraint } => {
130                backfill_unique(core, schema, table, constraint)?;
131            }
132            MigrationOp::DropUnique { table, constraint } => {
133                drop_unique_guards(core, table, constraint)?;
134            }
135            MigrationOp::AddForeignKey { table, constraint } => {
136                backfill_foreign_key(core, schema, table, constraint)?;
137            }
138            MigrationOp::AlterColumn { table, column } => {
139                alter_column(core, schema, table, column)?;
140            }
141            MigrationOp::AddCheck { .. }
142            | MigrationOp::DropCheck { .. }
143            | MigrationOp::DropForeignKey { .. } => {
144                // Metadata-only: check evaluation, foreign-key enforcement, and
145                // dropped foreign-key definitions are driven by the re-persisted
146                // schema, so there is no catalog or guard mutation to perform
147                // here.
148            }
149            MigrationOp::CreateProcedure { procedure, .. } => {
150                let parsed: mongreldb_core::StoredProcedure =
151                    serde_json::from_value(procedure.json.clone()).map_err(KitError::from)?;
152                let procedure = mongreldb_core::StoredProcedure::new(
153                    parsed.name,
154                    parsed.mode,
155                    parsed.params,
156                    parsed.body,
157                    0,
158                )
159                .map_err(KitError::from)?;
160                core.create_procedure(procedure).map_err(KitError::from)?;
161            }
162            MigrationOp::ReplaceProcedure { name: _, procedure } => {
163                let parsed: mongreldb_core::StoredProcedure =
164                    serde_json::from_value(procedure.json.clone()).map_err(KitError::from)?;
165                let procedure = mongreldb_core::StoredProcedure::new(
166                    parsed.name,
167                    parsed.mode,
168                    parsed.params,
169                    parsed.body,
170                    0,
171                )
172                .map_err(KitError::from)?;
173                core.create_or_replace_procedure(procedure)
174                    .map_err(KitError::from)?;
175            }
176            MigrationOp::DropProcedure { name } => {
177                let _ = core.drop_procedure(name);
178            }
179            MigrationOp::CreateTrigger { trigger, .. } => {
180                core.create_trigger(core_trigger(trigger)?)
181                    .map_err(KitError::from)?;
182            }
183            MigrationOp::ReplaceTrigger { trigger, .. } => {
184                core.create_or_replace_trigger(core_trigger(trigger)?)
185                    .map_err(KitError::from)?;
186            }
187            MigrationOp::DropTrigger { name } => {
188                let _ = core.drop_trigger(name);
189            }
190            MigrationOp::CreateVirtualTable { table } => {
191                // SQL-backed: run `CREATE VIRTUAL TABLE ...` through the
192                // embedded session, then refresh so the new table is visible
193                // to subsequent `sql()` calls.
194                db.sql(&table.create_sql())?;
195                db.refresh_sql_session()?;
196            }
197            MigrationOp::DropVirtualTable { name } => {
198                db.sql(&format!("DROP TABLE IF EXISTS {name}"))?;
199                db.refresh_sql_session()?;
200            }
201            MigrationOp::CreateView { view, .. } => {
202                // The engine's `CREATE VIEW` overwrites any existing entry, so
203                // create and replace are the same SQL (see `ReplaceView`).
204                db.sql(&view.create_sql())?;
205            }
206            MigrationOp::ReplaceView { view, .. } => {
207                db.sql(&view.create_sql())?;
208            }
209            MigrationOp::DropView { name } => {
210                // `IF EXISTS` so dropping an already-absent view is a no-op
211                // (idempotent re-applies of the same migration).
212                db.sql(&format!("DROP VIEW IF EXISTS {name}"))?;
213            }
214            MigrationOp::DropColumn { table, column } => {
215                let target = schema.table(table).ok_or_else(|| {
216                    KitError::Migration(format!("drop_column: table {table} not found in schema"))
217                })?;
218                if target.column(column).is_some() {
219                    return Err(KitError::Migration(format!(
220                        "drop_column: target schema still contains {table}.{column}"
221                    )));
222                }
223                rebuild_table(core, target)?;
224                drop_stale_unique_guards(core, target)?;
225            }
226            MigrationOp::AddIndex { table, index } => {
227                let target = schema.table(table).ok_or_else(|| {
228                    KitError::Migration(format!("add_index: table {table} not found in schema"))
229                })?;
230                let idx = target
231                    .indexes
232                    .iter()
233                    .find(|idx| idx.name == *index)
234                    .ok_or_else(|| {
235                        KitError::Migration(format!(
236                            "add_index: index {index} not found on table {table} in schema"
237                        ))
238                    })?;
239                if idx.unique {
240                    backfill_unique(core, schema, table, index)?;
241                }
242                rebuild_table(core, target)?;
243            }
244            MigrationOp::DropIndex { table, index } => {
245                let target = schema.table(table).ok_or_else(|| {
246                    KitError::Migration(format!("drop_index: table {table} not found in schema"))
247                })?;
248                if target.indexes.iter().any(|idx| idx.name == *index) {
249                    return Err(KitError::Migration(format!(
250                        "drop_index: target schema still contains index {index} on {table}"
251                    )));
252                }
253                rebuild_table(core, target)?;
254                drop_stale_unique_guards(core, target)?;
255            }
256            MigrationOp::RawSql(sql) => {
257                // Run arbitrary SQL (DDL or DML) through the embedded session.
258                // Useful for engine features without a dedicated migration op.
259                // Schema-affecting statements should be followed by
260                // `refresh_sql_session` if later ops in this migration query
261                // the new structure via SQL.
262                db.sql(sql)?;
263            }
264        }
265    }
266    Ok(())
267}
268
269fn core_trigger(spec: &mongreldb_kit_core::TriggerSpec) -> Result<mongreldb_core::StoredTrigger> {
270    let parsed: mongreldb_core::StoredTrigger =
271        serde_json::from_value(spec.json.clone()).map_err(KitError::from)?;
272    mongreldb_core::StoredTrigger::new(
273        parsed.name,
274        mongreldb_core::TriggerDefinition {
275            target: parsed.target,
276            timing: parsed.timing,
277            event: parsed.event,
278            update_of: parsed.update_of,
279            target_columns: parsed.target_columns,
280            when: parsed.when,
281            program: parsed.program,
282        },
283        0,
284    )
285    .map_err(KitError::from)
286}
287
288fn alter_column(
289    core: &CoreDatabase,
290    schema: &KitSchema,
291    table_name: &str,
292    column_name: &str,
293) -> Result<()> {
294    let table = schema
295        .table(table_name)
296        .ok_or_else(|| KitError::Migration(format!("table {table_name:?} not found")))?;
297
298    let handle = core.table(table_name).map_err(KitError::from)?;
299    let guard = handle.lock();
300    let current_columns = guard.schema().columns.clone();
301    drop(guard);
302
303    let target = match table.column(column_name) {
304        Some(col) => col,
305        None => {
306            let current = current_columns
307                .iter()
308                .find(|col| col.name == column_name)
309                .ok_or_else(|| {
310                    KitError::Migration(format!(
311                        "column {table_name}.{column_name} not found in current or target schema"
312                    ))
313                })?;
314            table
315                .columns
316                .iter()
317                .find(|col| col.id as u16 == current.id)
318                .ok_or_else(|| {
319                    KitError::Migration(format!(
320                        "target column for {table_name}.{column_name} with id {} not found",
321                        current.id
322                    ))
323                })?
324        }
325    };
326
327    let source_name = current_columns
328        .iter()
329        .find(|col| col.id == target.id as u16)
330        .map(|col| col.name.as_str())
331        .unwrap_or(column_name);
332
333    core.alter_column(
334        table_name,
335        source_name,
336        AlterColumn {
337            name: Some(target.name.clone()),
338            ty: Some(crate::schema::to_core_type(target.storage_type)),
339            flags: Some(crate::schema::to_core_flags(table, target)),
340            default_value: None,
341        },
342    )
343    .map_err(KitError::from)?;
344
345    Ok(())
346}
347
348/// All visible rows of an internal `__kit_*` table as raw core rows.
349fn visible_internal_rows(core: &CoreDatabase, name: &str) -> Result<Vec<CoreRow>> {
350    let handle = core.table(name).map_err(KitError::from)?;
351    let guard = handle.lock();
352    let snapshot = guard.snapshot();
353    guard.visible_rows(snapshot).map_err(KitError::from)
354}
355
356/// All visible rows of an application table as kit JSON rows.
357fn visible_app_rows(core: &CoreDatabase, table: &KitTable) -> Result<Vec<KitRow>> {
358    visible_internal_rows(core, &table.name)?
359        .iter()
360        .map(|r| core_row_to_json(r, table))
361        .collect()
362}
363
364fn copy_rows_to_table(
365    core: &CoreDatabase,
366    table_name: &str,
367    table: &KitTable,
368    rows: &[CoreRow],
369) -> Result<()> {
370    let mut txn = core.begin();
371    for row in rows {
372        let cells: Vec<(u16, CoreValue)> = table
373            .columns
374            .iter()
375            .map(|col| {
376                let id = col.id as u16;
377                (id, row.columns.get(&id).cloned().unwrap_or(CoreValue::Null))
378            })
379            .collect();
380        txn.put(table_name, cells).map_err(KitError::from)?;
381    }
382    txn.commit().map_err(KitError::from)?;
383    Ok(())
384}
385
386fn temp_rebuild_name(core: &CoreDatabase, table_name: &str) -> String {
387    for attempt in 0.. {
388        let name = format!("__kit_tmp_rebuild_{table_name}_{attempt}");
389        if core.table_id(&name).is_err() {
390            return name;
391        }
392    }
393    unreachable!("unbounded rebuild temp name search must return")
394}
395
396fn rebuild_table(core: &CoreDatabase, target: &KitTable) -> Result<()> {
397    let rows = visible_internal_rows(core, &target.name)?;
398    let target_schema = to_core_schema(target);
399    let temp_name = temp_rebuild_name(core, &target.name);
400
401    core.create_table(&temp_name, target_schema.clone())
402        .map_err(KitError::from)?;
403    let result = (|| -> Result<()> {
404        copy_rows_to_table(core, &temp_name, target, &rows)?;
405        core.drop_table(&target.name).map_err(KitError::from)?;
406        core.create_table(&target.name, target_schema)
407            .map_err(KitError::from)?;
408        copy_rows_to_table(core, &target.name, target, &rows)?;
409        Ok(())
410    })();
411
412    let cleanup = core.drop_table(&temp_name).map_err(KitError::from);
413    match (result, cleanup) {
414        (Ok(()), Ok(())) => Ok(()),
415        (Err(err), _) => Err(err),
416        (Ok(()), Err(err)) => Err(err),
417    }
418}
419
420fn drop_stale_unique_guards(core: &CoreDatabase, target: &KitTable) -> Result<()> {
421    let live_constraints: HashSet<&str> = target
422        .unique_constraints
423        .iter()
424        .map(|constraint| constraint.name.as_str())
425        .collect();
426    let existing = visible_internal_rows(core, UNIQUE_KEYS)?;
427    let mut txn = core.begin();
428    for guard in &existing {
429        let guard_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
430        if guard_table != target.name {
431            continue;
432        }
433        let guard_constraint = internal_bytes(guard, cols::UQ_CONSTRAINT).unwrap_or_default();
434        if !live_constraints.contains(guard_constraint.as_str()) {
435            txn.delete(UNIQUE_KEYS, guard.row_id)
436                .map_err(KitError::from)?;
437        }
438    }
439    txn.commit().map_err(KitError::from)?;
440    Ok(())
441}
442
443/// Backfill `__kit_unique_keys` guards for an added unique constraint (PLAN
444/// "Migrations"). Rows whose unique columns are all non-null reserve a guard;
445/// two rows producing the same key means existing data already violates the
446/// constraint, which rejects the migration. Existing guards are left untouched,
447/// so re-running is idempotent.
448fn backfill_unique(
449    core: &CoreDatabase,
450    schema: &KitSchema,
451    table_name: &str,
452    constraint: &str,
453) -> Result<()> {
454    let table = schema.table(table_name).ok_or_else(|| {
455        KitError::Migration(format!(
456            "add_unique: table {table_name} not found in schema"
457        ))
458    })?;
459    let uq = table
460        .unique_constraints
461        .iter()
462        .find(|u| u.name == constraint)
463        .ok_or_else(|| {
464            KitError::Migration(format!(
465                "add_unique: unique constraint {constraint} not found on table {table_name}"
466            ))
467        })?;
468
469    let rows = visible_app_rows(core, table)?;
470    let mut seen: HashMap<String, String> = HashMap::new();
471    let mut to_insert: Vec<(String, String)> = Vec::new();
472    for row in &rows {
473        let Some(key) = unique_key(table, uq, &row.values) else {
474            continue;
475        };
476        let owner_pk = encoded_pk_for(table, &row.values);
477        match seen.get(&key) {
478            Some(existing) if existing != &owner_pk => {
479                return Err(KitError::Migration(format!(
480                    "cannot add unique constraint {constraint} on {table_name}: \
481                     existing rows violate it"
482                )));
483            }
484            Some(_) => {}
485            None => {
486                seen.insert(key.clone(), owner_pk.clone());
487                to_insert.push((key, owner_pk));
488            }
489        }
490    }
491
492    let existing_keys: HashSet<String> = visible_internal_rows(core, UNIQUE_KEYS)?
493        .iter()
494        .filter_map(|g| internal_bytes(g, cols::UQ_ENCODED))
495        .collect();
496
497    let now = iso_now();
498    let mut txn = core.begin();
499    for (key, owner_pk) in to_insert {
500        if existing_keys.contains(&key) {
501            continue;
502        }
503        txn.put(
504            UNIQUE_KEYS,
505            vec![
506                (cols::UQ_ENCODED, CoreValue::Bytes(key.into_bytes())),
507                (
508                    cols::UQ_CONSTRAINT,
509                    CoreValue::Bytes(constraint.as_bytes().to_vec()),
510                ),
511                (
512                    cols::UQ_OWNER_TABLE,
513                    CoreValue::Bytes(table_name.as_bytes().to_vec()),
514                ),
515                (cols::UQ_OWNER_PK, CoreValue::Bytes(owner_pk.into_bytes())),
516                (cols::UQ_CREATED, CoreValue::Bytes(now.clone().into_bytes())),
517            ],
518        )
519        .map_err(KitError::from)?;
520    }
521    txn.commit().map_err(KitError::from)?;
522    Ok(())
523}
524
525/// Delete every `__kit_unique_keys` guard for a dropped unique constraint.
526fn drop_unique_guards(core: &CoreDatabase, table_name: &str, constraint: &str) -> Result<()> {
527    let existing = visible_internal_rows(core, UNIQUE_KEYS)?;
528    let mut txn = core.begin();
529    for g in &existing {
530        let g_table = internal_bytes(g, cols::UQ_OWNER_TABLE).unwrap_or_default();
531        let g_constraint = internal_bytes(g, cols::UQ_CONSTRAINT).unwrap_or_default();
532        if g_table == table_name && g_constraint == constraint {
533            txn.delete(UNIQUE_KEYS, g.row_id).map_err(KitError::from)?;
534        }
535    }
536    txn.commit().map_err(KitError::from)?;
537    Ok(())
538}
539
540/// Backfill parent `__kit_row_guards` for an added foreign key (PLAN
541/// "Migrations"). Every existing child row with a non-null FK must reference an
542/// existing parent; a missing parent rejects the migration. The referenced
543/// parent's row guard is touched so a later concurrent parent delete conflicts.
544fn backfill_foreign_key(
545    core: &CoreDatabase,
546    schema: &KitSchema,
547    table_name: &str,
548    constraint: &str,
549) -> Result<()> {
550    let table = schema.table(table_name).ok_or_else(|| {
551        KitError::Migration(format!(
552            "add_foreign_key: table {table_name} not found in schema"
553        ))
554    })?;
555    let fk = table
556        .foreign_keys
557        .iter()
558        .find(|f| f.name == constraint)
559        .ok_or_else(|| {
560            KitError::Migration(format!(
561                "add_foreign_key: foreign key {constraint} not found on table {table_name}"
562            ))
563        })?;
564    let parent = schema.table(&fk.references_table).ok_or_else(|| {
565        KitError::Migration(format!(
566            "add_foreign_key: referenced table {} not found in schema",
567            fk.references_table
568        ))
569    })?;
570
571    let child_rows = visible_app_rows(core, table)?;
572    let parent_pks: HashSet<String> = visible_app_rows(core, parent)?
573        .iter()
574        .map(|p| encoded_pk_for(parent, &p.values))
575        .collect();
576
577    let mut to_touch: Vec<Vec<KeyComponent>> = Vec::new();
578    let mut seen: HashSet<String> = HashSet::new();
579    for child in &child_rows {
580        if fk_values_null(fk, &child.values) {
581            continue;
582        }
583        let comps = parent_pk_components(&child.values, fk, parent);
584        let encoded = encode_pk(&comps);
585        if !parent_pks.contains(&encoded) {
586            return Err(KitError::ForeignKey(format!(
587                "{} references missing parent {}({})",
588                fk.name, fk.references_table, encoded
589            )));
590        }
591        if seen.insert(encoded) {
592            to_touch.push(comps);
593        }
594    }
595
596    let existing = visible_internal_rows(core, ROW_GUARDS)?;
597    let now = iso_now();
598    let mut txn = core.begin();
599    for comps in &to_touch {
600        let encoded_pk = encode_pk(comps);
601        let guard_key = encode_row_guard_key(&parent.name, &encoded_pk);
602        let mut version = 1i64;
603        for g in &existing {
604            if internal_bytes(g, cols::RG_ENCODED).as_deref() == Some(guard_key.as_str()) {
605                if let Some(CoreValue::Int64(v)) = g.columns.get(&cols::RG_VERSION) {
606                    version = v + 1;
607                }
608                txn.delete(ROW_GUARDS, g.row_id).map_err(KitError::from)?;
609            }
610        }
611        txn.put(
612            ROW_GUARDS,
613            vec![
614                (cols::RG_ENCODED, CoreValue::Bytes(guard_key.into_bytes())),
615                (
616                    cols::RG_TABLE,
617                    CoreValue::Bytes(parent.name.as_bytes().to_vec()),
618                ),
619                (cols::RG_PK, CoreValue::Bytes(encoded_pk.into_bytes())),
620                (cols::RG_VERSION, CoreValue::Int64(version)),
621                (cols::RG_UPDATED, CoreValue::Bytes(now.clone().into_bytes())),
622            ],
623        )
624        .map_err(KitError::from)?;
625    }
626    txn.commit().map_err(KitError::from)?;
627    Ok(())
628}
629
630/// Delete every unique-key and row guard owned by a dropped table.
631fn clean_table_guards(core: &CoreDatabase, table_name: &str) -> Result<()> {
632    let unique = visible_internal_rows(core, UNIQUE_KEYS)?;
633    let guards = visible_internal_rows(core, ROW_GUARDS)?;
634    let mut txn = core.begin();
635    for g in &unique {
636        if internal_bytes(g, cols::UQ_OWNER_TABLE).as_deref() == Some(table_name) {
637            txn.delete(UNIQUE_KEYS, g.row_id).map_err(KitError::from)?;
638        }
639    }
640    for g in &guards {
641        if internal_bytes(g, cols::RG_TABLE).as_deref() == Some(table_name) {
642            txn.delete(ROW_GUARDS, g.row_id).map_err(KitError::from)?;
643        }
644    }
645    txn.commit().map_err(KitError::from)?;
646    Ok(())
647}
648
649fn record_migration(
650    txn: &mut mongreldb_core::txn::Transaction<'_>,
651    migration: &Migration,
652) -> Result<()> {
653    let now = crate::internal::iso_now();
654    let cells = vec![
655        (cols::MIG_VERSION, CoreValue::Int64(migration.version)),
656        (
657            cols::MIG_NAME,
658            CoreValue::Bytes(migration.name.clone().into_bytes()),
659        ),
660        (
661            cols::MIG_CHECKSUM,
662            CoreValue::Bytes(migration.checksum().into_bytes()),
663        ),
664        (cols::MIG_APPLIED, CoreValue::Bytes(now.into_bytes())),
665        (
666            cols::MIG_KIT_VERSION,
667            CoreValue::Bytes(env!("CARGO_PKG_VERSION").as_bytes().to_vec()),
668        ),
669        (cols::MIG_STATUS, CoreValue::Bytes(b"applied".to_vec())),
670    ];
671    txn.put(MIGRATIONS_TABLE, cells).map_err(KitError::from)?;
672    Ok(())
673}