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