Skip to main content

rainier_orm/
migrate.rs

1//! Schema migrations — an ordered list of steps applied **idempotently**,
2//! tracked in a `_orm_migrations` table, against one executor or many.
3//!
4//! For sharding this is the piece that creates each table on every shard: build
5//! a [`Migrator`] of the sharded entities and [`run_on_each`](Migrator::run_on_each)
6//! it across the shard executors, and a second of the global entities against
7//! the global database. Migrations are wasm-safe (they only use [`Executor`]),
8//! so the same set runs from a server or inside a Worker.
9//!
10//! ```ignore
11//! use rainier_orm::migrate::Migrator;
12//!
13//! let shard_schema = Migrator::new()
14//!     .create_table::<User>("0001_users")
15//!     .create_table::<Token>("0002_tokens")
16//!     .add(MyCustomStep);          // impl Migration for anything bespoke
17//!
18//! shard_schema.run_on_each(&shard_execs).await?;   // every shard
19//! global_schema.run(&global_exec).await?;          // the directory DB
20//! ```
21
22use crate::{repo, schema, Entity, Executor, Result};
23use core::future::Future;
24use core::marker::PhantomData;
25use core::pin::Pin;
26
27/// A future returned by a migration step — boxed so [`Migration`] stays
28/// object-safe (the runner holds `Box<dyn Migration<X>>`).
29///
30/// # This future is **not** `Send`, and cannot be made so on stable
31///
32/// Everywhere else in this crate `Send`-ness **leaks**: `repo::` and
33/// [`Query`](crate::Query) return unboxed futures, so theirs are `Send`
34/// exactly when the concrete executor's are — no bound declared, and both a
35/// multi-threaded server and a single-threaded Worker are served by one
36/// implementation.
37///
38/// Boxing behind `dyn` destroys that, because `Box<dyn Future>` is `!Send`
39/// whatever was boxed. The bound would have to be written here — and it cannot
40/// be satisfied: [`CreateTable`] implements [`Migration<X>`] for *every*
41/// `X: Executor`, so its `up` would have to produce a `Send` future for every
42/// executor, and whether `X::execute`'s future is `Send` is unknowable in a
43/// generic context. Expressing it needs return-type notation
44/// (`X: Executor<execute(..): Send>`), still unstable.
45///
46/// So [`Migrator::run`] is the one API here that cannot be awaited inside a
47/// `tokio::spawn`ed task. Two ways round it, both fine in practice:
48///
49/// - **Run migrations outside a spawned task** — at boot, on the main future.
50///   This is what most applications do anyway.
51/// - **Render synchronously and execute the strings yourself.**
52///   [`schema::schema_ddl`] and [`ddl::Migration::render`](crate::ddl::Migration::render)
53///   are ordinary synchronous functions returning `Vec<String>`, so a caller
54///   that needs a `Send` future can render first and then await plain
55///   `execute` calls — no boxed future anywhere.
56///
57/// `tests/send_futures.rs` asserts the `Send` property for everything that can
58/// hold it, and documents this exception.
59pub type StepFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
60
61/// Tracking row: one per applied migration name. A plain global entity, so it
62/// lands in whichever single database the migrator runs against.
63#[derive(Debug, Clone, Entity)]
64#[orm(table = "_orm_migrations")]
65struct Applied {
66    #[orm(pk)]
67    name: String,
68}
69
70/// One migration step. The unique `name` orders + dedupes it; `up` performs the
71/// change against an executor. Implement this on a struct for bespoke DDL, or
72/// use [`Migrator::create_table`] for the common "create this entity's table".
73pub trait Migration<X: Executor> {
74    fn name(&self) -> &'static str;
75    fn up<'a>(&'a self, db: &'a X) -> StepFuture<'a>;
76}
77
78/// A migration that creates `E`'s table (+ its indexes) from the derived
79/// schema, idempotently (`CREATE TABLE IF NOT EXISTS`).
80pub struct CreateTable<E> {
81    name: &'static str,
82    _entity: PhantomData<E>,
83}
84
85impl<E> CreateTable<E> {
86    pub fn new(name: &'static str) -> Self {
87        Self { name, _entity: PhantomData }
88    }
89}
90
91impl<E: Entity, X: Executor> Migration<X> for CreateTable<E> {
92    fn name(&self) -> &'static str {
93        self.name
94    }
95    fn up<'a>(&'a self, db: &'a X) -> StepFuture<'a> {
96        Box::pin(async move {
97            for ddl in schema::schema_ddl::<E>(db.dialect()) {
98                db.execute(&ddl, vec![]).await?;
99            }
100            Ok(())
101        })
102    }
103}
104
105/// An ordered, idempotent set of migrations for one executor *type* `X`.
106pub struct Migrator<X> {
107    steps: Vec<Box<dyn Migration<X>>>,
108}
109
110impl<X: Executor> Default for Migrator<X> {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116impl<X: Executor> Migrator<X> {
117    pub fn new() -> Self {
118        Self { steps: Vec::new() }
119    }
120
121    /// Append a step (any `Migration` impl).
122    #[allow(clippy::should_implement_trait)] // builder method, not `std::ops::Add`
123    pub fn add(mut self, m: impl Migration<X> + 'static) -> Self {
124        self.steps.push(Box::new(m));
125        self
126    }
127
128    /// Append a "create `E`'s table" step.
129    pub fn create_table<E: Entity + 'static>(self, name: &'static str) -> Self {
130        self.add(CreateTable::<E>::new(name))
131    }
132
133    /// The names of every step, in registration order — what a
134    /// migration-runner port reports as `defined()`, and the source of
135    /// the migration "head" used to dedupe boot-time runs.
136    /// The port that reports it is defined by the consumer, not here.
137    pub fn names(&self) -> Vec<&'static str> {
138        self.steps.iter().map(|s| s.name()).collect()
139    }
140
141    /// Apply every not-yet-applied step to `db`, recording each in
142    /// `_orm_migrations`. Idempotent: re-running applies only new steps.
143    /// Returns the names applied this run.
144    ///
145    /// When `db` reports it is sharded ([`Executor::is_sharded`]),
146    /// the sharding control tables (`shards`, `shard_directory`) are ensured on
147    /// its global database first — the ORM owns that metadata, so no flavor
148    /// declares it. A no-op on a single, unsharded database.
149    pub async fn run(&self, db: &X) -> Result<Vec<&'static str>> {
150        if db.is_sharded() {
151            crate::sharding::ensure_control_tables(db).await?;
152        }
153        db.execute(&schema::create_table_ddl::<Applied>(db.dialect()), vec![]).await?;
154        let done: Vec<String> =
155            repo::all::<Applied, _>(db).await?.into_iter().map(|a| a.name).collect();
156
157        let mut applied = Vec::new();
158        for step in &self.steps {
159            if done.iter().any(|n| n == step.name()) {
160                continue;
161            }
162            step.up(db).await?;
163            repo::insert(db, &Applied { name: step.name().to_string() }).await?;
164            applied.push(step.name());
165        }
166        Ok(applied)
167    }
168
169    /// [`run`](Self::run) against each executor in turn — the shard fan-out
170    /// (every shard gets the same schema, tracked independently).
171    pub async fn run_on_each(&self, dbs: &[&X]) -> Result<()> {
172        for db in dbs {
173            self.run(db).await?;
174        }
175        Ok(())
176    }
177}