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}