Skip to main content

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}