1use std::collections::{HashMap, HashSet};
2
3use super::{Db, Dialect, now, quote, script, sql};
4use anyhow::{Context, anyhow, bail};
5
6const TABLE: &str = "renox_migrations";
7
8macro_rules! framework_migration {
11 ($dir:literal, $name:literal) => {
12 $crate::db::Migration::new(
13 $name,
14 include_str!(concat!(
15 env!("CARGO_MANIFEST_DIR"),
16 "/migrations/",
17 $dir,
18 "/",
19 $name,
20 ".up.sql"
21 )),
22 Some(include_str!(concat!(
23 env!("CARGO_MANIFEST_DIR"),
24 "/migrations/",
25 $dir,
26 "/",
27 $name,
28 ".down.sql"
29 ))),
30 )
31 .postgres(
32 include_str!(concat!(
33 env!("CARGO_MANIFEST_DIR"),
34 "/migrations/",
35 $dir,
36 "/",
37 $name,
38 ".postgres.up.sql"
39 )),
40 None,
41 )
42 };
43}
44pub(crate) use framework_migration;
45
46#[derive(Debug, Clone, Copy)]
71#[non_exhaustive]
72pub struct Migration {
73 name: &'static str,
74 up: &'static str,
77 down: Option<&'static str>,
79 sqlite: Option<Scripts>,
81 postgres: Option<Scripts>,
83}
84
85#[derive(Debug, Clone, Copy)]
87struct Scripts {
88 up: &'static str,
89 down: Option<&'static str>,
91}
92
93impl Migration {
94 pub const fn new(name: &'static str, up: &'static str, down: Option<&'static str>) -> Self {
98 Self {
99 name,
100 up,
101 down,
102 sqlite: None,
103 postgres: None,
104 }
105 }
106
107 pub const fn sqlite(mut self, up: &'static str, down: Option<&'static str>) -> Self {
110 self.sqlite = Some(Scripts { up, down });
111 self
112 }
113
114 pub const fn postgres(mut self, up: &'static str, down: Option<&'static str>) -> Self {
117 self.postgres = Some(Scripts { up, down });
118 self
119 }
120
121 pub const fn name(&self) -> &'static str {
123 self.name
124 }
125
126 fn own(&self, dialect: Dialect) -> Option<&Scripts> {
127 match dialect {
128 Dialect::Sqlite => self.sqlite.as_ref(),
129 Dialect::Postgres => self.postgres.as_ref(),
130 }
131 }
132
133 pub fn up_for(&self, dialect: Dialect) -> &'static str {
135 self.own(dialect).map_or(self.up, |own| own.up)
136 }
137
138 pub fn down_for(&self, dialect: Dialect) -> Option<&'static str> {
140 self.own(dialect).and_then(|own| own.down).or(self.down)
141 }
142}
143
144#[derive(Debug, Clone)]
146#[non_exhaustive]
147pub struct MigrationStatus {
148 pub name: String,
150 pub batch: Option<i64>,
152 pub missing: bool,
154 pub changed: bool,
157}
158
159const NO_TRANSACTION: &str = "-- renox:no-transaction";
161
162fn runs_outside_transaction(sql: &str) -> bool {
166 if sql.lines().any(|line| line.trim() == NO_TRANSACTION) {
167 return true;
168 }
169 let upper = sql.to_ascii_uppercase();
170 uses_concurrently(sql)
171 || upper.split(';').any(|statement| {
172 let statement = statement.trim();
173 statement == "BEGIN"
174 || statement.starts_with("BEGIN TRANSACTION")
175 || statement.starts_with("BEGIN IMMEDIATE")
176 })
177}
178
179fn uses_concurrently(sql: &str) -> bool {
182 sql.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
183 .any(|word| word.eq_ignore_ascii_case("CONCURRENTLY"))
184}
185
186async fn run_each(db: &Db, sql: &str) -> Result<(), super::DbError> {
190 if db.dialect() == Dialect::Postgres && uses_concurrently(sql) {
191 for statement in statements(sql) {
192 script(db, statement).await?;
193 }
194 Ok(())
195 } else {
196 script(db, sql).await.map(|_| ())
197 }
198}
199
200fn statements(sql: &str) -> Vec<&str> {
202 let bytes = sql.as_bytes();
203 let mut out = Vec::new();
204 let (mut start, mut i) = (0, 0);
205 while i < bytes.len() {
206 match bytes[i] {
207 quote @ (b'\'' | b'"') => {
208 i += 1;
209 while i < bytes.len() && bytes[i] != quote {
210 i += 1;
211 }
212 }
213 b'-' if bytes.get(i + 1) == Some(&b'-') => {
214 while i < bytes.len() && bytes[i] != b'\n' {
215 i += 1;
216 }
217 }
218 b'/' if bytes.get(i + 1) == Some(&b'*') => {
219 i = sql[i + 2..]
220 .find("*/")
221 .map_or(bytes.len(), |end| i + 2 + end + 1);
222 }
223 b'$' => {
224 let tag_end = sql[i + 1..]
225 .find(|c: char| !(c.is_alphanumeric() || c == '_'))
226 .map(|n| i + 1 + n);
227 if let Some(end) = tag_end.filter(|&end| bytes[end] == b'$') {
228 let tag = &sql[i..=end];
229 i = sql[end + 1..]
230 .find(tag)
231 .map_or(bytes.len(), |close| end + 1 + close + tag.len() - 1);
232 }
233 }
234 b';' => {
235 out.push(&sql[start..i]);
236 start = i + 1;
237 }
238 _ => {}
239 }
240 i += 1;
241 }
242 out.push(&sql[start..]);
243 out.into_iter()
244 .filter(|statement| {
245 statement
246 .lines()
247 .any(|line| !line.trim().is_empty() && !line.trim().starts_with("--"))
248 })
249 .collect()
250}
251
252fn checksum(sql: &str) -> String {
253 crate::webhook::sha256_hex(sql)
254}
255
256pub(crate) struct Migrator {
261 migrations: Vec<Migration>,
262}
263
264struct Applied {
266 batch: i64,
267 checksum: Option<String>,
268}
269
270struct MigrationLock {
272 _local: tokio::sync::MutexGuard<'static, ()>,
273 #[cfg(feature = "postgres")]
274 _postgres: Option<sqlx::postgres::PgConnection>,
275}
276
277#[cfg(feature = "postgres")]
279const ADVISORY_KEY: i64 = 0x7265_6e6f_786d_6967;
280
281impl Migrator {
282 pub fn new(mut migrations: Vec<Migration>) -> anyhow::Result<Self> {
283 migrations.sort_by_key(|m| m.name);
284 for pair in migrations.windows(2) {
285 if pair[0].name == pair[1].name {
286 bail!("migration `{}` is registered twice", pair[0].name);
287 }
288 }
289 Ok(Self { migrations })
290 }
291
292 async fn lock(db: &Db) -> anyhow::Result<MigrationLock> {
293 static LOCAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
294 let local = LOCAL.lock().await;
295 #[cfg(feature = "postgres")]
296 let postgres = match db.postgres() {
297 Some(pool) => {
298 let mut conn = pool.acquire().await?.detach();
301 sqlx::query("SELECT pg_advisory_lock($1)")
302 .bind(ADVISORY_KEY)
303 .execute(&mut conn)
304 .await?;
305 Some(conn)
306 }
307 None => None,
308 };
309 #[cfg(not(feature = "postgres"))]
310 let _ = db;
311 Ok(MigrationLock {
312 _local: local,
313 #[cfg(feature = "postgres")]
314 _postgres: postgres,
315 })
316 }
317
318 async fn ensure_table(db: &Db) -> anyhow::Result<()> {
319 sql(format!(
320 "CREATE TABLE IF NOT EXISTS {TABLE} (
321 name TEXT PRIMARY KEY NOT NULL,
322 batch BIGINT NOT NULL,
323 applied_at TEXT NOT NULL,
324 checksum TEXT
325 )"
326 ))
327 .execute(db)
328 .await?;
329 if sql(format!("SELECT checksum FROM {TABLE} WHERE 1 = 0"))
331 .execute(db)
332 .await
333 .is_err()
334 {
335 sql(format!("ALTER TABLE {TABLE} ADD COLUMN checksum TEXT"))
336 .execute(db)
337 .await?;
338 db.schema_changed();
339 }
340 Ok(())
341 }
342
343 async fn applied(db: &Db) -> anyhow::Result<HashMap<String, Applied>> {
344 Self::ensure_table(db).await?;
345 let rows = sql(format!("SELECT name, batch, checksum FROM {TABLE}"))
346 .fetch_all(db)
347 .await?;
348 rows.iter()
349 .map(|row| {
350 Ok((
351 row.try_get("name")?,
352 Applied {
353 batch: row.try_get("batch")?,
354 checksum: row.try_get("checksum")?,
355 },
356 ))
357 })
358 .collect()
359 }
360
361 pub async fn run(&self, db: &Db) -> anyhow::Result<Vec<&'static str>> {
363 let _lock = Self::lock(db).await?;
364 self.run_locked(db).await
365 }
366
367 async fn run_locked(&self, db: &Db) -> anyhow::Result<Vec<&'static str>> {
368 let dialect = db.dialect();
369 let applied = Self::applied(db).await?;
370 for migration in &self.migrations {
371 let changed = applied.get(migration.name).is_some_and(|a| {
372 a.checksum
373 .as_ref()
374 .is_some_and(|sum| *sum != checksum(migration.up_for(dialect)))
375 });
376 if changed {
377 tracing::warn!(
378 migration = migration.name,
379 "an applied migration was edited; the change won't run (add a new migration)"
380 );
381 }
382 }
383 let batch = applied.values().map(|a| a.batch).max().unwrap_or(0) + 1;
384 let mut done = Vec::new();
385 let result = self.run_pending(db, &applied, batch, &mut done).await;
386 if !done.is_empty() {
388 db.schema_changed();
389 }
390 result.map(|()| done)
391 }
392
393 async fn run_pending(
394 &self,
395 db: &Db,
396 applied: &HashMap<String, Applied>,
397 batch: i64,
398 done: &mut Vec<&'static str>,
399 ) -> anyhow::Result<()> {
400 let dialect = db.dialect();
401
402 for migration in self
403 .migrations
404 .iter()
405 .filter(|m| !applied.contains_key(m.name))
406 {
407 let up = migration.up_for(dialect);
408 let failed = || format!("migration `{}` failed", migration.name);
409 let record = sql(format!(
410 "INSERT INTO {TABLE} (name, batch, applied_at, checksum) VALUES (?, ?, ?, ?)"
411 ))
412 .bind(migration.name)
413 .bind(batch)
414 .bind(now().to_rfc3339())
415 .bind(checksum(up));
416 if runs_outside_transaction(up) {
417 run_each(db, up).await.with_context(failed)?;
418 record.execute(db).await?;
419 } else {
420 let mut tx = db.begin_immediate().await?;
423 let already: i64 = sql(format!("SELECT COUNT(*) FROM {TABLE} WHERE name = ?"))
424 .bind(migration.name)
425 .scalar(&mut tx)
426 .await?;
427 if already > 0 {
428 continue;
429 }
430 script(&mut tx, up).await.with_context(failed)?;
431 record.execute(&mut tx).await?;
432 tx.commit().await?;
433 }
434 done.push(migration.name);
435 }
436 Ok(())
437 }
438
439 pub async fn rollback(&self, db: &Db, batches: u32) -> anyhow::Result<Vec<String>> {
443 let _lock = Self::lock(db).await?;
444 let dialect = db.dialect();
445 let applied = Self::applied(db).await?;
446 let mut numbers: Vec<i64> = applied
447 .values()
448 .map(|a| a.batch)
449 .collect::<HashSet<_>>()
450 .into_iter()
451 .collect();
452 numbers.sort_unstable_by(|a, b| b.cmp(a));
453 let targets: HashSet<i64> = numbers.into_iter().take(batches as usize).collect();
454
455 let mut names: Vec<&String> = applied
456 .iter()
457 .filter(|(_, a)| targets.contains(&a.batch))
458 .map(|(name, _)| name)
459 .collect();
460 names.sort_unstable_by(|a, b| b.cmp(a));
461
462 let mut steps = Vec::new();
464 for name in names {
465 let down = match self.migrations.iter().find(|m| m.name == name) {
466 None => None,
467 Some(migration) => Some(migration.down_for(dialect).ok_or_else(|| {
468 anyhow!(
469 "migration `{name}` has no .down.sql, so its batch can't be rolled back; \
470 nothing was rolled back"
471 )
472 })?),
473 };
474 steps.push((name, down));
475 }
476
477 let mut done = Vec::new();
478 for (name, down) in steps {
479 let forget = sql(format!("DELETE FROM {TABLE} WHERE name = ?")).bind(name);
480 match down {
481 None => {
482 tracing::warn!(
483 migration = %name,
484 "not registered any more; forgotten without undoing its changes"
485 );
486 forget.execute(db).await?;
487 }
488 Some(down) if runs_outside_transaction(down) => {
489 run_each(db, down)
490 .await
491 .with_context(|| format!("rolling back `{name}` failed"))?;
492 forget.execute(db).await?;
493 }
494 Some(down) => {
495 let mut tx = db.begin().await?;
496 script(&mut tx, down)
497 .await
498 .with_context(|| format!("rolling back `{name}` failed"))?;
499 forget.execute(&mut tx).await?;
500 tx.commit().await?;
501 }
502 }
503 done.push(name.clone());
504 db.schema_changed();
506 }
507 Ok(done)
508 }
509
510 pub async fn fresh(&self, db: &Db) -> anyhow::Result<Vec<&'static str>> {
512 let _lock = Self::lock(db).await?;
513 if let Some(pool) = db.sqlite() {
514 drop_all_sqlite(pool).await?;
515 }
516 #[cfg(feature = "postgres")]
517 if let Some(pool) = db.postgres() {
518 drop_all_postgres(pool).await?;
519 }
520 db.schema_changed();
521 self.run_locked(db).await
522 }
523
524 pub async fn status(&self, db: &Db) -> anyhow::Result<Vec<MigrationStatus>> {
527 let dialect = db.dialect();
528 let applied = Self::applied(db).await?;
529 let mut status: Vec<MigrationStatus> = self
530 .migrations
531 .iter()
532 .map(|m| {
533 let found = applied.get(m.name);
534 MigrationStatus {
535 name: m.name.to_owned(),
536 batch: found.map(|a| a.batch),
537 missing: false,
538 changed: found.is_some_and(|a| {
539 a.checksum
540 .as_ref()
541 .is_some_and(|sum| *sum != checksum(m.up_for(dialect)))
542 }),
543 }
544 })
545 .collect();
546 let mut missing: Vec<MigrationStatus> = applied
547 .iter()
548 .filter(|(name, _)| !self.migrations.iter().any(|m| m.name == name.as_str()))
549 .map(|(name, a)| MigrationStatus {
550 name: name.clone(),
551 batch: Some(a.batch),
552 missing: true,
553 changed: false,
554 })
555 .collect();
556 missing.sort_by(|a, b| a.name.cmp(&b.name));
557 status.extend(missing);
558 Ok(status)
559 }
560}
561
562async fn drop_all_sqlite(pool: &sqlx::SqlitePool) -> anyhow::Result<()> {
565 use sqlx::{AssertSqlSafe, Row};
566
567 let mut conn = pool.acquire().await?;
568 let objects = sqlx::query(
571 "SELECT type, name FROM sqlite_master \
572 WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' \
573 ORDER BY CASE WHEN sql LIKE 'CREATE VIRTUAL TABLE%' THEN 0 ELSE 1 END, rowid",
574 )
575 .fetch_all(&mut *conn)
576 .await?;
577 sqlx::query("PRAGMA foreign_keys = OFF")
578 .execute(&mut *conn)
579 .await?;
580 for object in &objects {
581 let kind: String = object.try_get("type")?;
582 let name: String = object.try_get("name")?;
583 let sql = format!("DROP {} IF EXISTS {}", kind.to_uppercase(), quote(&name));
584 sqlx::query(AssertSqlSafe(sql)).execute(&mut *conn).await?;
585 }
586 sqlx::query("PRAGMA foreign_keys = ON")
587 .execute(&mut *conn)
588 .await?;
589 Ok(())
590}
591
592#[cfg(feature = "postgres")]
596async fn drop_all_postgres(pool: &sqlx::PgPool) -> anyhow::Result<()> {
597 use sqlx::{AssertSqlSafe, Row};
598
599 const NOT_FROM_EXTENSION: &str = "NOT EXISTS (SELECT 1 FROM pg_depend d \
601 WHERE d.objid = {oid} AND d.deptype = 'e')";
602 let owned = |oid: &str| NOT_FROM_EXTENSION.replace("{oid}", oid);
603 let queries = [
604 format!(
605 "SELECT format('DROP MATERIALIZED VIEW IF EXISTS %I CASCADE', c.relname) \
606 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace \
607 WHERE n.nspname = current_schema() AND c.relkind = 'm' AND {}",
608 owned("c.oid")
609 ),
610 format!(
611 "SELECT format('DROP VIEW IF EXISTS %I CASCADE', c.relname) \
612 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace \
613 WHERE n.nspname = current_schema() AND c.relkind = 'v' AND {}",
614 owned("c.oid")
615 ),
616 format!(
617 "SELECT format('DROP TABLE IF EXISTS %I CASCADE', c.relname) \
618 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace \
619 WHERE n.nspname = current_schema() AND c.relkind IN ('r', 'p') AND {}",
620 owned("c.oid")
621 ),
622 format!(
623 "SELECT format('DROP SEQUENCE IF EXISTS %I CASCADE', c.relname) \
624 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace \
625 WHERE n.nspname = current_schema() AND c.relkind = 'S' AND {}",
626 owned("c.oid")
627 ),
628 format!(
629 "SELECT format('DROP ROUTINE IF EXISTS %s CASCADE', p.oid::regprocedure) \
630 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace \
631 WHERE n.nspname = current_schema() AND p.prokind IN ('f', 'p') AND {}",
632 owned("p.oid")
633 ),
634 format!(
635 "SELECT format(CASE t.typtype WHEN 'd' THEN 'DROP DOMAIN IF EXISTS %I CASCADE' \
636 ELSE 'DROP TYPE IF EXISTS %I CASCADE' END, t.typname) \
637 FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace \
638 LEFT JOIN pg_class c ON c.oid = t.typrelid \
639 WHERE n.nspname = current_schema() AND t.typtype IN ('e', 'd', 'r', 'c') \
640 AND (t.typtype <> 'c' OR c.relkind = 'c') AND {}",
641 owned("t.oid")
642 ),
643 ];
644 for query in queries {
645 let statements = sqlx::query(AssertSqlSafe(query)).fetch_all(pool).await?;
646 for statement in &statements {
647 let statement: String = statement.try_get(0)?;
648 sqlx::query(AssertSqlSafe(statement)).execute(pool).await?;
649 }
650 }
651 Ok(())
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn splits_statements_outside_quotes_comments_and_bodies() {
660 let sql = "-- renox:no-transaction\nCREATE TABLE t (s TEXT DEFAULT ';');\n\
661 /* a; b */ CREATE FUNCTION f() RETURNS INT AS $body$ SELECT 1; $body$ LANGUAGE SQL;\n\
662 CREATE INDEX CONCURRENTLY i ON t (s);\n";
663 let parts = statements(sql);
664 assert_eq!(parts.len(), 3, "{parts:?}");
665 assert!(parts[1].contains("SELECT 1; $body$"));
666 assert!(parts[2].trim().starts_with("CREATE INDEX CONCURRENTLY"));
667 assert!(runs_outside_transaction(sql));
668 assert!(runs_outside_transaction(
669 "BEGIN; CREATE TABLE a (id INT); COMMIT;"
670 ));
671 assert!(!runs_outside_transaction(
672 "CREATE TABLE begin_log (id INT);"
673 ));
674 }
675
676 #[test]
678 fn concurrently_is_found_whatever_follows_it() {
679 assert!(runs_outside_transaction(
680 "CREATE INDEX CONCURRENTLY\n users_email ON users (email);"
681 ));
682 assert!(runs_outside_transaction(
683 "create index\tconcurrently\tx ON t (a);"
684 ));
685 assert!(!runs_outside_transaction(
686 "CREATE TABLE concurrently_log (id INT);"
687 ));
688 }
689
690 async fn tables(db: &Db) -> Vec<String> {
691 let list = match db.dialect() {
692 Dialect::Sqlite => "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name",
693 Dialect::Postgres => {
694 "SELECT tablename AS name FROM pg_tables \
695 WHERE schemaname = current_schema() ORDER BY tablename"
696 }
697 };
698 sql(list)
699 .fetch_all(db)
700 .await
701 .unwrap()
702 .iter()
703 .map(|row| row.try_get::<String>("name").unwrap())
704 .collect()
705 }
706
707 #[tokio::test]
711 async fn scripts_outside_a_transaction_run_every_statement_both_ways() {
712 let db = super::super::connect(&crate::Config::default())
713 .await
714 .unwrap();
715 let outside = Migration::new(
716 "1_outside",
717 "-- renox:no-transaction\nCREATE TABLE a (id INT);\nCREATE TABLE b (id INT);",
718 Some("-- renox:no-transaction\nDROP TABLE b;\nDROP TABLE a;"),
719 )
720 .postgres(
721 "CREATE TABLE a (id INT);\nCREATE INDEX CONCURRENTLY a_id ON a (id);\nCREATE TABLE b (id INT);",
722 Some("DROP INDEX CONCURRENTLY a_id;\nDROP TABLE b;\nDROP TABLE a;"),
723 );
724 let comment = Migration::new("2_comment", "-- nothing to do yet\n", Some("-- nor here\n"));
725 let migrator = Migrator::new(vec![outside, comment]).unwrap();
726 assert_eq!(migrator.run(&db).await.unwrap(), ["1_outside", "2_comment"]);
727 let made = tables(&db).await;
728 assert!(
729 made.contains(&"a".to_owned()) && made.contains(&"b".to_owned()),
730 "{made:?}"
731 );
732 let undone = migrator.rollback(&db, 1).await.unwrap();
733 assert_eq!(undone, ["2_comment", "1_outside"]);
734 let left = tables(&db).await;
735 assert!(
736 !left.contains(&"a".to_owned()) && !left.contains(&"b".to_owned()),
737 "{left:?}"
738 );
739 }
740
741 #[tokio::test]
744 async fn an_old_migrations_table_gets_its_checksum_column() {
745 let db = super::super::connect(&crate::Config::default())
746 .await
747 .unwrap();
748 sql(format!(
749 "CREATE TABLE {TABLE} (name TEXT PRIMARY KEY NOT NULL, batch BIGINT NOT NULL, \
750 applied_at TEXT NOT NULL)"
751 ))
752 .execute(&db)
753 .await
754 .unwrap();
755 sql(format!(
756 "INSERT INTO {TABLE} (name, batch, applied_at) VALUES ('1_old', 1, 'then')"
757 ))
758 .execute(&db)
759 .await
760 .unwrap();
761 let old = Migration::new("1_old", "CREATE TABLE never (id INT);", None);
762 let new = Migration::new("2_new", "CREATE TABLE fresh (id INT);", None);
763 let migrator = Migrator::new(vec![old, new]).unwrap();
764 assert_eq!(migrator.run(&db).await.unwrap(), ["2_new"]);
765 let status = migrator.status(&db).await.unwrap();
766 assert_eq!(status.len(), 2);
767 assert!(!tables(&db).await.contains(&"never".to_owned()));
768 }
769
770 #[tokio::test]
774 async fn a_migration_recorded_meanwhile_is_skipped() {
775 let db = super::super::connect(&crate::Config::default())
776 .await
777 .unwrap();
778 let first = Migration::new(
779 "1_first",
780 "INSERT INTO renox_migrations (name, batch, applied_at) VALUES ('2_second', 1, 'meanwhile');",
781 None,
782 );
783 let second = Migration::new("2_second", "CREATE TABLE twice (id INT);", None);
784 let done = Migrator::new(vec![first, second])
785 .unwrap()
786 .run(&db)
787 .await
788 .unwrap();
789 assert_eq!(done, ["1_first"]);
790 assert!(!tables(&db).await.contains(&"twice".to_owned()));
791 }
792
793 #[tokio::test]
797 async fn pooled_connections_see_columns_added_by_a_migration() {
798 let dir = tempfile::tempdir().unwrap();
799 let config = crate::Config {
800 database_url: format!("sqlite://{}/app.db", dir.path().display()),
801 database_pool_size: 4,
802 ..crate::Config::default()
803 };
804 let db = super::super::connect(&config).await.unwrap();
805 let create = Migration::new(
806 "1_notes",
807 "CREATE TABLE notes (id INTEGER PRIMARY KEY, a TEXT);",
808 None,
809 );
810 Migrator::new(vec![create]).unwrap().run(&db).await.unwrap();
811 sql("INSERT INTO notes (a) VALUES ('x')")
812 .execute(&db)
813 .await
814 .unwrap();
815
816 let mut open = Vec::new();
818 for _ in 0..4 {
819 let mut tx = db.begin().await.unwrap();
820 sql("SELECT * FROM notes").fetch_all(&mut tx).await.unwrap();
821 open.push(tx);
822 }
823 drop(open);
824
825 let alter = Migration::new("2_notes_b", "ALTER TABLE notes ADD COLUMN b TEXT;", None);
826 Migrator::new(vec![create, alter])
827 .unwrap()
828 .run(&db)
829 .await
830 .unwrap();
831 for _ in 0..8 {
832 let rows = sql("SELECT * FROM notes").fetch_all(&db).await.unwrap();
833 assert_eq!(rows[0].try_get::<Option<String>>("b").unwrap(), None);
834 }
835 }
836}