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