siderite_migrations/registry.rs
1//! Runtime registry of [`crate::operation::Operation::RunRust`] functions.
2
3use crate::error::MigrationError;
4use siderite_orm::Db;
5use std::collections::{BTreeMap, BTreeSet};
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9
10/// Boxed future returned by a registered Rust operation.
11pub type RustFuture = Pin<Box<dyn Future<Output = Result<(), MigrationError>> + Send>>;
12/// `RunRust` callback. Receives a cheap clone of [`Db`].
13pub type RustOp = Arc<dyn Fn(Db) -> RustFuture + Send + Sync>;
14
15/// Names → forward (and optional backwards) functions.
16#[derive(Clone, Default)]
17pub struct MigrationRegistry {
18 ops: BTreeMap<String, RustOp>,
19 replay_safe: BTreeSet<String>,
20}
21
22impl std::fmt::Debug for MigrationRegistry {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.debug_struct("MigrationRegistry")
25 .field("ops", &self.ops.keys().collect::<Vec<_>>())
26 .field("replay_safe", &self.replay_safe)
27 .finish()
28 }
29}
30
31impl MigrationRegistry {
32 /// Empty registry.
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 /// Register `name`. The same map is used for forwards and backwards;
38 /// a reversible `RunRust` stores both names as separate entries.
39 ///
40 /// Args:
41 /// name: Operation identifier in the migration file.
42 /// f: Callback whose future owns completion of its work.
43 ///
44 /// Returns:
45 /// None after registering with ordinary, conservative retry policy.
46 ///
47 /// Unconfirmed non-transactional execution blocks automatic retry.
48 /// Use `register_replay_safe` only with an established idempotence contract.
49 pub fn register(
50 &mut self,
51 name: impl Into<String>,
52 f: impl Fn(Db) -> RustFuture + Send + Sync + 'static,
53 ) {
54 let name = name.into();
55 self.replay_safe.remove(&name);
56 self.ops.insert(name, Arc::new(f));
57 }
58
59 /// Register a callback whose committed effects can safely be repeated.
60 ///
61 /// Args:
62 /// name: Operation name used by the migration file.
63 /// f: Callback safe to retry after interruption at any await point.
64 ///
65 /// Returns:
66 /// None after registering the callback and its explicit retry policy.
67 ///
68 /// Use only after establishing idempotence for database and external
69 /// effects. Registration permits retry; it does not verify idempotence.
70 /// Ordinary `register` clears this declaration when replacing a name.
71 pub fn register_replay_safe(
72 &mut self,
73 name: impl Into<String>,
74 f: impl Fn(Db) -> RustFuture + Send + Sync + 'static,
75 ) {
76 let name = name.into();
77 self.register(name.clone(), f);
78 self.replay_safe.insert(name);
79 }
80
81 pub(crate) fn contains(&self, name: &str) -> bool {
82 self.ops.contains_key(name)
83 }
84
85 pub(crate) fn is_replay_safe(&self, name: &str) -> bool {
86 self.replay_safe.contains(name)
87 }
88
89 /// Run the function named `name`.
90 ///
91 /// On SQLite the function runs on the schema-change connection with
92 /// `PRAGMA foreign_keys` off (see the migrations guide), so data code
93 /// must stay FK-clean by hand; orphans fail the migration at commit.
94 ///
95 /// # Errors
96 /// [`MigrationError::UnregisteredRust`] or the function's own error.
97 pub async fn run(&self, name: &str, db: &Db) -> Result<(), MigrationError> {
98 let op = self
99 .ops
100 .get(name)
101 .ok_or_else(|| MigrationError::UnregisteredRust(name.to_owned()))?;
102 op(db.clone()).await
103 }
104}