1use 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
17pub fn migrate(db: &mut crate::db::Database, migrations: &[Migration]) -> Result<()> {
22 let core = db.core_db();
23
24 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 for migration in &pending {
37 apply_migration_ops(db, migration, &db.schema)?;
38 }
39
40 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 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 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 }
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 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 db.sql(&view.create_sql())?;
204 }
205 MigrationOp::ReplaceView { view, .. } => {
206 db.sql(&view.create_sql())?;
207 }
208 MigrationOp::DropView { name } => {
209 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 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
346fn 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
354fn 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
441fn 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
523fn 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
538fn 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
628fn 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}