Skip to content

Commit d005111

Browse files
committed
fix: better I/O errors when migrate!() cannot read a file
1 parent 24be262 commit d005111

File tree

3 files changed

+154
-121
lines changed

3 files changed

+154
-121
lines changed

sqlx-core/src/migrate/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ pub use migration::{AppliedMigration, Migration};
1212
pub use migration_type::MigrationType;
1313
pub use migrator::Migrator;
1414
pub use source::MigrationSource;
15+
16+
#[doc(hidden)]
17+
pub use source::resolve_blocking;

sqlx-core/src/migrate/source.rs

Lines changed: 102 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::error::BoxDynError;
2-
use crate::fs;
32
use crate::migrate::{Migration, MigrationType};
43
use futures_core::future::BoxFuture;
54

65
use std::borrow::Cow;
76
use std::fmt::Debug;
7+
use std::fs;
8+
use std::io;
89
use std::path::{Path, PathBuf};
910

1011
/// In the default implementation, a MigrationSource is a directory which
@@ -28,51 +29,11 @@ pub trait MigrationSource<'s>: Debug {
2829
impl<'s> MigrationSource<'s> for &'s Path {
2930
fn resolve(self) -> BoxFuture<'s, Result<Vec<Migration>, BoxDynError>> {
3031
Box::pin(async move {
31-
let mut s = fs::read_dir(self.canonicalize()?).await?;
32-
let mut migrations = Vec::new();
33-
34-
while let Some(entry) = s.next().await? {
35-
// std::fs::metadata traverses symlinks
36-
if !std::fs::metadata(&entry.path)?.is_file() {
37-
// not a file; ignore
38-
continue;
39-
}
40-
41-
let file_name = entry.file_name.to_string_lossy();
42-
43-
let parts = file_name.splitn(2, '_').collect::<Vec<_>>();
44-
45-
if parts.len() != 2 || !parts[1].ends_with(".sql") {
46-
// not of the format: <VERSION>_<DESCRIPTION>.sql; ignore
47-
continue;
48-
}
49-
50-
let version: i64 = parts[0].parse()
51-
.map_err(|_e| {
52-
format!("error parsing migration filename {file_name:?}; expected integer version prefix (e.g. `01_foo.sql`)")
53-
})?;
54-
55-
let migration_type = MigrationType::from_filename(parts[1]);
56-
// remove the `.sql` and replace `_` with ` `
57-
let description = parts[1]
58-
.trim_end_matches(migration_type.suffix())
59-
.replace('_', " ")
60-
.to_owned();
61-
62-
let sql = fs::read_to_string(&entry.path).await?;
63-
64-
migrations.push(Migration::new(
65-
version,
66-
Cow::Owned(description),
67-
migration_type,
68-
Cow::Owned(sql),
69-
));
70-
}
71-
72-
// ensure that we are sorted by `VERSION ASC`
73-
migrations.sort_by_key(|m| m.version);
74-
75-
Ok(migrations)
32+
let canonical = self.canonicalize()?;
33+
let migrations_with_paths =
34+
crate::rt::spawn_blocking(move || resolve_blocking(canonical)).await?;
35+
36+
Ok(migrations_with_paths.into_iter().map(|(m, _p)| m).collect())
7637
})
7738
}
7839
}
@@ -82,3 +43,98 @@ impl MigrationSource<'static> for PathBuf {
8243
Box::pin(async move { self.as_path().resolve().await })
8344
}
8445
}
46+
47+
#[derive(thiserror::Error, Debug)]
48+
#[error("{message}")]
49+
pub struct ResolveError {
50+
message: String,
51+
#[source]
52+
source: Option<io::Error>,
53+
}
54+
55+
// FIXME: paths should just be part of `Migration` but we can't add a field backwards compatibly
56+
// since it's `#[non_exhaustive]`.
57+
pub fn resolve_blocking(path: PathBuf) -> Result<Vec<(Migration, PathBuf)>, ResolveError> {
58+
let mut s = fs::read_dir(&path).map_err(|e| ResolveError {
59+
message: format!("error reading migration directory {}: {e}", path.display()),
60+
source: Some(e),
61+
})?;
62+
63+
let mut migrations = Vec::new();
64+
65+
while let Some(res) = s.next() {
66+
let entry = res.map_err(|e| ResolveError {
67+
message: format!(
68+
"error reading contents of migration directory {}: {e}",
69+
path.display()
70+
),
71+
source: Some(e),
72+
})?;
73+
74+
let entry_path = entry.path();
75+
76+
let metadata = fs::metadata(&entry_path).map_err(|e| ResolveError {
77+
message: format!(
78+
"error getting metadata of migration path {}",
79+
entry_path.display()
80+
),
81+
source: Some(e),
82+
})?;
83+
84+
if !metadata.is_file() {
85+
// not a file; ignore
86+
continue;
87+
}
88+
89+
let file_name = entry.file_name();
90+
// This is arguably the wrong choice,
91+
// but it really only matters for parsing the version and description.
92+
//
93+
// Using `.to_str()` and returning an error if the filename is not UTF-8
94+
// would be a breaking change.
95+
let file_name = file_name.to_string_lossy();
96+
97+
let parts = file_name.splitn(2, '_').collect::<Vec<_>>();
98+
99+
if parts.len() != 2 || !parts[1].ends_with(".sql") {
100+
// not of the format: <VERSION>_<DESCRIPTION>.sql; ignore
101+
continue;
102+
}
103+
104+
let version: i64 = parts[0].parse()
105+
.map_err(|_e| ResolveError {
106+
message: format!("error parsing migration filename {file_name:?}; expected integer version prefix (e.g. `01_foo.sql`)"),
107+
source: None,
108+
})?;
109+
110+
let migration_type = MigrationType::from_filename(parts[1]);
111+
// remove the `.sql` and replace `_` with ` `
112+
let description = parts[1]
113+
.trim_end_matches(migration_type.suffix())
114+
.replace('_', " ")
115+
.to_owned();
116+
117+
let sql = fs::read_to_string(&entry_path).map_err(|e| ResolveError {
118+
message: format!(
119+
"error reading contents of migration {}: {e}",
120+
entry_path.display()
121+
),
122+
source: Some(e),
123+
})?;
124+
125+
migrations.push((
126+
Migration::new(
127+
version,
128+
Cow::Owned(description),
129+
migration_type,
130+
Cow::Owned(sql),
131+
),
132+
entry_path,
133+
));
134+
}
135+
136+
// Ensure that we are sorted by version in ascending order.
137+
migrations.sort_by_key(|(m, _)| m.version);
138+
139+
Ok(migrations)
140+
}

sqlx-macros-core/src/migrate.rs

Lines changed: 49 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))]
22
extern crate proc_macro;
33

4+
use std::path::{Path, PathBuf};
5+
46
use proc_macro2::TokenStream;
57
use quote::{quote, ToTokens, TokenStreamExt};
6-
use sha2::{Digest, Sha384};
7-
use sqlx_core::migrate::MigrationType;
8-
use std::fs;
9-
use std::path::Path;
108
use syn::LitStr;
119

12-
pub struct QuotedMigrationType(MigrationType);
10+
use sqlx_core::migrate::{Migration, MigrationType};
11+
12+
pub struct QuoteMigrationType(MigrationType);
1313

14-
impl ToTokens for QuotedMigrationType {
14+
impl ToTokens for QuoteMigrationType {
1515
fn to_tokens(&self, tokens: &mut TokenStream) {
1616
let ts = match self.0 {
1717
MigrationType::Simple => quote! { ::sqlx::migrate::MigrationType::Simple },
@@ -24,31 +24,51 @@ impl ToTokens for QuotedMigrationType {
2424
}
2525
}
2626

27-
struct QuotedMigration {
28-
version: i64,
29-
description: String,
30-
migration_type: QuotedMigrationType,
31-
path: String,
32-
checksum: Vec<u8>,
27+
struct QuoteMigration {
28+
migration: Migration,
29+
path: PathBuf,
3330
}
3431

35-
impl ToTokens for QuotedMigration {
32+
impl ToTokens for QuoteMigration {
3633
fn to_tokens(&self, tokens: &mut TokenStream) {
37-
let QuotedMigration {
34+
let Migration {
3835
version,
3936
description,
4037
migration_type,
41-
path,
4238
checksum,
43-
} = &self;
39+
..
40+
} = &self.migration;
41+
42+
let migration_type = QuoteMigrationType(*migration_type);
43+
44+
let sql = self
45+
.path
46+
.canonicalize()
47+
.map_err(|e| {
48+
format!(
49+
"error canonicalizing migration path {}: {e}",
50+
self.path.display()
51+
)
52+
})
53+
.and_then(|path| {
54+
let path_str = path.to_str().ok_or_else(|| {
55+
format!(
56+
"migration path cannot be represented as a string: {}",
57+
self.path.display()
58+
)
59+
})?;
60+
61+
// this tells the compiler to watch this path for changes
62+
Ok(quote! { include_str!(#path_str) })
63+
})
64+
.unwrap_or_else(|e| quote! { compile_error!(#e) });
4465

4566
let ts = quote! {
4667
::sqlx::migrate::Migration {
4768
version: #version,
4869
description: ::std::borrow::Cow::Borrowed(#description),
4970
migration_type: #migration_type,
50-
// this tells the compiler to watch this path for changes
51-
sql: ::std::borrow::Cow::Borrowed(include_str!(#path)),
71+
sql: ::std::borrow::Cow::Borrowed(#sql),
5272
checksum: ::std::borrow::Cow::Borrowed(&[
5373
#(#checksum),*
5474
]),
@@ -59,7 +79,6 @@ impl ToTokens for QuotedMigration {
5979
}
6080
}
6181

62-
// mostly copied from sqlx-core/src/migrate/source.rs
6382
pub fn expand_migrator_from_lit_dir(dir: LitStr) -> crate::Result<TokenStream> {
6483
expand_migrator_from_dir(&dir.value(), dir.span())
6584
}
@@ -74,65 +93,20 @@ pub(crate) fn expand_migrator_from_dir(
7493
}
7594

7695
pub(crate) fn expand_migrator(path: &Path) -> crate::Result<TokenStream> {
77-
let mut migrations = Vec::new();
78-
79-
for entry in fs::read_dir(&path)? {
80-
let entry = entry?;
81-
if !fs::metadata(entry.path())?.is_file() {
82-
// not a file; ignore
83-
continue;
84-
}
85-
86-
let file_name = entry.file_name();
87-
let file_name = file_name.to_string_lossy();
88-
89-
let parts = file_name.splitn(2, '_').collect::<Vec<_>>();
90-
91-
if parts.len() != 2 || !parts[1].ends_with(".sql") {
92-
// not of the format: <VERSION>_<DESCRIPTION>.sql; ignore
93-
continue;
94-
}
95-
96-
let version: i64 = parts[0].parse()?;
97-
98-
let migration_type = MigrationType::from_filename(parts[1]);
99-
// remove the `.sql` and replace `_` with ` `
100-
let description = parts[1]
101-
.trim_end_matches(migration_type.suffix())
102-
.replace('_', " ")
103-
.to_owned();
104-
105-
let sql = fs::read_to_string(&entry.path())?;
106-
107-
let checksum = Vec::from(Sha384::digest(sql.as_bytes()).as_slice());
108-
109-
// canonicalize the path so we can pass it to `include_str!()`
110-
let path = entry.path().canonicalize()?;
111-
let path = path
112-
.to_str()
113-
.ok_or_else(|| {
114-
format!(
115-
"migration path cannot be represented as a string: {:?}",
116-
path
117-
)
118-
})?
119-
.to_owned();
120-
121-
migrations.push(QuotedMigration {
122-
version,
123-
description,
124-
migration_type: QuotedMigrationType(migration_type),
125-
path,
126-
checksum,
127-
})
128-
}
129-
130-
// ensure that we are sorted by `VERSION ASC`
131-
migrations.sort_by_key(|m| m.version);
96+
let path = path.canonicalize().map_err(|e| {
97+
format!(
98+
"error canonicalizing migration directory {}: {e}",
99+
path.display()
100+
)
101+
})?;
102+
103+
// Use the same code path to resolve migrations at compile time and runtime.
104+
let migrations = sqlx_core::migrate::resolve_blocking(path)?
105+
.into_iter()
106+
.map(|(migration, path)| QuoteMigration { migration, path });
132107

133108
#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))]
134109
{
135-
let path = path.canonicalize()?;
136110
let path = path.to_str().ok_or_else(|| {
137111
format!(
138112
"migration directory path cannot be represented as a string: {:?}",

0 commit comments

Comments
 (0)