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(core, 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 core: &CoreDatabase,
93 migration: &Migration,
94 schema: &KitSchema,
95) -> Result<()> {
96 for op in &migration.ops {
97 match op {
98 MigrationOp::CreateTable { name } => {
99 if let Some(table) = schema.table(name) {
100 if core.table_id(name).is_err() {
101 core.create_table(name, to_core_schema(table))
102 .map_err(KitError::from)?;
103 }
104 }
105 }
106 MigrationOp::DropTable { name } => {
107 let _ = core.drop_table(name);
110 clean_table_guards(core, name)?;
111 }
112 MigrationOp::AddColumn { table, column } => {
113 if let Some(t) = schema.table(table) {
114 if let Some(col) = t.column(column) {
115 let handle = core.table(table).map_err(KitError::from)?;
116 let mut guard = handle.lock();
117 guard
118 .add_column(
119 column,
120 crate::schema::to_core_type(col.storage_type),
121 crate::schema::to_core_flags(t, col),
122 )
123 .map_err(KitError::from)?;
124 }
125 }
126 }
127 MigrationOp::AddUnique { table, constraint } => {
128 backfill_unique(core, schema, table, constraint)?;
129 }
130 MigrationOp::DropUnique { table, constraint } => {
131 drop_unique_guards(core, table, constraint)?;
132 }
133 MigrationOp::AddForeignKey { table, constraint } => {
134 backfill_foreign_key(core, schema, table, constraint)?;
135 }
136 MigrationOp::AlterColumn { table, column } => {
137 alter_column(core, schema, table, column)?;
138 }
139 MigrationOp::AddCheck { .. }
140 | MigrationOp::DropCheck { .. }
141 | MigrationOp::DropForeignKey { .. } => {
142 }
147 MigrationOp::CreateProcedure { procedure, .. } => {
148 let parsed: mongreldb_core::StoredProcedure =
149 serde_json::from_value(procedure.json.clone()).map_err(KitError::from)?;
150 let procedure = mongreldb_core::StoredProcedure::new(
151 parsed.name,
152 parsed.mode,
153 parsed.params,
154 parsed.body,
155 0,
156 )
157 .map_err(KitError::from)?;
158 core.create_procedure(procedure).map_err(KitError::from)?;
159 }
160 MigrationOp::ReplaceProcedure { name: _, procedure } => {
161 let parsed: mongreldb_core::StoredProcedure =
162 serde_json::from_value(procedure.json.clone()).map_err(KitError::from)?;
163 let procedure = mongreldb_core::StoredProcedure::new(
164 parsed.name,
165 parsed.mode,
166 parsed.params,
167 parsed.body,
168 0,
169 )
170 .map_err(KitError::from)?;
171 core.create_or_replace_procedure(procedure)
172 .map_err(KitError::from)?;
173 }
174 MigrationOp::DropProcedure { name } => {
175 let _ = core.drop_procedure(name);
176 }
177 MigrationOp::DropColumn { table, column } => {
178 let target = schema.table(table).ok_or_else(|| {
179 KitError::Migration(format!("drop_column: table {table} not found in schema"))
180 })?;
181 if target.column(column).is_some() {
182 return Err(KitError::Migration(format!(
183 "drop_column: target schema still contains {table}.{column}"
184 )));
185 }
186 rebuild_table(core, target)?;
187 drop_stale_unique_guards(core, target)?;
188 }
189 MigrationOp::AddIndex { table, index } => {
190 let target = schema.table(table).ok_or_else(|| {
191 KitError::Migration(format!("add_index: table {table} not found in schema"))
192 })?;
193 let idx = target
194 .indexes
195 .iter()
196 .find(|idx| idx.name == *index)
197 .ok_or_else(|| {
198 KitError::Migration(format!(
199 "add_index: index {index} not found on table {table} in schema"
200 ))
201 })?;
202 if idx.unique {
203 backfill_unique(core, schema, table, index)?;
204 }
205 rebuild_table(core, target)?;
206 }
207 MigrationOp::DropIndex { table, index } => {
208 let target = schema.table(table).ok_or_else(|| {
209 KitError::Migration(format!("drop_index: table {table} not found in schema"))
210 })?;
211 if target.indexes.iter().any(|idx| idx.name == *index) {
212 return Err(KitError::Migration(format!(
213 "drop_index: target schema still contains index {index} on {table}"
214 )));
215 }
216 rebuild_table(core, target)?;
217 drop_stale_unique_guards(core, target)?;
218 }
219 MigrationOp::RawSql(sql) => {
220 return Err(KitError::Migration(format!(
221 "RawSql migration op is not supported: {sql}"
222 )));
223 }
224 }
225 }
226 Ok(())
227}
228
229fn alter_column(
230 core: &CoreDatabase,
231 schema: &KitSchema,
232 table_name: &str,
233 column_name: &str,
234) -> Result<()> {
235 let table = schema
236 .table(table_name)
237 .ok_or_else(|| KitError::Migration(format!("table {table_name:?} not found")))?;
238
239 let handle = core.table(table_name).map_err(KitError::from)?;
240 let guard = handle.lock();
241 let current_columns = guard.schema().columns.clone();
242 drop(guard);
243
244 let target = match table.column(column_name) {
245 Some(col) => col,
246 None => {
247 let current = current_columns
248 .iter()
249 .find(|col| col.name == column_name)
250 .ok_or_else(|| {
251 KitError::Migration(format!(
252 "column {table_name}.{column_name} not found in current or target schema"
253 ))
254 })?;
255 table
256 .columns
257 .iter()
258 .find(|col| col.id as u16 == current.id)
259 .ok_or_else(|| {
260 KitError::Migration(format!(
261 "target column for {table_name}.{column_name} with id {} not found",
262 current.id
263 ))
264 })?
265 }
266 };
267
268 let source_name = current_columns
269 .iter()
270 .find(|col| col.id == target.id as u16)
271 .map(|col| col.name.as_str())
272 .unwrap_or(column_name);
273
274 core.alter_column(
275 table_name,
276 source_name,
277 AlterColumn {
278 name: Some(target.name.clone()),
279 ty: Some(crate::schema::to_core_type(target.storage_type)),
280 flags: Some(crate::schema::to_core_flags(table, target)),
281 },
282 )
283 .map_err(KitError::from)?;
284
285 Ok(())
286}
287
288fn visible_internal_rows(core: &CoreDatabase, name: &str) -> Result<Vec<CoreRow>> {
290 let handle = core.table(name).map_err(KitError::from)?;
291 let guard = handle.lock();
292 let snapshot = guard.snapshot();
293 guard.visible_rows(snapshot).map_err(KitError::from)
294}
295
296fn visible_app_rows(core: &CoreDatabase, table: &KitTable) -> Result<Vec<KitRow>> {
298 visible_internal_rows(core, &table.name)?
299 .iter()
300 .map(|r| core_row_to_json(r, table))
301 .collect()
302}
303
304fn copy_rows_to_table(
305 core: &CoreDatabase,
306 table_name: &str,
307 table: &KitTable,
308 rows: &[CoreRow],
309) -> Result<()> {
310 let mut txn = core.begin();
311 for row in rows {
312 let cells: Vec<(u16, CoreValue)> = table
313 .columns
314 .iter()
315 .map(|col| {
316 let id = col.id as u16;
317 (id, row.columns.get(&id).cloned().unwrap_or(CoreValue::Null))
318 })
319 .collect();
320 txn.put(table_name, cells).map_err(KitError::from)?;
321 }
322 txn.commit().map_err(KitError::from)?;
323 Ok(())
324}
325
326fn temp_rebuild_name(core: &CoreDatabase, table_name: &str) -> String {
327 for attempt in 0.. {
328 let name = format!("__kit_tmp_rebuild_{table_name}_{attempt}");
329 if core.table_id(&name).is_err() {
330 return name;
331 }
332 }
333 unreachable!("unbounded rebuild temp name search must return")
334}
335
336fn rebuild_table(core: &CoreDatabase, target: &KitTable) -> Result<()> {
337 let rows = visible_internal_rows(core, &target.name)?;
338 let target_schema = to_core_schema(target);
339 let temp_name = temp_rebuild_name(core, &target.name);
340
341 core.create_table(&temp_name, target_schema.clone())
342 .map_err(KitError::from)?;
343 let result = (|| -> Result<()> {
344 copy_rows_to_table(core, &temp_name, target, &rows)?;
345 core.drop_table(&target.name).map_err(KitError::from)?;
346 core.create_table(&target.name, target_schema)
347 .map_err(KitError::from)?;
348 copy_rows_to_table(core, &target.name, target, &rows)?;
349 Ok(())
350 })();
351
352 let cleanup = core.drop_table(&temp_name).map_err(KitError::from);
353 match (result, cleanup) {
354 (Ok(()), Ok(())) => Ok(()),
355 (Err(err), _) => Err(err),
356 (Ok(()), Err(err)) => Err(err),
357 }
358}
359
360fn drop_stale_unique_guards(core: &CoreDatabase, target: &KitTable) -> Result<()> {
361 let live_constraints: HashSet<&str> = target
362 .unique_constraints
363 .iter()
364 .map(|constraint| constraint.name.as_str())
365 .collect();
366 let existing = visible_internal_rows(core, UNIQUE_KEYS)?;
367 let mut txn = core.begin();
368 for guard in &existing {
369 let guard_table = internal_bytes(guard, cols::UQ_OWNER_TABLE).unwrap_or_default();
370 if guard_table != target.name {
371 continue;
372 }
373 let guard_constraint = internal_bytes(guard, cols::UQ_CONSTRAINT).unwrap_or_default();
374 if !live_constraints.contains(guard_constraint.as_str()) {
375 txn.delete(UNIQUE_KEYS, guard.row_id)
376 .map_err(KitError::from)?;
377 }
378 }
379 txn.commit().map_err(KitError::from)?;
380 Ok(())
381}
382
383fn backfill_unique(
389 core: &CoreDatabase,
390 schema: &KitSchema,
391 table_name: &str,
392 constraint: &str,
393) -> Result<()> {
394 let table = schema.table(table_name).ok_or_else(|| {
395 KitError::Migration(format!(
396 "add_unique: table {table_name} not found in schema"
397 ))
398 })?;
399 let uq = table
400 .unique_constraints
401 .iter()
402 .find(|u| u.name == constraint)
403 .ok_or_else(|| {
404 KitError::Migration(format!(
405 "add_unique: unique constraint {constraint} not found on table {table_name}"
406 ))
407 })?;
408
409 let rows = visible_app_rows(core, table)?;
410 let mut seen: HashMap<String, String> = HashMap::new();
411 let mut to_insert: Vec<(String, String)> = Vec::new();
412 for row in &rows {
413 let Some(key) = unique_key(table, uq, &row.values) else {
414 continue;
415 };
416 let owner_pk = encoded_pk_for(table, &row.values);
417 match seen.get(&key) {
418 Some(existing) if existing != &owner_pk => {
419 return Err(KitError::Migration(format!(
420 "cannot add unique constraint {constraint} on {table_name}: \
421 existing rows violate it"
422 )));
423 }
424 Some(_) => {}
425 None => {
426 seen.insert(key.clone(), owner_pk.clone());
427 to_insert.push((key, owner_pk));
428 }
429 }
430 }
431
432 let existing_keys: HashSet<String> = visible_internal_rows(core, UNIQUE_KEYS)?
433 .iter()
434 .filter_map(|g| internal_bytes(g, cols::UQ_ENCODED))
435 .collect();
436
437 let now = iso_now();
438 let mut txn = core.begin();
439 for (key, owner_pk) in to_insert {
440 if existing_keys.contains(&key) {
441 continue;
442 }
443 txn.put(
444 UNIQUE_KEYS,
445 vec![
446 (cols::UQ_ENCODED, CoreValue::Bytes(key.into_bytes())),
447 (
448 cols::UQ_CONSTRAINT,
449 CoreValue::Bytes(constraint.as_bytes().to_vec()),
450 ),
451 (
452 cols::UQ_OWNER_TABLE,
453 CoreValue::Bytes(table_name.as_bytes().to_vec()),
454 ),
455 (cols::UQ_OWNER_PK, CoreValue::Bytes(owner_pk.into_bytes())),
456 (cols::UQ_CREATED, CoreValue::Bytes(now.clone().into_bytes())),
457 ],
458 )
459 .map_err(KitError::from)?;
460 }
461 txn.commit().map_err(KitError::from)?;
462 Ok(())
463}
464
465fn drop_unique_guards(core: &CoreDatabase, table_name: &str, constraint: &str) -> Result<()> {
467 let existing = visible_internal_rows(core, UNIQUE_KEYS)?;
468 let mut txn = core.begin();
469 for g in &existing {
470 let g_table = internal_bytes(g, cols::UQ_OWNER_TABLE).unwrap_or_default();
471 let g_constraint = internal_bytes(g, cols::UQ_CONSTRAINT).unwrap_or_default();
472 if g_table == table_name && g_constraint == constraint {
473 txn.delete(UNIQUE_KEYS, g.row_id).map_err(KitError::from)?;
474 }
475 }
476 txn.commit().map_err(KitError::from)?;
477 Ok(())
478}
479
480fn backfill_foreign_key(
485 core: &CoreDatabase,
486 schema: &KitSchema,
487 table_name: &str,
488 constraint: &str,
489) -> Result<()> {
490 let table = schema.table(table_name).ok_or_else(|| {
491 KitError::Migration(format!(
492 "add_foreign_key: table {table_name} not found in schema"
493 ))
494 })?;
495 let fk = table
496 .foreign_keys
497 .iter()
498 .find(|f| f.name == constraint)
499 .ok_or_else(|| {
500 KitError::Migration(format!(
501 "add_foreign_key: foreign key {constraint} not found on table {table_name}"
502 ))
503 })?;
504 let parent = schema.table(&fk.references_table).ok_or_else(|| {
505 KitError::Migration(format!(
506 "add_foreign_key: referenced table {} not found in schema",
507 fk.references_table
508 ))
509 })?;
510
511 let child_rows = visible_app_rows(core, table)?;
512 let parent_pks: HashSet<String> = visible_app_rows(core, parent)?
513 .iter()
514 .map(|p| encoded_pk_for(parent, &p.values))
515 .collect();
516
517 let mut to_touch: Vec<Vec<KeyComponent>> = Vec::new();
518 let mut seen: HashSet<String> = HashSet::new();
519 for child in &child_rows {
520 if fk_values_null(fk, &child.values) {
521 continue;
522 }
523 let comps = parent_pk_components(&child.values, fk, parent);
524 let encoded = encode_pk(&comps);
525 if !parent_pks.contains(&encoded) {
526 return Err(KitError::ForeignKey(format!(
527 "{} references missing parent {}({})",
528 fk.name, fk.references_table, encoded
529 )));
530 }
531 if seen.insert(encoded) {
532 to_touch.push(comps);
533 }
534 }
535
536 let existing = visible_internal_rows(core, ROW_GUARDS)?;
537 let now = iso_now();
538 let mut txn = core.begin();
539 for comps in &to_touch {
540 let encoded_pk = encode_pk(comps);
541 let guard_key = encode_row_guard_key(&parent.name, &encoded_pk);
542 let mut version = 1i64;
543 for g in &existing {
544 if internal_bytes(g, cols::RG_ENCODED).as_deref() == Some(guard_key.as_str()) {
545 if let Some(CoreValue::Int64(v)) = g.columns.get(&cols::RG_VERSION) {
546 version = v + 1;
547 }
548 txn.delete(ROW_GUARDS, g.row_id).map_err(KitError::from)?;
549 }
550 }
551 txn.put(
552 ROW_GUARDS,
553 vec![
554 (cols::RG_ENCODED, CoreValue::Bytes(guard_key.into_bytes())),
555 (
556 cols::RG_TABLE,
557 CoreValue::Bytes(parent.name.as_bytes().to_vec()),
558 ),
559 (cols::RG_PK, CoreValue::Bytes(encoded_pk.into_bytes())),
560 (cols::RG_VERSION, CoreValue::Int64(version)),
561 (cols::RG_UPDATED, CoreValue::Bytes(now.clone().into_bytes())),
562 ],
563 )
564 .map_err(KitError::from)?;
565 }
566 txn.commit().map_err(KitError::from)?;
567 Ok(())
568}
569
570fn clean_table_guards(core: &CoreDatabase, table_name: &str) -> Result<()> {
572 let unique = visible_internal_rows(core, UNIQUE_KEYS)?;
573 let guards = visible_internal_rows(core, ROW_GUARDS)?;
574 let mut txn = core.begin();
575 for g in &unique {
576 if internal_bytes(g, cols::UQ_OWNER_TABLE).as_deref() == Some(table_name) {
577 txn.delete(UNIQUE_KEYS, g.row_id).map_err(KitError::from)?;
578 }
579 }
580 for g in &guards {
581 if internal_bytes(g, cols::RG_TABLE).as_deref() == Some(table_name) {
582 txn.delete(ROW_GUARDS, g.row_id).map_err(KitError::from)?;
583 }
584 }
585 txn.commit().map_err(KitError::from)?;
586 Ok(())
587}
588
589fn record_migration(
590 txn: &mut mongreldb_core::txn::Transaction<'_>,
591 migration: &Migration,
592) -> Result<()> {
593 let now = crate::internal::iso_now();
594 let cells = vec![
595 (cols::MIG_VERSION, CoreValue::Int64(migration.version)),
596 (
597 cols::MIG_NAME,
598 CoreValue::Bytes(migration.name.clone().into_bytes()),
599 ),
600 (
601 cols::MIG_CHECKSUM,
602 CoreValue::Bytes(migration.checksum().into_bytes()),
603 ),
604 (cols::MIG_APPLIED, CoreValue::Bytes(now.into_bytes())),
605 (
606 cols::MIG_KIT_VERSION,
607 CoreValue::Bytes(env!("CARGO_PKG_VERSION").as_bytes().to_vec()),
608 ),
609 (cols::MIG_STATUS, CoreValue::Bytes(b"applied".to_vec())),
610 ];
611 txn.put(MIGRATIONS_TABLE, cells).map_err(KitError::from)?;
612 Ok(())
613}