1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use super::*;
8use crate::application::migration_collections as mc;
9use crate::application::migration_graph;
10use crate::application::migration_inference;
11use crate::application::vcs::{Author, CreateCommitInput};
12use crate::storage::query::ast::{
13 ApplyMigrationQuery, ApplyMigrationTarget, CreateMigrationQuery, ExplainMigrationQuery,
14 RollbackMigrationQuery,
15};
16use crate::storage::unified::entity::{EntityData, EntityId, EntityKind, RowData, UnifiedEntity};
17
18fn now_ms() -> i64 {
19 SystemTime::now()
20 .duration_since(UNIX_EPOCH)
21 .map(|d| d.as_millis() as i64)
22 .unwrap_or(0)
23}
24
25fn val_text(v: &Value) -> Option<&str> {
26 if let Value::Text(s) = v {
27 Some(s.as_ref())
28 } else {
29 None
30 }
31}
32
33fn val_bool(v: &Value) -> Option<bool> {
34 if let Value::Boolean(b) = v {
35 Some(*b)
36 } else {
37 None
38 }
39}
40
41fn migration_author(rt: &RedDBRuntime) -> Author {
42 let store = rt.inner.db.store();
43 let name = store
44 .get_config("red.vcs.author.name")
45 .and_then(|v| {
46 if let Value::Text(s) = v {
47 Some(s.to_string())
48 } else {
49 None
50 }
51 })
52 .unwrap_or_else(|| "reddb".to_string());
53 let email = store
54 .get_config("red.vcs.author.email")
55 .and_then(|v| {
56 if let Value::Text(s) = v {
57 Some(s.to_string())
58 } else {
59 None
60 }
61 })
62 .unwrap_or_else(|| "reddb@localhost".to_string());
63 Author { name, email }
64}
65
66fn insert_meta_row(
67 store: &UnifiedStore,
68 collection: &str,
69 fields: HashMap<String, Value>,
70) -> RedDBResult<EntityId> {
71 let _ = store.get_or_create_collection(collection);
72 store
73 .insert_auto(
74 collection,
75 UnifiedEntity::new(
76 EntityId::new(0),
77 EntityKind::TableRow {
78 table: Arc::from(collection),
79 row_id: 0,
80 },
81 EntityData::Row(RowData {
82 columns: Vec::new(),
83 named: Some(fields),
84 schema: None,
85 }),
86 ),
87 )
88 .map_err(|e| RedDBError::Internal(e.to_string()))
89}
90
91fn find_migration(
93 store: &UnifiedStore,
94 name: &str,
95) -> Option<(UnifiedEntity, HashMap<String, Value>)> {
96 let manager = store.get_collection(mc::MIGRATIONS)?;
97 let results = manager.query_all(|entity| {
98 if let EntityData::Row(ref row) = entity.data {
99 if let Some(ref named) = row.named {
100 return named.get("name").and_then(|v| val_text(v)) == Some(name);
101 }
102 }
103 false
104 });
105 results.into_iter().find_map(|entity| {
106 if let EntityData::Row(ref row) = entity.data {
107 if let Some(ref named) = row.named {
108 return Some((entity.clone(), named.clone()));
109 }
110 }
111 None
112 })
113}
114
115fn update_migration_field(
117 store: &UnifiedStore,
118 name: &str,
119 key: &str,
120 value: Value,
121) -> RedDBResult<()> {
122 let manager = store
123 .get_collection(mc::MIGRATIONS)
124 .ok_or_else(|| RedDBError::Internal("red_migrations collection not found".to_string()))?;
125 let results = manager.query_all(|entity| {
126 if let EntityData::Row(ref row) = entity.data {
127 if let Some(ref named) = row.named {
128 return named.get("name").and_then(|v| val_text(v)) == Some(name);
129 }
130 }
131 false
132 });
133 for mut entity in results {
134 if let EntityData::Row(ref mut row) = entity.data {
135 if let Some(ref mut named) = row.named {
136 named.insert(key.to_string(), value.clone());
137 manager
138 .update(entity)
139 .map_err(|e| RedDBError::Internal(e.to_string()))?;
140 return Ok(());
141 }
142 }
143 }
144 Err(RedDBError::NotFound(format!(
145 "migration '{name}' not found"
146 )))
147}
148
149fn load_all_edges(store: &UnifiedStore) -> Vec<(String, String)> {
151 let Some(manager) = store.get_collection(mc::MIGRATION_DEPS) else {
152 return Vec::new();
153 };
154 manager
155 .query_all(|_| true)
156 .into_iter()
157 .filter_map(|entity| {
158 if let EntityData::Row(ref row) = entity.data {
159 if let Some(ref named) = row.named {
160 let from = named
161 .get("migration_id")
162 .and_then(|v| val_text(v))?
163 .to_string();
164 let to = named
165 .get("depends_on_id")
166 .and_then(|v| val_text(v))?
167 .to_string();
168 return Some((from, to));
169 }
170 }
171 None
172 })
173 .collect()
174}
175
176fn load_all_migration_names(store: &UnifiedStore) -> Vec<String> {
178 let Some(manager) = store.get_collection(mc::MIGRATIONS) else {
179 return Vec::new();
180 };
181 manager
182 .query_all(|_| true)
183 .into_iter()
184 .filter_map(|entity| {
185 if let EntityData::Row(ref row) = entity.data {
186 if let Some(ref named) = row.named {
187 return named
188 .get("name")
189 .and_then(|v| val_text(v))
190 .map(|s| s.to_string());
191 }
192 }
193 None
194 })
195 .collect()
196}
197
198impl RedDBRuntime {
199 pub fn execute_create_migration(
201 &self,
202 raw_query: &str,
203 q: &CreateMigrationQuery,
204 ) -> RedDBResult<RuntimeQueryResult> {
205 let store_arc = self.inner.db.store();
206 let store: &UnifiedStore = &store_arc;
207
208 if find_migration(store, &q.name).is_some() {
209 return Err(RedDBError::Query(format!(
210 "migration '{}' already exists",
211 q.name
212 )));
213 }
214
215 for dep in &q.depends_on {
216 if find_migration(store, dep).is_none() {
217 return Err(RedDBError::Query(format!(
218 "migration '{dep}' referenced in DEPENDS ON does not exist"
219 )));
220 }
221 }
222
223 let existing_edges = load_all_edges(store);
225 for dep in &q.depends_on {
226 if migration_graph::would_create_cycle(&existing_edges, q.name.as_str(), dep) {
227 return Err(RedDBError::Query(format!(
228 "adding DEPENDS ON '{dep}' to migration '{}' would create a dependency cycle",
229 q.name
230 )));
231 }
232 }
233
234 let mut fields: HashMap<String, Value> = HashMap::new();
235 fields.insert("name".to_string(), Value::text(q.name.as_str()));
236 fields.insert("status".to_string(), Value::text("pending"));
237 fields.insert(
238 "kind".to_string(),
239 Value::text(if q.batch_size.is_some() {
240 "data"
241 } else {
242 "schema"
243 }),
244 );
245 fields.insert("body".to_string(), Value::text(q.body.as_str()));
246 fields.insert(
247 "author".to_string(),
248 Value::text(migration_author(self).name.as_str()),
249 );
250 fields.insert("created_at".to_string(), Value::TimestampMs(now_ms()));
251 fields.insert("applied_at".to_string(), Value::Null);
252 fields.insert("rows_total".to_string(), Value::Null);
253 fields.insert("rows_processed".to_string(), Value::UnsignedInteger(0));
254 fields.insert("vcs_commit_hash".to_string(), Value::Null);
255 fields.insert("no_rollback".to_string(), Value::Boolean(q.no_rollback));
256 fields.insert(
257 "batch_size".to_string(),
258 q.batch_size
259 .map(Value::UnsignedInteger)
260 .unwrap_or(Value::Null),
261 );
262 insert_meta_row(store, mc::MIGRATIONS, fields)?;
263
264 for dep in &q.depends_on {
265 let mut dep_fields: HashMap<String, Value> = HashMap::new();
266 dep_fields.insert("migration_id".to_string(), Value::text(q.name.as_str()));
267 dep_fields.insert("depends_on_id".to_string(), Value::text(dep.as_str()));
268 dep_fields.insert("inferred".to_string(), Value::Boolean(false));
269 insert_meta_row(store, mc::MIGRATION_DEPS, dep_fields)?;
270 }
271
272 let existing_migrations: Vec<(String, String)> = store
274 .get_collection(mc::MIGRATIONS)
275 .map(|manager| {
276 manager
277 .query_all(|_| true)
278 .into_iter()
279 .filter_map(|entity| {
280 if let EntityData::Row(ref row) = entity.data {
281 if let Some(ref named) = row.named {
282 let name = named.get("name").and_then(|v| val_text(v))?.to_string();
283 let body = named.get("body").and_then(|v| val_text(v))?.to_string();
284 return Some((name, body));
285 }
286 }
287 None
288 })
289 .collect()
290 })
291 .unwrap_or_default();
292 let explicit_deps: std::collections::HashSet<String> =
293 q.depends_on.iter().cloned().collect();
294 let inferred_edges = migration_inference::infer_dependencies(
295 q.name.as_str(),
296 q.body.as_str(),
297 &existing_migrations,
298 );
299 for (_, dep) in inferred_edges {
300 if explicit_deps.contains(&dep) {
301 continue; }
303 let current_edges = load_all_edges(store);
305 if !migration_graph::would_create_cycle(¤t_edges, q.name.as_str(), &dep) {
306 let mut dep_fields: HashMap<String, Value> = HashMap::new();
307 dep_fields.insert("migration_id".to_string(), Value::text(q.name.as_str()));
308 dep_fields.insert("depends_on_id".to_string(), Value::text(dep.as_str()));
309 dep_fields.insert("inferred".to_string(), Value::Boolean(true));
310 let _ = insert_meta_row(store, mc::MIGRATION_DEPS, dep_fields);
311 }
312 }
313
314 Ok(RuntimeQueryResult::ok_message(
315 raw_query.to_string(),
316 &format!("migration '{}' registered (pending)", q.name),
317 "create_migration",
318 ))
319 }
320
321 pub fn execute_apply_migration(
323 &self,
324 raw_query: &str,
325 q: &ApplyMigrationQuery,
326 ) -> RedDBResult<RuntimeQueryResult> {
327 if let Some(tenant) = &q.for_tenant {
329 if tenant == "*" {
330 return self.apply_migration_all_tenants(raw_query, q);
331 }
332 crate::runtime::impl_core::set_current_tenant(tenant.clone());
334 }
335
336 let result = match &q.target {
337 ApplyMigrationTarget::Named(name) => self.apply_single_migration(raw_query, name),
338 ApplyMigrationTarget::All => self.apply_all_pending(raw_query),
339 };
340
341 if q.for_tenant.is_some() {
343 crate::runtime::impl_core::clear_current_tenant();
344 }
345
346 result
347 }
348
349 fn apply_all_pending(&self, raw_query: &str) -> RedDBResult<RuntimeQueryResult> {
350 let store_arc = self.inner.db.store();
351 let store: &UnifiedStore = &store_arc;
352 let pending = self.collect_pending_migrations(store);
353 if pending.is_empty() {
354 return Ok(RuntimeQueryResult::ok_message(
355 raw_query.to_string(),
356 "no pending migrations",
357 "apply_migration",
358 ));
359 }
360 let mut applied = 0u32;
361 let mut messages: Vec<String> = Vec::new();
362 for name in pending {
363 match self.apply_single_migration(raw_query, &name) {
364 Ok(_) => {
365 applied += 1;
366 messages.push(format!("applied: {name}"));
367 }
368 Err(e) => {
369 messages.push(format!("failed: {name} — {e}"));
370 break;
371 }
372 }
373 }
374 let summary = messages.join("; ");
375 Ok(RuntimeQueryResult::ok_message(
376 raw_query.to_string(),
377 &format!("applied {applied} migration(s): {summary}"),
378 "apply_migration",
379 ))
380 }
381
382 fn apply_migration_all_tenants(
384 &self,
385 raw_query: &str,
386 q: &ApplyMigrationQuery,
387 ) -> RedDBResult<RuntimeQueryResult> {
388 let tenant_ids = self.list_known_tenants();
389 if tenant_ids.is_empty() {
390 return Ok(RuntimeQueryResult::ok_message(
391 raw_query.to_string(),
392 "no tenants found — nothing applied",
393 "apply_migration",
394 ));
395 }
396 let mut results: Vec<String> = Vec::new();
397 for tenant in &tenant_ids {
398 crate::runtime::impl_core::set_current_tenant(tenant.clone());
399 let inner_q = ApplyMigrationQuery {
400 target: q.target.clone(),
401 for_tenant: None,
402 };
403 match self.execute_apply_migration(raw_query, &inner_q) {
404 Ok(r) => results.push(format!(
405 "tenant={tenant}: {}",
406 r.result
407 .records
408 .first()
409 .and_then(|rec| rec.get("message"))
410 .and_then(|v| val_text(v))
411 .unwrap_or("ok")
412 )),
413 Err(e) => results.push(format!("tenant={tenant}: error — {e}")),
414 }
415 crate::runtime::impl_core::clear_current_tenant();
416 }
417 Ok(RuntimeQueryResult::ok_message(
418 raw_query.to_string(),
419 &results.join("; "),
420 "apply_migration",
421 ))
422 }
423
424 fn list_known_tenants(&self) -> Vec<String> {
426 let auth_store = match self.inner.auth_store.read().clone() {
427 Some(s) => s,
428 None => return Vec::new(),
429 };
430 let users = auth_store.list_users_scoped(None);
431 let mut tenants: std::collections::HashSet<String> = std::collections::HashSet::new();
432 for u in users {
433 if let Some(ref t) = u.tenant_id {
434 tenants.insert(t.clone());
435 }
436 }
437 let mut out: Vec<String> = tenants.into_iter().collect();
438 out.sort();
439 out
440 }
441
442 fn collect_pending_migrations(&self, store: &UnifiedStore) -> Vec<String> {
443 let Some(manager) = store.get_collection(mc::MIGRATIONS) else {
445 return Vec::new();
446 };
447 let pending: Vec<String> = manager
448 .query_all(|entity| {
449 if let EntityData::Row(ref row) = entity.data {
450 if let Some(ref named) = row.named {
451 return named.get("status").and_then(|v| val_text(v)) == Some("pending");
452 }
453 }
454 false
455 })
456 .into_iter()
457 .filter_map(|entity| {
458 if let EntityData::Row(ref row) = entity.data {
459 if let Some(ref named) = row.named {
460 return named
461 .get("name")
462 .and_then(|v| val_text(v))
463 .map(|s| s.to_string());
464 }
465 }
466 None
467 })
468 .collect();
469
470 let all_edges = load_all_edges(store);
473 let pending_set: std::collections::HashSet<&str> =
475 pending.iter().map(|s| s.as_str()).collect();
476 let relevant_edges: Vec<(String, String)> = all_edges
477 .into_iter()
478 .filter(|(m, d)| pending_set.contains(m.as_str()) && pending_set.contains(d.as_str()))
479 .collect();
480
481 match migration_graph::topological_sort(&pending, &relevant_edges) {
482 Ok(sorted) => sorted,
483 Err(_) => pending, }
485 }
486
487 fn apply_single_migration(
488 &self,
489 raw_query: &str,
490 name: &str,
491 ) -> RedDBResult<RuntimeQueryResult> {
492 let store_arc = self.inner.db.store();
493 let store: &UnifiedStore = &store_arc;
494
495 let (_, fields) = find_migration(store, name)
496 .ok_or_else(|| RedDBError::NotFound(format!("migration '{name}' not found")))?;
497
498 let status = fields.get("status").and_then(|v| val_text(v)).unwrap_or("");
499
500 if status == "applied" {
501 return Ok(RuntimeQueryResult::ok_message(
502 raw_query.to_string(),
503 &format!("migration '{name}' is already applied"),
504 "apply_migration",
505 ));
506 }
507
508 let deps = self.load_migration_deps(store, name);
510 for dep in &deps {
511 match find_migration(store, dep) {
512 Some((_, dep_fields)) => {
513 let dep_status = dep_fields
514 .get("status")
515 .and_then(|v| val_text(v))
516 .unwrap_or("");
517 if dep_status != "applied" {
518 return Err(RedDBError::Query(format!(
519 "migration '{name}' depends on '{dep}' which is not yet applied"
520 )));
521 }
522 }
523 None => {
524 return Err(RedDBError::Query(format!(
525 "migration '{name}' depends on '{dep}' which does not exist"
526 )));
527 }
528 }
529 }
530
531 let body = fields
532 .get("body")
533 .and_then(|v| val_text(v))
534 .unwrap_or("")
535 .to_string();
536 let batch_size = match fields.get("batch_size") {
537 Some(Value::UnsignedInteger(n)) => Some(*n),
538 _ => None,
539 };
540 let no_rollback = fields
541 .get("no_rollback")
542 .and_then(val_bool)
543 .unwrap_or(false);
544 let rows_processed_start = match fields.get("rows_processed") {
545 Some(Value::UnsignedInteger(n)) => *n,
546 _ => 0,
547 };
548
549 let apply_result = if let Some(batch) = batch_size {
550 self.apply_batched(store, name, &body, batch, rows_processed_start)
551 } else {
552 self.apply_statements(name, &body)
553 };
554
555 match apply_result {
556 Err(e) => {
557 let err_msg = e.to_string();
558 let _ = update_migration_field(store, name, "status", Value::text("failed"));
559 let _ = update_migration_field(store, name, "error", Value::text(err_msg.as_str()));
560 Err(RedDBError::Query(format!(
561 "migration '{name}' failed: {err_msg}"
562 )))
563 }
564 Ok(rows_processed) => {
565 let author = migration_author(self);
566 let commit_hash = match self.vcs_commit(CreateCommitInput {
567 connection_id: 0,
568 message: format!("migration: apply {name}"),
569 author,
570 committer: None,
571 amend: false,
572 allow_empty: true,
573 }) {
574 Ok(commit) => commit.hash,
575 Err(e) => {
576 let err_msg = format!("VCS commit failed: {e}");
577 let _ =
578 update_migration_field(store, name, "status", Value::text("failed"));
579 let _ = update_migration_field(
580 store,
581 name,
582 "error",
583 Value::text(err_msg.as_str()),
584 );
585 return Err(RedDBError::Query(format!(
586 "migration '{name}' applied but {err_msg}"
587 )));
588 }
589 };
590
591 let _ = update_migration_field(store, name, "status", Value::text("applied"));
592 let _ =
593 update_migration_field(store, name, "applied_at", Value::TimestampMs(now_ms()));
594 let _ = update_migration_field(
595 store,
596 name,
597 "vcs_commit_hash",
598 Value::text(commit_hash.as_str()),
599 );
600 if batch_size.is_some() {
601 let _ = update_migration_field(
602 store,
603 name,
604 "rows_processed",
605 Value::UnsignedInteger(rows_processed),
606 );
607 }
608 let msg = if no_rollback {
609 format!(
610 "migration '{name}' applied — {rows_processed} rows (no rollback, commit: {commit_hash})"
611 )
612 } else {
613 format!("migration '{name}' applied (commit: {commit_hash})")
614 };
615 Ok(RuntimeQueryResult::ok_message(
616 raw_query.to_string(),
617 &msg,
618 "apply_migration",
619 ))
620 }
621 }
622 }
623
624 fn apply_statements(&self, name: &str, body: &str) -> RedDBResult<u64> {
627 let statements: Vec<&str> = body
628 .split(';')
629 .map(|s| s.trim())
630 .filter(|s| !s.is_empty())
631 .collect();
632 for stmt in statements {
633 self.execute_query(stmt).map_err(|e| {
634 RedDBError::Query(format!("statement in migration '{name}' failed: {e}"))
635 })?;
636 }
637 Ok(0)
638 }
639
640 fn apply_batched(
645 &self,
646 store: &UnifiedStore,
647 name: &str,
648 body: &str,
649 batch_size: u64,
650 initial_processed: u64,
651 ) -> RedDBResult<u64> {
652 let mut total = initial_processed;
653 loop {
654 let batch_body = format!("{body} LIMIT {batch_size}");
655 let result = self.execute_query(&batch_body).map_err(|e| {
656 RedDBError::Query(format!("batch in migration '{name}' failed: {e}"))
657 })?;
658 let affected = result.affected_rows;
659 total += affected;
660 let _ = update_migration_field(
662 store,
663 name,
664 "rows_processed",
665 Value::UnsignedInteger(total),
666 );
667 if affected < batch_size {
668 break;
669 }
670 }
671 Ok(total)
672 }
673
674 fn load_migration_deps(&self, store: &UnifiedStore, name: &str) -> Vec<String> {
675 let Some(manager) = store.get_collection(mc::MIGRATION_DEPS) else {
676 return Vec::new();
677 };
678 manager
679 .query_all(|entity| {
680 if let EntityData::Row(ref row) = entity.data {
681 if let Some(ref named) = row.named {
682 return named.get("migration_id").and_then(|v| val_text(v)) == Some(name);
683 }
684 }
685 false
686 })
687 .into_iter()
688 .filter_map(|entity| {
689 if let EntityData::Row(ref row) = entity.data {
690 if let Some(ref named) = row.named {
691 return named
692 .get("depends_on_id")
693 .and_then(|v| val_text(v))
694 .map(|s| s.to_string());
695 }
696 }
697 None
698 })
699 .collect()
700 }
701
702 pub fn execute_rollback_migration(
704 &self,
705 raw_query: &str,
706 q: &RollbackMigrationQuery,
707 ) -> RedDBResult<RuntimeQueryResult> {
708 let store_arc = self.inner.db.store();
709 let store: &UnifiedStore = &store_arc;
710
711 let (_, fields) = find_migration(store, &q.name)
712 .ok_or_else(|| RedDBError::NotFound(format!("migration '{}' not found", q.name)))?;
713
714 if fields
715 .get("no_rollback")
716 .and_then(val_bool)
717 .unwrap_or(false)
718 {
719 return Err(RedDBError::Query(format!(
720 "migration '{}' was declared NO ROLLBACK and cannot be rolled back",
721 q.name
722 )));
723 }
724
725 let status = fields.get("status").and_then(|v| val_text(v)).unwrap_or("");
726
727 if status != "applied" {
728 return Err(RedDBError::Query(format!(
729 "migration '{}' has status '{status}' — only applied migrations can be rolled back",
730 q.name
731 )));
732 }
733
734 for (migration, dep) in load_all_edges(store) {
735 if dep != q.name {
736 continue;
737 }
738 let Some((_, dependent_fields)) = find_migration(store, &migration) else {
739 continue;
740 };
741 if dependent_fields.get("status").and_then(|v| val_text(v)) == Some("applied") {
742 return Err(RedDBError::Query(format!(
743 "cannot rollback '{}' - applied migration '{}' depends on it",
744 q.name, migration
745 )));
746 }
747 }
748
749 let commit_hash = fields
750 .get("vcs_commit_hash")
751 .and_then(|v| val_text(v))
752 .unwrap_or("")
753 .to_string();
754
755 if commit_hash.is_empty() {
756 return Err(RedDBError::Query(format!(
757 "migration '{}' has no VCS commit hash to revert",
758 q.name
759 )));
760 }
761
762 let author = migration_author(self);
763 self.vcs_revert(0, &commit_hash, author).map_err(|e| {
764 RedDBError::Query(format!(
765 "rollback of migration '{}' failed: VCS revert failed: {e}",
766 q.name
767 ))
768 })?;
769
770 let _ = update_migration_field(store, &q.name, "status", Value::text("pending"));
771 let _ = update_migration_field(store, &q.name, "applied_at", Value::Null);
772 let _ = update_migration_field(store, &q.name, "vcs_commit_hash", Value::Null);
773
774 Ok(RuntimeQueryResult::ok_message(
775 raw_query.to_string(),
776 &format!("migration '{}' rolled back (status: pending)", q.name),
777 "rollback_migration",
778 ))
779 }
780
781 pub fn execute_explain_migration(
783 &self,
784 raw_query: &str,
785 q: &ExplainMigrationQuery,
786 ) -> RedDBResult<RuntimeQueryResult> {
787 let store_arc = self.inner.db.store();
788 let store: &UnifiedStore = &store_arc;
789
790 let columns = vec![
791 "migration".to_string(),
792 "status".to_string(),
793 "kind".to_string(),
794 "body".to_string(),
795 "estimated_rows".to_string(),
796 "lock_duration_ms".to_string(),
797 ];
798
799 if q.name == "*" {
800 let mut rows = Vec::new();
801 for name in self.collect_pending_migrations(store) {
802 let Some((_, fields)) = find_migration(store, &name) else {
803 continue;
804 };
805 rows.push(explain_migration_row(&name, &fields));
806 }
807 return Ok(RuntimeQueryResult::ok_records(
808 raw_query.to_string(),
809 columns,
810 rows,
811 "explain_migration",
812 ));
813 }
814
815 let (_, fields) = find_migration(store, &q.name)
816 .ok_or_else(|| RedDBError::NotFound(format!("migration '{}' not found", q.name)))?;
817
818 Ok(RuntimeQueryResult::ok_records(
819 raw_query.to_string(),
820 columns,
821 vec![explain_migration_row(&q.name, &fields)],
822 "explain_migration",
823 ))
824 }
825}
826
827fn explain_migration_row(name: &str, fields: &HashMap<String, Value>) -> Vec<(String, Value)> {
828 let status = fields
829 .get("status")
830 .and_then(|v| val_text(v))
831 .unwrap_or("unknown");
832 let body = fields.get("body").and_then(|v| val_text(v)).unwrap_or("");
833 let kind = fields
834 .get("kind")
835 .and_then(|v| val_text(v))
836 .unwrap_or("schema");
837
838 vec![
839 ("migration".to_string(), Value::text(name)),
840 ("status".to_string(), Value::text(status)),
841 ("kind".to_string(), Value::text(kind)),
842 ("body".to_string(), Value::text(body)),
843 ("estimated_rows".to_string(), Value::Null),
844 ("lock_duration_ms".to_string(), Value::UnsignedInteger(0)),
845 ]
846}