Skip to content

Commit 044bcac

Browse files
committed
fix(migrations) :: failed migrations now report the SQL filename
Errors from a failed migration now point at the file and the SQL that failed. Before a reversible migration could be reported with the contents of its `.down.sql` half, and any migration with a multi-word name was reported as `0001_add new users.sql` rather than `0001_add_new_users.sql`.
1 parent 8bdd3c1 commit 044bcac

4 files changed

Lines changed: 103 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
```
1414

1515
SQLPage now keeps the variable value, producing `https://api.example.com/john.doe` as expected.
16+
- Errors from a failed migration now point at the file and the SQL that failed. Before a reversible migration could be reported with the contents of its `.down.sql` half, and any migration with a multi-word name was reported as `0001_add new users.sql` rather than `0001_add_new_users.sql`.
1617

1718
## v0.46
1819

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ lambda-web = [
140140

141141
[dev-dependencies]
142142
actix-http = "3"
143+
tempfile = "3"
143144
tokio = { version = "1", features = ["rt", "time", "test-util"] }
144145

145146
[build-dependencies]

src/webserver/database/migrations.rs

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@ pub async fn apply(config: &crate::app_config::AppConfig, db: &Database) -> anyh
3737
migrator.run(&db.connection).await.map_err(|err| {
3838
match err {
3939
MigrateError::Execute(n, source) => {
40-
let migration = migrator.iter().find(|&m| m.version == n).unwrap();
41-
let source_file =
42-
migrations_dir.join(format!("{:04}_{}.sql", n, migration.description));
40+
let migration = failing_migration(&migrator.migrations, n)
41+
.expect("sqlx reports the version of a migration it just ran");
42+
let source_file = migrations_dir.join(format!(
43+
"{:04}_{}{}",
44+
n,
45+
migration.description.replace(' ', "_"),
46+
migration.migration_type.suffix()
47+
));
4348
display_db_error(&source_file, &migration.sql, source).context(format!(
4449
"Failed to apply {} migration {}",
4550
db,
@@ -55,6 +60,12 @@ pub async fn apply(config: &crate::app_config::AppConfig, db: &Database) -> anyh
5560
Ok(())
5661
}
5762

63+
fn failing_migration(migrations: &[Migration], version: i64) -> Option<&Migration> {
64+
migrations
65+
.iter()
66+
.find(|m| m.version == version && !m.migration_type.is_down_migration())
67+
}
68+
5869
struct DisplayMigration<'a>(&'a Migration);
5970

6071
impl std::fmt::Display for DisplayMigration<'_> {
@@ -83,3 +94,89 @@ fn migration_err(operation: &'static str) -> String {
8394
The current state of migrations will be stored in a table called _sqlx_migrations."
8495
)
8596
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use super::*;
101+
use sqlx::migrate::MigrationType;
102+
use tempfile::TempDir;
103+
104+
fn migration(version: i64, description: &str, migration_type: MigrationType) -> Migration {
105+
Migration::new(
106+
version,
107+
description.to_owned().into(),
108+
migration_type,
109+
String::new().into(),
110+
)
111+
}
112+
113+
async fn apply_error(files: &[(&str, &str)]) -> String {
114+
let dir = TempDir::new().unwrap();
115+
let migrations_dir = dir.path().join(MIGRATIONS_DIR);
116+
std::fs::create_dir(&migrations_dir).unwrap();
117+
for (name, sql) in files {
118+
std::fs::write(migrations_dir.join(name), sql).unwrap();
119+
}
120+
let mut config = crate::app_config::tests::test_config();
121+
config.database_url = "sqlite::memory:".to_owned();
122+
config.configuration_directory = dir.path().to_owned();
123+
let db = Database::init(&config).await.unwrap();
124+
let error = apply(&config, &db)
125+
.await
126+
.expect_err("the migration must fail");
127+
format!("{error:#}")
128+
}
129+
130+
#[actix_web::test]
131+
async fn only_the_failing_migration_is_named() {
132+
let error = apply_error(&[
133+
("0001_ok.sql", "CREATE TABLE t(x);"),
134+
("0002_bad_thing.sql", "SELECT * FROM does_not_exist;"),
135+
])
136+
.await;
137+
assert!(error.contains("[0002] bad thing"), "{error}");
138+
assert!(error.contains("0002_bad_thing.sql"), "{error}");
139+
assert!(error.contains("does_not_exist"), "{error}");
140+
assert!(!error.contains("[0001] ok"), "{error}");
141+
}
142+
143+
#[actix_web::test]
144+
async fn a_reversible_migration_reports_the_half_that_ran() {
145+
let error = apply_error(&[
146+
("0001_add_new_users.up.sql", "SELECT * FROM does_not_exist;"),
147+
("0001_add_new_users.down.sql", "SELECT 'the down half';"),
148+
])
149+
.await;
150+
assert!(
151+
error.contains("[0001] (ReversibleUp) add new users"),
152+
"{error}"
153+
);
154+
assert!(error.contains("0001_add_new_users.up.sql"), "{error}");
155+
assert!(!error.contains("the down half"), "{error}");
156+
}
157+
158+
#[test]
159+
fn the_down_half_is_never_the_failing_migration() {
160+
let up = migration(1, "x", MigrationType::ReversibleUp);
161+
let down = migration(1, "x", MigrationType::ReversibleDown);
162+
for pair in [[up.clone(), down.clone()], [down.clone(), up.clone()]] {
163+
assert_eq!(
164+
failing_migration(&pair, 1).map(|m| m.migration_type),
165+
Some(MigrationType::ReversibleUp)
166+
);
167+
}
168+
assert!(failing_migration(&[down], 1).is_none());
169+
}
170+
171+
#[test]
172+
fn display_migration_shows_version_description_and_reversibility() {
173+
assert_eq!(
174+
DisplayMigration(&migration(7, "add users", MigrationType::Simple)).to_string(),
175+
"[0007] add users"
176+
);
177+
assert_eq!(
178+
DisplayMigration(&migration(7, "add users", MigrationType::ReversibleUp)).to_string(),
179+
"[0007] (ReversibleUp) add users"
180+
);
181+
}
182+
}

0 commit comments

Comments
 (0)