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 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 }
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 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 db.sql(&view.create_sql())?;
205 }
206 MigrationOp::ReplaceView { view, .. } => {
207 db.sql(&view.create_sql())?;
208 }
209 MigrationOp::DropView { name } => {
210 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 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 embedding_source: target
343 .embedding_source
344 .as_ref()
345 .map(|src| Some(crate::schema::to_core_embedding_source(src))),
346 },
347 )
348 .map_err(KitError::from)?;
349
350 Ok(())
351}
352
353fn visible_internal_rows(core: &CoreDatabase, name: &str) -> Result<Vec<CoreRow>> {
355 let handle = core.table(name).map_err(KitError::from)?;
356 let guard = handle.lock();
357 let snapshot = guard.snapshot();
358 guard.visible_rows(snapshot).map_err(KitError::from)
359}
360
361fn visible_app_rows(core: &CoreDatabase, table: &KitTable) -> Result<Vec<KitRow>> {
363 visible_internal_rows(core, &table.name)?
364 .iter()
365 .map(|r| core_row_to_json(r, table))
366 .collect()
367}
368
369fn copy_rows_to_table(
370 core: &CoreDatabase,
371 table_name: &str,
372 table: &KitTable,
373 rows: &[CoreRow],
374) -> Result<()> {
375 let mut txn = core.begin();
376 for row in rows {
377 let cells: Vec<(u16, CoreValue)> = table
378 .columns
379 .iter()
380 .map(|col| {
381 let id = col.id as u16;
382 (id, row.columns.get(&id).cloned().unwrap_or(CoreValue::Null))
383 })
384 .collect();
385 txn.put(table_name, cells).map_err(KitError::from)?;
386 }
387 txn.commit().map_err(KitError::from)?;
388 Ok(())
389}
390
391fn temp_rebuild_name(core: &CoreDatabase, table_name: &str) -> String {
392 for attempt in 0.. {
393 let name = format!("__kit_tmp_rebuild_{table_name}_{attempt}");
394 if core.table_id(&name).is_err() {
395 return name;
396 }
397 }
398 unreachable!("unbounded rebuild temp name search must return")
399}
400
401fn rebuild_table(core: &CoreDatabase, target: &KitTable) -> Result<()> {
402 let rows = visible_internal_rows(core, &target.name)?;
403 let target_schema = to_core_schema(target)?;
404 let temp_name = temp_rebuild_name(core, &target.name);
405
406 core.create_table(&temp_name, target_schema.clone())
407 .map_err(KitError::from)?;
408 let result = (|| -> Result<()> {
409 copy_rows_to_table(core, &temp_name, target, &rows)?;
410 core.drop_table(&target.name).map_err(KitError::from)?;
411 core.create_table(&target.name, target_schema)
412 .map_err(KitError::from)?;
413 copy_rows_to_table(core, &target.name, target, &rows)?;
414 Ok(())
415 })();
416
417 let cleanup = core.drop_table(&temp_name).map_err(KitError::from);
418 match (result, cleanup) {
419 (Ok(()), Ok(())) => Ok(()),
420 (Err(err), _) => Err(err),
421 (Ok(()), Err(err)) => Err(err),
422 }
423}
424
425fn drop_stale_unique_guards(core: &CoreDatabase, target: &KitTable) -> Result<()> {
426 let live_constraints: HashSet<&str> = target
427 .unique_constraints
428 .iter()
429 .map(|constraint| constraint.name.as_str())
430 .collect();
431 let existing = visible_internal_rows(core, UNIQUE_KEYS)?;
432 let mut txn = core.begin();
433 for guard in &existing {
434 let guard_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
435 if guard_table != target.name {
436 continue;
437 }
438 let guard_constraint = internal_bytes(guard, cols::UQ_CONSTRAINT).unwrap_or_default();
439 if !live_constraints.contains(guard_constraint.as_str()) {
440 txn.delete(UNIQUE_KEYS, guard.row_id)
441 .map_err(KitError::from)?;
442 }
443 }
444 txn.commit().map_err(KitError::from)?;
445 Ok(())
446}
447
448fn backfill_unique(
454 core: &CoreDatabase,
455 schema: &KitSchema,
456 table_name: &str,
457 constraint: &str,
458) -> Result<()> {
459 let table = schema.table(table_name).ok_or_else(|| {
460 KitError::Migration(format!(
461 "add_unique: table {table_name} not found in schema"
462 ))
463 })?;
464 let uq = table
465 .unique_constraints
466 .iter()
467 .find(|u| u.name == constraint)
468 .ok_or_else(|| {
469 KitError::Migration(format!(
470 "add_unique: unique constraint {constraint} not found on table {table_name}"
471 ))
472 })?;
473
474 let rows = visible_app_rows(core, table)?;
475 let mut seen: HashMap<String, String> = HashMap::new();
476 let mut to_insert: Vec<(String, String)> = Vec::new();
477 for row in &rows {
478 let Some(key) = unique_key(table, uq, &row.values) else {
479 continue;
480 };
481 let owner_pk = encoded_pk_for(table, &row.values);
482 match seen.get(&key) {
483 Some(existing) if existing != &owner_pk => {
484 return Err(KitError::Migration(format!(
485 "cannot add unique constraint {constraint} on {table_name}: \
486 existing rows violate it"
487 )));
488 }
489 Some(_) => {}
490 None => {
491 seen.insert(key.clone(), owner_pk.clone());
492 to_insert.push((key, owner_pk));
493 }
494 }
495 }
496
497 let existing_keys: HashSet<String> = visible_internal_rows(core, UNIQUE_KEYS)?
498 .iter()
499 .filter_map(|g| internal_bytes(g, cols::UQ_ENCODED))
500 .collect();
501
502 let now = iso_now();
503 let mut txn = core.begin();
504 for (key, owner_pk) in to_insert {
505 if existing_keys.contains(&key) {
506 continue;
507 }
508 txn.put(
509 UNIQUE_KEYS,
510 vec![
511 (cols::UQ_ENCODED, CoreValue::Bytes(key.into_bytes())),
512 (
513 cols::UQ_CONSTRAINT,
514 CoreValue::Bytes(constraint.as_bytes().to_vec()),
515 ),
516 (
517 cols::UQ_OWNER_TABLE,
518 CoreValue::Bytes(table_name.as_bytes().to_vec()),
519 ),
520 (cols::UQ_OWNER_PK, CoreValue::Bytes(owner_pk.into_bytes())),
521 (cols::UQ_CREATED, CoreValue::Bytes(now.clone().into_bytes())),
522 ],
523 )
524 .map_err(KitError::from)?;
525 }
526 txn.commit().map_err(KitError::from)?;
527 Ok(())
528}
529
530fn drop_unique_guards(core: &CoreDatabase, table_name: &str, constraint: &str) -> Result<()> {
532 let existing = visible_internal_rows(core, UNIQUE_KEYS)?;
533 let mut txn = core.begin();
534 for g in &existing {
535 let g_table = internal_bytes(g, cols::UQ_OWNER_TABLE).unwrap_or_default();
536 let g_constraint = internal_bytes(g, cols::UQ_CONSTRAINT).unwrap_or_default();
537 if g_table == table_name && g_constraint == constraint {
538 txn.delete(UNIQUE_KEYS, g.row_id).map_err(KitError::from)?;
539 }
540 }
541 txn.commit().map_err(KitError::from)?;
542 Ok(())
543}
544
545fn backfill_foreign_key(
550 core: &CoreDatabase,
551 schema: &KitSchema,
552 table_name: &str,
553 constraint: &str,
554) -> Result<()> {
555 let table = schema.table(table_name).ok_or_else(|| {
556 KitError::Migration(format!(
557 "add_foreign_key: table {table_name} not found in schema"
558 ))
559 })?;
560 let fk = table
561 .foreign_keys
562 .iter()
563 .find(|f| f.name == constraint)
564 .ok_or_else(|| {
565 KitError::Migration(format!(
566 "add_foreign_key: foreign key {constraint} not found on table {table_name}"
567 ))
568 })?;
569 let parent = schema.table(&fk.references_table).ok_or_else(|| {
570 KitError::Migration(format!(
571 "add_foreign_key: referenced table {} not found in schema",
572 fk.references_table
573 ))
574 })?;
575
576 let child_rows = visible_app_rows(core, table)?;
577 let parent_pks: HashSet<String> = visible_app_rows(core, parent)?
578 .iter()
579 .map(|p| encoded_pk_for(parent, &p.values))
580 .collect();
581
582 let mut to_touch: Vec<Vec<KeyComponent>> = Vec::new();
583 let mut seen: HashSet<String> = HashSet::new();
584 for child in &child_rows {
585 if fk_values_null(fk, &child.values) {
586 continue;
587 }
588 let comps = parent_pk_components(&child.values, fk, parent);
589 let encoded = encode_pk(&comps);
590 if !parent_pks.contains(&encoded) {
591 return Err(KitError::ForeignKey(format!(
592 "{} references missing parent {}({})",
593 fk.name, fk.references_table, encoded
594 )));
595 }
596 if seen.insert(encoded) {
597 to_touch.push(comps);
598 }
599 }
600
601 let existing = visible_internal_rows(core, ROW_GUARDS)?;
602 let now = iso_now();
603 let mut txn = core.begin();
604 for comps in &to_touch {
605 let encoded_pk = encode_pk(comps);
606 let guard_key = encode_row_guard_key(&parent.name, &encoded_pk);
607 let mut version = 1i64;
608 for g in &existing {
609 if internal_bytes(g, cols::RG_ENCODED).as_deref() == Some(guard_key.as_str()) {
610 if let Some(CoreValue::Int64(v)) = g.columns.get(&cols::RG_VERSION) {
611 version = v + 1;
612 }
613 txn.delete(ROW_GUARDS, g.row_id).map_err(KitError::from)?;
614 }
615 }
616 txn.put(
617 ROW_GUARDS,
618 vec![
619 (cols::RG_ENCODED, CoreValue::Bytes(guard_key.into_bytes())),
620 (
621 cols::RG_TABLE,
622 CoreValue::Bytes(parent.name.as_bytes().to_vec()),
623 ),
624 (cols::RG_PK, CoreValue::Bytes(encoded_pk.into_bytes())),
625 (cols::RG_VERSION, CoreValue::Int64(version)),
626 (cols::RG_UPDATED, CoreValue::Bytes(now.clone().into_bytes())),
627 ],
628 )
629 .map_err(KitError::from)?;
630 }
631 txn.commit().map_err(KitError::from)?;
632 Ok(())
633}
634
635fn clean_table_guards(core: &CoreDatabase, table_name: &str) -> Result<()> {
637 let unique = visible_internal_rows(core, UNIQUE_KEYS)?;
638 let guards = visible_internal_rows(core, ROW_GUARDS)?;
639 let mut txn = core.begin();
640 for g in &unique {
641 if internal_bytes(g, cols::UQ_OWNER_TABLE).as_deref() == Some(table_name) {
642 txn.delete(UNIQUE_KEYS, g.row_id).map_err(KitError::from)?;
643 }
644 }
645 for g in &guards {
646 if internal_bytes(g, cols::RG_TABLE).as_deref() == Some(table_name) {
647 txn.delete(ROW_GUARDS, g.row_id).map_err(KitError::from)?;
648 }
649 }
650 txn.commit().map_err(KitError::from)?;
651 Ok(())
652}
653
654fn record_migration(
655 txn: &mut mongreldb_core::txn::Transaction<'_>,
656 migration: &Migration,
657) -> Result<()> {
658 let now = crate::internal::iso_now();
659 let cells = vec![
660 (cols::MIG_VERSION, CoreValue::Int64(migration.version)),
661 (
662 cols::MIG_NAME,
663 CoreValue::Bytes(migration.name.clone().into_bytes()),
664 ),
665 (
666 cols::MIG_CHECKSUM,
667 CoreValue::Bytes(migration.checksum().into_bytes()),
668 ),
669 (cols::MIG_APPLIED, CoreValue::Bytes(now.into_bytes())),
670 (
671 cols::MIG_KIT_VERSION,
672 CoreValue::Bytes(env!("CARGO_PKG_VERSION").as_bytes().to_vec()),
673 ),
674 (cols::MIG_STATUS, CoreValue::Bytes(b"applied".to_vec())),
675 ];
676 txn.put(MIGRATIONS_TABLE, cells).map_err(KitError::from)?;
677 Ok(())
678}