1use crate::schema::Schema;
9use crate::{Database, Value};
10use rustlavel_core::Result;
11
12pub const DEFAULT_TABLE: &str = "rustlavel_migrations";
14
15pub trait Migration: Send + Sync {
20 fn name(&self) -> &'static str;
21
22 fn up<'a>(
24 &'a self,
25 schema: &'a Schema<'a>,
26 ) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
27
28 fn down<'a>(
31 &'a self,
32 schema: &'a Schema<'a>,
33 ) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
34}
35
36#[macro_export]
53macro_rules! migration {
54 (
55 $type:ident,
56 $name:literal,
57 up: |$up_schema:ident| $up:block,
58 down: |$down_schema:ident| $down:block $(,)?
59 ) => {
60 pub struct $type;
61
62 impl $crate::migration::Migration for $type {
63 fn name(&self) -> &'static str {
64 $name
65 }
66
67 fn up<'a>(
68 &'a self,
69 schema: &'a $crate::schema::Schema<'a>,
70 ) -> ::std::pin::Pin<
71 ::std::boxed::Box<dyn ::std::future::Future<Output = $crate::Result<()>> + Send + 'a>,
72 > {
73 let $up_schema = schema;
74 ::std::boxed::Box::pin(async move { $up })
75 }
76
77 fn down<'a>(
78 &'a self,
79 schema: &'a $crate::schema::Schema<'a>,
80 ) -> ::std::pin::Pin<
81 ::std::boxed::Box<dyn ::std::future::Future<Output = $crate::Result<()>> + Send + 'a>,
82 > {
83 let $down_schema = schema;
84 ::std::boxed::Box::pin(async move { $down })
85 }
86 }
87 };
88}
89
90#[derive(Debug, Default, PartialEq)]
92pub struct MigrationReport {
93 pub applied: Vec<String>,
94 pub rolled_back: Vec<String>,
95 pub skipped: usize,
96}
97
98pub struct Migrator<'a> {
100 db: &'a Database,
101 migrations: Vec<&'a dyn Migration>,
102 table: String,
108}
109
110impl<'a> Migrator<'a> {
111 pub fn new(db: &'a Database, migrations: Vec<&'a dyn Migration>) -> Self {
112 Migrator { db, migrations, table: DEFAULT_TABLE.to_string() }
113 }
114
115 pub fn with_table(mut self, table: &str) -> Result<Self> {
117 crate::validate_identifier(table)?;
118 self.table = table.to_string();
119 Ok(self)
120 }
121
122 pub fn table(&self) -> &str {
123 &self.table
124 }
125
126 pub async fn prepare(&self) -> Result<()> {
131 let sql = self.db.dialect().migrations_table_sql(&self.table);
134 self.db.run(&sql).await?;
135 Ok(())
136 }
137
138 async fn record(&self, name: &str, batch: i64) -> Result<()> {
140 self.db
141 .execute(
142 &format!(
143 "insert into {} (name, batch) values ({}, {})",
144 self.quoted_table(),
145 self.db.dialect().placeholder(1),
146 self.db.dialect().placeholder(2)
147 ),
148 &[Value::from(name), Value::from(batch)],
149 )
150 .await?;
151 Ok(())
152 }
153
154 fn quoted_table(&self) -> String {
156 self.db.dialect().quote(&self.table)
157 }
158
159 pub async fn applied(&self) -> Result<Vec<String>> {
161 let rows = self
162 .db
163 .select(&format!("select name from {} order by id", self.quoted_table()), &[])
164 .await?;
165 rows.iter().map(|row| row.get::<String>("name")).collect()
166 }
167
168 pub async fn pending(&self) -> Result<Vec<&'a dyn Migration>> {
170 let applied = self.applied().await?;
171 Ok(self
172 .migrations
173 .iter()
174 .filter(|migration| !applied.iter().any(|name| name == migration.name()))
175 .copied()
176 .collect())
177 }
178
179 async fn next_batch(&self) -> Result<i64> {
180 let highest = self
181 .db
182 .scalar::<Option<i64>>(
183 &format!("select max(batch) from {}", self.quoted_table()),
184 &[],
185 )
186 .await?
187 .flatten();
188 Ok(highest.unwrap_or(0) + 1)
189 }
190
191 pub async fn run(&self) -> Result<MigrationReport> {
203 self.prepare().await?;
204
205 let pending = self.pending().await?;
206 let batch = self.next_batch().await?;
207 let mut report = MigrationReport {
208 skipped: self.migrations.len() - pending.len(),
209 ..MigrationReport::default()
210 };
211
212 for migration in pending {
213 let schema = Schema::new(self.db);
214
215 match migration.up(&schema).await {
216 Ok(()) => {
217 self.record(migration.name(), batch).await?;
218 report.applied.push(migration.name().to_string());
219 rustlavel_core::info!("migrated: {}", migration.name());
220 }
221 Err(error) => {
222 return Err(rustlavel_core::Error::msg(format!(
223 "migration `{}` failed: {error}\n \
224 Anything it had already done is still applied. Fix the migration and \
225 run `rustlavel migrate` again.",
226 migration.name()
227 )));
228 }
229 }
230 }
231
232 Ok(report)
233 }
234
235 pub async fn rollback(&self) -> Result<MigrationReport> {
237 self.prepare().await?;
238
239 let batch = self
240 .db
241 .scalar::<Option<i64>>(
242 &format!("select max(batch) from {}", self.quoted_table()),
243 &[],
244 )
245 .await?
246 .flatten();
247
248 let Some(batch) = batch else { return Ok(MigrationReport::default()) };
249
250 let rows = self
251 .db
252 .select(
253 &format!(
254 "select name from {} where batch = {} order by id desc",
255 self.quoted_table(),
256 self.db.dialect().placeholder(1)
257 ),
258 &[Value::from(batch)],
259 )
260 .await?;
261
262 let mut report = MigrationReport::default();
263
264 for row in rows {
265 let name = row.get::<String>("name")?;
266 let Some(migration) = self.migrations.iter().find(|m| m.name() == name) else {
267 return Err(rustlavel_core::Error::msg(format!(
268 "cannot roll back `{name}`: it is recorded as applied but is not in the \
269 migration registry. Did the file get deleted?"
270 )));
271 };
272
273 let schema = Schema::new(self.db);
274 match migration.down(&schema).await {
275 Ok(()) => {
276 self.db
277 .execute(
278 &format!(
279 "delete from {} where name = {}",
280 self.quoted_table(),
281 self.db.dialect().placeholder(1)
282 ),
283 &[Value::from(name.as_str())],
284 )
285 .await?;
286 report.rolled_back.push(name.clone());
287 rustlavel_core::info!("rolled back: {name}");
288 }
289 Err(error) => {
290 return Err(rustlavel_core::Error::msg(format!(
291 "rolling back `{name}` failed: {error}\n \
292 The migration is still recorded as applied."
293 )));
294 }
295 }
296 }
297
298 Ok(report)
299 }
300
301 pub async fn fresh(&self, environment: &str) -> Result<MigrationReport> {
306 if environment == "production" {
307 return Err(rustlavel_core::Error::msg(
308 "migrate:fresh drops every table and is refused in production. \
309 Use migrate, or set APP_ENV to something else if this really is a scratch database."
310 .to_string(),
311 ));
312 }
313
314 let dialect = self.db.dialect();
317
318 if let Some(sql) = dialect.disable_foreign_keys_sql() {
319 self.db.run(sql).await?;
320 }
321
322 let rows = self.db.select(dialect.list_tables_sql(), &[]).await?;
323 let tables: Vec<String> =
324 rows.iter().map(|row| row.get_at::<String>(0)).collect::<Result<_>>()?;
325
326 for table in &tables {
327 let sql = dialect.drop_table_sql(table);
328 self.db.run(&sql).await?;
329 }
330
331 if let Some(sql) = dialect.enable_foreign_keys_sql() {
332 self.db.run(sql).await?;
333 }
334
335 self.run().await
336 }
337
338 pub async fn status(&self) -> Result<Vec<(String, bool)>> {
340 self.prepare().await?;
341 let applied = self.applied().await?;
342
343 Ok(self
344 .migrations
345 .iter()
346 .map(|migration| {
347 let name = migration.name().to_string();
348 let has_run = applied.contains(&name);
349 (name, has_run)
350 })
351 .collect())
352 }
353}
354
355pub trait Seeder: Send + Sync {
357 fn name(&self) -> &'static str;
358
359 fn run<'a>(
360 &'a self,
361 db: &'a Database,
362 ) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
363}
364
365pub async fn seed(db: &Database, seeders: &[&dyn Seeder]) -> Result<Vec<String>> {
367 let mut ran = Vec::new();
368
369 for seeder in seeders {
370 seeder.run(db).await.map_err(|e| {
371 rustlavel_core::Error::msg(format!("seeder `{}` failed: {e}", seeder.name()))
372 })?;
373 rustlavel_core::info!("seeded: {}", seeder.name());
374 ran.push(seeder.name().to_string());
375 }
376
377 Ok(ran)
378}
379
380pub fn pluralize(word: &str) -> String {
386 let lower = word.to_lowercase();
387
388 for (singular, plural) in [
389 ("person", "people"),
390 ("child", "children"),
391 ("man", "men"),
392 ("woman", "women"),
393 ("tooth", "teeth"),
394 ("foot", "feet"),
395 ("mouse", "mice"),
396 ("goose", "geese"),
397 ] {
398 if lower.ends_with(singular) {
399 return format!("{}{plural}", &word[..word.len() - singular.len()]);
400 }
401 }
402
403 if lower.ends_with('s') && !lower.ends_with("us") && !lower.ends_with("ss") {
404 return word.to_string();
405 }
406 if let Some(stem) = lower.strip_suffix('y')
407 && !stem.ends_with(['a', 'e', 'i', 'o', 'u']) {
408 return format!("{}ies", &word[..word.len() - 1]);
409 }
410 if lower.ends_with(['s', 'x', 'z']) || lower.ends_with("ch") || lower.ends_with("sh") {
411 return format!("{word}es");
412 }
413 format!("{word}s")
414}
415
416pub struct Faker {
421 state: u64,
422}
423
424impl Faker {
425 pub fn new(seed: u64) -> Self {
426 Faker { state: seed.max(1) }
427 }
428
429 fn next(&mut self) -> u64 {
430 self.state ^= self.state << 13;
432 self.state ^= self.state >> 7;
433 self.state ^= self.state << 17;
434 self.state
435 }
436
437 pub fn number(&mut self, low: i64, high: i64) -> i64 {
438 if high <= low {
439 return low;
440 }
441 low + (self.next() % (high - low + 1) as u64) as i64
442 }
443
444 pub fn boolean(&mut self) -> bool {
445 self.next().is_multiple_of(2)
446 }
447
448 pub fn pick<'a, T>(&mut self, options: &'a [T]) -> &'a T {
449 &options[(self.next() % options.len() as u64) as usize]
450 }
451
452 pub fn name(&mut self) -> String {
453 const FIRST: &[&str] = &[
454 "Ada", "Grace", "Alan", "Linus", "Barbara", "Ken", "Margaret", "Dennis", "Radia", "Guido",
455 ];
456 const LAST: &[&str] = &[
457 "Lovelace", "Hopper", "Turing", "Torvalds", "Liskov", "Thompson", "Hamilton", "Ritchie",
458 "Perlman", "Rossum",
459 ];
460 format!("{} {}", self.pick(FIRST), self.pick(LAST))
461 }
462
463 pub fn email(&mut self) -> String {
464 let name = self.name().to_lowercase().replace(' ', ".");
465 format!("{name}{}@example.com", self.number(1, 9999))
466 }
467
468 pub fn sentence(&mut self) -> String {
469 const WORDS: &[&str] = &[
470 "rust", "framework", "query", "handler", "migration", "route", "template", "record",
471 "worker", "cache",
472 ];
473 let count = self.number(4, 9) as usize;
474 let mut words: Vec<String> = (0..count).map(|_| self.pick(WORDS).to_string()).collect();
475 words[0] = {
476 let first = &words[0];
477 let mut chars = first.chars();
478 match chars.next() {
479 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
480 None => String::new(),
481 }
482 };
483 format!("{}.", words.join(" "))
484 }
485
486 pub fn slug(&mut self) -> String {
487 self.sentence().trim_end_matches('.').to_lowercase().replace(' ', "-")
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn pluralizes_for_foreign_keys() {
497 assert_eq!(pluralize("user"), "users");
498 assert_eq!(pluralize("category"), "categories");
499 assert_eq!(pluralize("person"), "people");
500 assert_eq!(pluralize("status"), "statuses");
501 }
502
503 #[test]
504 fn the_faker_is_reproducible() {
505 let mut first = Faker::new(42);
506 let mut second = Faker::new(42);
507
508 assert_eq!(first.name(), second.name());
509 assert_eq!(first.email(), second.email());
510 assert_eq!(first.number(1, 100), second.number(1, 100));
511 }
512
513 #[test]
514 fn different_seeds_diverge() {
515 assert_ne!(Faker::new(1).sentence(), Faker::new(2).sentence());
516 }
517
518 #[test]
519 fn faker_numbers_stay_in_range() {
520 let mut faker = Faker::new(7);
521 for _ in 0..200 {
522 let value = faker.number(5, 10);
523 assert!((5..=10).contains(&value), "{value} out of range");
524 }
525 }
526}