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 {
135 Migrator { db, migrations, table: DEFAULT_TABLE.to_string() }
136 }
137
138 pub fn with_table(mut self, table: &str) -> Result<Self> {
140 crate::validate_identifier(table)?;
141 self.table = table.to_string();
142 Ok(self)
143 }
144
145 pub fn table(&self) -> &str {
146 &self.table
147 }
148
149 pub async fn prepare(&self) -> Result<()> {
154 let sql = self.db.dialect().migrations_table_sql(&self.table);
157 self.db.run(&sql).await?;
158 Ok(())
159 }
160
161 async fn record(&self, name: &str, batch: i64) -> Result<()> {
163 self.db
164 .execute(
165 &format!(
166 "insert into {} (name, batch) values ({}, {})",
167 self.quoted_table(),
168 self.db.dialect().placeholder(1),
169 self.db.dialect().placeholder(2)
170 ),
171 &[Value::from(name), Value::from(batch)],
172 )
173 .await?;
174 Ok(())
175 }
176
177 fn quoted_table(&self) -> String {
179 self.db.dialect().quote(&self.table)
180 }
181
182 pub async fn applied(&self) -> Result<Vec<String>> {
184 let rows = self
185 .db
186 .select(&format!("select name from {} order by id", self.quoted_table()), &[])
187 .await?;
188 rows.iter().map(|row| row.get::<String>("name")).collect()
189 }
190
191 pub async fn pending(&self) -> Result<Vec<&'a dyn Migration>> {
193 let applied = self.applied().await?;
194 Ok(self
195 .migrations
196 .iter()
197 .filter(|migration| !applied.iter().any(|name| name == migration.name()))
198 .copied()
199 .collect())
200 }
201
202 async fn next_batch(&self) -> Result<i64> {
203 let highest = self
204 .db
205 .scalar::<Option<i64>>(
206 &format!("select max(batch) from {}", self.quoted_table()),
207 &[],
208 )
209 .await?
210 .flatten();
211 Ok(highest.unwrap_or(0) + 1)
212 }
213
214 pub async fn run(&self) -> Result<MigrationReport> {
226 self.prepare().await?;
227
228 let pending = self.pending().await?;
229 let batch = self.next_batch().await?;
230 let mut report = MigrationReport {
231 skipped: self.migrations.len() - pending.len(),
232 ..MigrationReport::default()
233 };
234
235 for migration in pending {
236 let schema = Schema::new(self.db);
237
238 match migration.up(&schema).await {
239 Ok(()) => {
240 self.record(migration.name(), batch).await?;
241 report.applied.push(migration.name().to_string());
242 rustlavel_core::info!("migrated: {}", migration.name());
243 }
244 Err(error) => {
245 return Err(rustlavel_core::Error::msg(format!(
246 "migration `{}` failed: {error}\n \
247 Anything it had already done is still applied. Fix the migration and \
248 run `rustlavel migrate` again.",
249 migration.name()
250 )));
251 }
252 }
253 }
254
255 Ok(report)
256 }
257
258 pub async fn rollback(&self) -> Result<MigrationReport> {
260 self.prepare().await?;
261
262 let batch = self
263 .db
264 .scalar::<Option<i64>>(
265 &format!("select max(batch) from {}", self.quoted_table()),
266 &[],
267 )
268 .await?
269 .flatten();
270
271 let Some(batch) = batch else { return Ok(MigrationReport::default()) };
272
273 let rows = self
274 .db
275 .select(
276 &format!(
277 "select name from {} where batch = {} order by id desc",
278 self.quoted_table(),
279 self.db.dialect().placeholder(1)
280 ),
281 &[Value::from(batch)],
282 )
283 .await?;
284
285 let mut report = MigrationReport::default();
286
287 for row in rows {
288 let name = row.get::<String>("name")?;
289 let Some(migration) = self.migrations.iter().find(|m| m.name() == name) else {
290 return Err(rustlavel_core::Error::msg(format!(
291 "cannot roll back `{name}`: it is recorded as applied but is not in the \
292 migration registry. Did the file get deleted?"
293 )));
294 };
295
296 let schema = Schema::new(self.db);
297 match migration.down(&schema).await {
298 Ok(()) => {
299 self.db
300 .execute(
301 &format!(
302 "delete from {} where name = {}",
303 self.quoted_table(),
304 self.db.dialect().placeholder(1)
305 ),
306 &[Value::from(name.as_str())],
307 )
308 .await?;
309 report.rolled_back.push(name.clone());
310 rustlavel_core::info!("rolled back: {name}");
311 }
312 Err(error) => {
313 return Err(rustlavel_core::Error::msg(format!(
314 "rolling back `{name}` failed: {error}\n \
315 The migration is still recorded as applied."
316 )));
317 }
318 }
319 }
320
321 Ok(report)
322 }
323
324 pub async fn fresh(&self, environment: &str) -> Result<MigrationReport> {
329 if environment == "production" {
330 return Err(rustlavel_core::Error::msg(
331 "migrate:fresh drops every table and is refused in production. \
332 Use migrate, or set APP_ENV to something else if this really is a scratch database."
333 .to_string(),
334 ));
335 }
336
337 let dialect = self.db.dialect();
340
341 if let Some(sql) = dialect.disable_foreign_keys_sql() {
342 self.db.run(sql).await?;
343 }
344
345 let rows = self.db.select(dialect.list_tables_sql(), &[]).await?;
346 let tables: Vec<String> =
347 rows.iter().map(|row| row.get_at::<String>(0)).collect::<Result<_>>()?;
348
349 for table in &tables {
350 let sql = dialect.drop_table_sql(table);
351 self.db.run(&sql).await?;
352 }
353
354 if let Some(sql) = dialect.enable_foreign_keys_sql() {
355 self.db.run(sql).await?;
356 }
357
358 self.run().await
359 }
360
361 pub async fn status(&self) -> Result<Vec<(String, bool)>> {
363 self.prepare().await?;
364 let applied = self.applied().await?;
365
366 Ok(self
367 .migrations
368 .iter()
369 .map(|migration| {
370 let name = migration.name().to_string();
371 let has_run = applied.contains(&name);
372 (name, has_run)
373 })
374 .collect())
375 }
376}
377
378pub trait Seeder: Send + Sync {
380 fn name(&self) -> &'static str;
381
382 fn run<'a>(
383 &'a self,
384 db: &'a Database,
385 ) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
386}
387
388pub async fn seed(db: &Database, seeders: &[&dyn Seeder]) -> Result<Vec<String>> {
390 let mut ran = Vec::new();
391
392 for seeder in seeders {
393 seeder.run(db).await.map_err(|e| {
394 rustlavel_core::Error::msg(format!("seeder `{}` failed: {e}", seeder.name()))
395 })?;
396 rustlavel_core::info!("seeded: {}", seeder.name());
397 ran.push(seeder.name().to_string());
398 }
399
400 Ok(ran)
401}
402
403pub fn pluralize(word: &str) -> String {
409 let lower = word.to_lowercase();
410
411 for (singular, plural) in [
412 ("person", "people"),
413 ("child", "children"),
414 ("man", "men"),
415 ("woman", "women"),
416 ("tooth", "teeth"),
417 ("foot", "feet"),
418 ("mouse", "mice"),
419 ("goose", "geese"),
420 ] {
421 if lower.ends_with(singular) {
422 return format!("{}{plural}", &word[..word.len() - singular.len()]);
423 }
424 }
425
426 if lower.ends_with('s') && !lower.ends_with("us") && !lower.ends_with("ss") {
427 return word.to_string();
428 }
429 if let Some(stem) = lower.strip_suffix('y')
430 && !stem.ends_with(['a', 'e', 'i', 'o', 'u']) {
431 return format!("{}ies", &word[..word.len() - 1]);
432 }
433 if lower.ends_with(['s', 'x', 'z']) || lower.ends_with("ch") || lower.ends_with("sh") {
434 return format!("{word}es");
435 }
436 format!("{word}s")
437}
438
439pub struct Faker {
444 state: u64,
445}
446
447impl Faker {
448 pub fn new(seed: u64) -> Self {
449 Faker { state: seed.max(1) }
450 }
451
452 fn next(&mut self) -> u64 {
453 self.state ^= self.state << 13;
455 self.state ^= self.state >> 7;
456 self.state ^= self.state << 17;
457 self.state
458 }
459
460 pub fn number(&mut self, low: i64, high: i64) -> i64 {
461 if high <= low {
462 return low;
463 }
464 low + (self.next() % (high - low + 1) as u64) as i64
465 }
466
467 pub fn boolean(&mut self) -> bool {
468 self.next().is_multiple_of(2)
469 }
470
471 pub fn pick<'a, T>(&mut self, options: &'a [T]) -> &'a T {
472 &options[(self.next() % options.len() as u64) as usize]
473 }
474
475 pub fn name(&mut self) -> String {
476 const FIRST: &[&str] = &[
477 "Ada", "Grace", "Alan", "Linus", "Barbara", "Ken", "Margaret", "Dennis", "Radia", "Guido",
478 ];
479 const LAST: &[&str] = &[
480 "Lovelace", "Hopper", "Turing", "Torvalds", "Liskov", "Thompson", "Hamilton", "Ritchie",
481 "Perlman", "Rossum",
482 ];
483 format!("{} {}", self.pick(FIRST), self.pick(LAST))
484 }
485
486 pub fn email(&mut self) -> String {
487 let name = self.name().to_lowercase().replace(' ', ".");
488 format!("{name}{}@example.com", self.number(1, 9999))
489 }
490
491 pub fn sentence(&mut self) -> String {
492 const WORDS: &[&str] = &[
493 "rust", "framework", "query", "handler", "migration", "route", "template", "record",
494 "worker", "cache",
495 ];
496 let count = self.number(4, 9) as usize;
497 let mut words: Vec<String> = (0..count).map(|_| self.pick(WORDS).to_string()).collect();
498 words[0] = {
499 let first = &words[0];
500 let mut chars = first.chars();
501 match chars.next() {
502 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
503 None => String::new(),
504 }
505 };
506 format!("{}.", words.join(" "))
507 }
508
509 pub fn slug(&mut self) -> String {
510 self.sentence().trim_end_matches('.').to_lowercase().replace(' ', "-")
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn pluralizes_for_foreign_keys() {
520 assert_eq!(pluralize("user"), "users");
521 assert_eq!(pluralize("category"), "categories");
522 assert_eq!(pluralize("person"), "people");
523 assert_eq!(pluralize("status"), "statuses");
524 }
525
526 #[test]
527 fn the_faker_is_reproducible() {
528 let mut first = Faker::new(42);
529 let mut second = Faker::new(42);
530
531 assert_eq!(first.name(), second.name());
532 assert_eq!(first.email(), second.email());
533 assert_eq!(first.number(1, 100), second.number(1, 100));
534 }
535
536 #[test]
537 fn different_seeds_diverge() {
538 assert_ne!(Faker::new(1).sentence(), Faker::new(2).sentence());
539 }
540
541 #[test]
542 fn faker_numbers_stay_in_range() {
543 let mut faker = Faker::new(7);
544 for _ in 0..200 {
545 let value = faker.number(5, 10);
546 assert!((5..=10).contains(&value), "{value} out of range");
547 }
548 }
549}