Skip to main content

diesel_migrations/
file_based_migrations.rs

1use std::fmt::Display;
2use std::fs::{DirEntry, File};
3use std::io::Read;
4use std::path::{Path, PathBuf};
5
6use diesel::backend::Backend;
7use diesel::connection::BoxableConnection;
8use diesel::migration::{
9    self, Migration, MigrationMetadata, MigrationName, MigrationSource, MigrationVersion,
10};
11use migrations_internals::TomlMetadata;
12
13use crate::errors::{MigrationError, RunMigrationsError};
14
15/// A migration source based on a migration directory in the file system
16///
17/// A valid migration directory contains a sub folder per migration.
18/// Each migration folder contains a `up.sql` file containing the migration itself
19/// and a `down.sql` file containing the necessary SQL to revert the migration.
20///
21/// To embed an existing migration folder into the final binary see
22/// [`embed_migrations!`](crate::embed_migrations!).
23///
24/// ## Transactions
25///
26/// Each migration folder can additionally contain a `metadata.toml` file,
27/// controlling how the individual migration should be handled by the migration
28/// harness.
29///
30/// By default, each migration is run inside a dedicated transaction block.
31/// If the metadata file contains `run_in_transaction = false`, then this
32/// behavior will be disabled.
33///
34/// **Important:** If you see "cannot run inside a transaction block" errors
35/// despite having set `run_in_transaction = false`, then your migration likely
36/// contains multiple instructions, which some databases automatically wrap in
37/// a transaction block. In this case we recommend splitting the migration into
38/// multiple migrations.
39///
40/// ## Example
41///
42/// ```text
43/// # Directory Structure
44/// - 20151219180527_create_users
45///     - up.sql
46///     - down.sql
47/// - 20160107082941_create_posts
48///     - up.sql
49///     - down.sql
50///     - metadata.toml
51/// ```
52///
53/// ```sql
54/// -- 20151219180527_create_users/up.sql
55/// CREATE TABLE users (
56///   id SERIAL PRIMARY KEY,
57///   name VARCHAR NOT NULL,
58///   hair_color VARCHAR
59/// );
60/// ```
61///
62/// ```sql
63/// -- 20151219180527_create_users/down.sql
64/// DROP TABLE users;
65/// ```
66///
67/// ```sql
68/// -- 20160107082941_create_posts/up.sql
69/// CREATE TABLE posts (
70///   id SERIAL PRIMARY KEY,
71///   user_id INTEGER NOT NULL,
72///   title VARCHAR NOT NULL,
73///   body TEXT
74/// );
75/// ```
76///
77/// ```sql
78/// -- 20160107082941_create_posts/down.sql
79/// DROP TABLE posts;
80/// ```
81///
82/// ```toml
83/// ## 20160107082941_create_posts/metadata.toml
84///
85/// ## specifies if a migration is executed inside a
86/// ## transaction or not. This configuration is optional
87/// ## by default all migrations are run in transactions.
88/// ##
89/// ## For certain types of migrations, like creating an
90/// ## index onto a existing column, it is required
91/// ## to set this to false
92/// run_in_transaction = true
93/// ```
94#[derive(#[automatically_derived]
impl ::core::clone::Clone for FileBasedMigrations {
    #[inline]
    fn clone(&self) -> FileBasedMigrations {
        FileBasedMigrations {
            base_path: ::core::clone::Clone::clone(&self.base_path),
        }
    }
}Clone)]
95pub struct FileBasedMigrations {
96    base_path: PathBuf,
97}
98
99impl FileBasedMigrations {
100    /// Create a new file based migration source based on a specific path
101    ///
102    /// This methods fails if the path passed as argument is no valid migration directory
103    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, MigrationError> {
104        for dir in migrations_directories(path.as_ref())? {
105            let path = dir?.path();
106            if !migrations_internals::valid_sql_migration_directory(&path) {
107                return Err(MigrationError::UnknownMigrationFormat(path));
108            }
109        }
110        Ok(Self {
111            base_path: path.as_ref().to_path_buf(),
112        })
113    }
114
115    /// Create a new file based migration source by searching the migration directory
116    ///
117    /// This method looks in the current and all parent directories for a folder named
118    /// `migrations`
119    ///
120    /// This method fails if no valid migration directory is found
121    pub fn find_migrations_directory() -> Result<Self, MigrationError> {
122        Self::find_migrations_directory_in_path(std::env::current_dir()?.as_path())
123    }
124
125    /// Create a new file based migration source by searching a given path for the migration
126    /// directory
127    ///
128    /// This method looks in the passed directory and all parent directories for a folder
129    /// named `migrations`
130    ///
131    /// This method fails if no valid migration directory is found
132    pub fn find_migrations_directory_in_path(
133        path: impl AsRef<Path>,
134    ) -> Result<Self, MigrationError> {
135        let migrations_directory = search_for_migrations_directory(path.as_ref())?;
136        Self::from_path(migrations_directory.as_path())
137    }
138
139    #[doc(hidden)]
140    pub fn path(&self) -> &Path {
141        &self.base_path
142    }
143}
144
145fn search_for_migrations_directory(path: &Path) -> Result<PathBuf, MigrationError> {
146    migrations_internals::search_for_migrations_directory(path)
147        .ok_or_else(|| MigrationError::MigrationDirectoryNotFound(path.to_path_buf()))
148}
149
150fn migrations_directories(
151    path: &'_ Path,
152) -> Result<impl Iterator<Item = Result<DirEntry, MigrationError>> + '_, MigrationError> {
153    Ok(migrations_internals::migrations_directories(path)?.map(move |e| e.map_err(Into::into)))
154}
155
156fn migrations_in_directory(
157    path: &'_ Path,
158) -> Result<impl Iterator<Item = Result<SqlFileMigration, MigrationError>> + '_, MigrationError> {
159    Ok(migrations_directories(path)?.map(|entry| SqlFileMigration::from_path(&entry?.path())))
160}
161
162impl<DB: Backend> MigrationSource<DB> for FileBasedMigrations {
163    fn migrations(&self) -> migration::Result<Vec<Box<dyn Migration<DB>>>> {
164        migrations_in_directory(&self.base_path)?
165            .map(|r| Ok(Box::new(r?) as Box<dyn Migration<DB>>))
166            .collect()
167    }
168}
169
170struct SqlFileMigration {
171    base_path: PathBuf,
172    metadata: TomlMetadataWrapper,
173    name: DieselMigrationName,
174}
175
176impl SqlFileMigration {
177    fn from_path(path: &Path) -> Result<Self, MigrationError> {
178        if migrations_internals::valid_sql_migration_directory(path) {
179            let metadata = TomlMetadataWrapper(
180                TomlMetadata::read_from_file(&path.join("metadata.toml")).unwrap_or_default(),
181            );
182            Ok(Self {
183                base_path: path.to_path_buf(),
184                metadata,
185                name: DieselMigrationName::from_path(path)?,
186            })
187        } else {
188            Err(MigrationError::UnknownMigrationFormat(path.to_path_buf()))
189        }
190    }
191}
192
193impl<DB: Backend> Migration<DB> for SqlFileMigration {
194    fn run(&self, conn: &mut dyn BoxableConnection<DB>) -> migration::Result<()> {
195        Ok(run_sql_from_file(
196            conn,
197            &self.base_path.join("up.sql"),
198            &self.name,
199        )?)
200    }
201
202    fn revert(&self, conn: &mut dyn BoxableConnection<DB>) -> migration::Result<()> {
203        let down_path = self.base_path.join("down.sql");
204        if #[allow(non_exhaustive_omitted_patterns)] match down_path.metadata() {
    Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
    _ => false,
}matches!(down_path.metadata(), Err(e) if e.kind() == std::io::ErrorKind::NotFound) {
205            Err(MigrationError::NoMigrationRevertFile.into())
206        } else {
207            Ok(run_sql_from_file(conn, &down_path, &self.name)?)
208        }
209    }
210
211    fn metadata(&self) -> &dyn MigrationMetadata {
212        &self.metadata
213    }
214
215    fn name(&self) -> &dyn MigrationName {
216        &self.name
217    }
218}
219
220#[derive(#[automatically_derived]
impl ::core::fmt::Debug for DieselMigrationName {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "DieselMigrationName", "name", &self.name, "version",
            &&self.version)
    }
}Debug, #[automatically_derived]
impl ::core::cmp::PartialEq for DieselMigrationName {
    #[inline]
    fn eq(&self, other: &DieselMigrationName) -> bool {
        self.name == other.name && self.version == other.version
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for DieselMigrationName {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<String>;
        let _: ::core::cmp::AssertParamIsEq<MigrationVersion<'static>>;
    }
}Eq)]
221pub struct DieselMigrationName {
222    name: String,
223    version: MigrationVersion<'static>,
224}
225
226impl Clone for DieselMigrationName {
227    fn clone(&self) -> Self {
228        Self {
229            name: self.name.clone(),
230            version: self.version.as_owned(),
231        }
232    }
233}
234
235impl DieselMigrationName {
236    fn from_path(path: &Path) -> Result<Self, MigrationError> {
237        let name = path
238            .file_name()
239            .ok_or_else(|| MigrationError::UnknownMigrationFormat(path.to_path_buf()))?
240            .to_string_lossy();
241        Self::from_name(&name)
242    }
243
244    pub(crate) fn from_name(name: &str) -> Result<Self, MigrationError> {
245        let version = migrations_internals::version_from_string(name)
246            .ok_or_else(|| MigrationError::UnknownMigrationFormat(PathBuf::from(name)))?;
247        Ok(Self {
248            name: name.to_owned(),
249            version: MigrationVersion::from(version),
250        })
251    }
252}
253
254impl MigrationName for DieselMigrationName {
255    fn version(&self) -> MigrationVersion<'_> {
256        self.version.as_owned()
257    }
258}
259
260impl Display for DieselMigrationName {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        f.write_fmt(format_args!("{0}", self.name))write!(f, "{}", self.name)
263    }
264}
265
266#[derive(#[automatically_derived]
impl ::core::default::Default for TomlMetadataWrapper {
    #[inline]
    fn default() -> TomlMetadataWrapper {
        TomlMetadataWrapper(::core::default::Default::default())
    }
}Default)]
267#[doc(hidden)]
268pub struct TomlMetadataWrapper(TomlMetadata);
269
270impl TomlMetadataWrapper {
271    #[doc(hidden)]
272    pub const fn new(run_in_transaction: bool) -> Self {
273        Self(TomlMetadata::new(run_in_transaction))
274    }
275}
276
277impl MigrationMetadata for TomlMetadataWrapper {
278    fn run_in_transaction(&self) -> bool {
279        self.0.run_in_transaction
280    }
281}
282
283fn run_sql_from_file<DB: Backend>(
284    conn: &mut dyn BoxableConnection<DB>,
285    path: &Path,
286    name: &DieselMigrationName,
287) -> Result<(), RunMigrationsError> {
288    let map_io_err = |e| RunMigrationsError::MigrationError(name.clone(), MigrationError::from(e));
289
290    let mut sql = String::new();
291    let mut file = File::open(path).map_err(map_io_err)?;
292    file.read_to_string(&mut sql).map_err(map_io_err)?;
293
294    if sql.is_empty() {
295        return Err(RunMigrationsError::EmptyMigration(name.clone()));
296    }
297
298    conn.batch_execute(&sql)
299        .map_err(|e| RunMigrationsError::QueryError(name.clone(), e))?;
300    Ok(())
301}
302
303#[cfg(test)]
304mod tests {
305    extern crate tempfile;
306
307    use super::*;
308
309    use self::tempfile::Builder;
310    use std::fs;
311
312    #[test]
313    fn migration_directory_not_found_if_no_migration_dir_exists() {
314        let dir = Builder::new().prefix("diesel").tempdir().unwrap();
315
316        assert_eq!(
317            Err(MigrationError::MigrationDirectoryNotFound(
318                dir.path().into()
319            )),
320            search_for_migrations_directory(dir.path())
321        );
322    }
323
324    #[test]
325    fn migration_directory_defaults_to_pwd_slash_migrations() {
326        let dir = Builder::new().prefix("diesel").tempdir().unwrap();
327        let temp_path = dir.path().canonicalize().unwrap();
328        let migrations_path = temp_path.join("migrations");
329
330        fs::create_dir(&migrations_path).unwrap();
331
332        assert_eq!(
333            Ok(migrations_path),
334            search_for_migrations_directory(&temp_path)
335        );
336    }
337
338    #[test]
339    fn migration_directory_checks_parents() {
340        let dir = Builder::new().prefix("diesel").tempdir().unwrap();
341        let temp_path = dir.path().canonicalize().unwrap();
342        let migrations_path = temp_path.join("migrations");
343        let child_path = temp_path.join("child");
344
345        fs::create_dir(&child_path).unwrap();
346        fs::create_dir(&migrations_path).unwrap();
347
348        assert_eq!(
349            Ok(migrations_path),
350            search_for_migrations_directory(&child_path)
351        );
352    }
353
354    #[test]
355    fn migration_paths_in_directory_ignores_files() {
356        let dir = Builder::new().prefix("diesel").tempdir().unwrap();
357        let temp_path = dir.path().canonicalize().unwrap();
358        let migrations_path = temp_path.join("migrations");
359        let file_path = migrations_path.join("README.md");
360
361        fs::create_dir(migrations_path.as_path()).unwrap();
362        fs::File::create(file_path.as_path()).unwrap();
363
364        let migrations = migrations_in_directory(&migrations_path)
365            .unwrap()
366            .collect::<Result<Vec<_>, _>>()
367            .unwrap();
368
369        assert_eq!(0, migrations.len());
370    }
371
372    #[test]
373    fn migration_paths_in_directory_ignores_dot_directories() {
374        let dir = Builder::new().prefix("diesel").tempdir().unwrap();
375        let temp_path = dir.path().canonicalize().unwrap();
376        let migrations_path = temp_path.join("migrations");
377        let dot_path = migrations_path.join(".hidden_dir");
378
379        fs::create_dir(migrations_path.as_path()).unwrap();
380        fs::create_dir(dot_path.as_path()).unwrap();
381
382        let migrations = migrations_in_directory(&migrations_path)
383            .unwrap()
384            .collect::<Result<Vec<_>, _>>()
385            .unwrap();
386
387        assert_eq!(0, migrations.len());
388    }
389
390    #[test]
391    fn migration_paths_in_directory_ignores_empty_directories() {
392        let dir = Builder::new().prefix("diesel").tempdir().unwrap();
393        let temp_path = dir.path().canonicalize().unwrap();
394        let migrations_path = temp_path.join("migrations");
395        let empty_path = migrations_path.join("an_empty_migration_dir");
396
397        fs::create_dir(migrations_path.as_path()).unwrap();
398        fs::create_dir(empty_path.as_path()).unwrap();
399
400        let migrations = migrations_in_directory(&migrations_path)
401            .unwrap()
402            .collect::<Result<Vec<_>, _>>()
403            .unwrap();
404
405        assert_eq!(0, migrations.len());
406    }
407}