Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions rusqlite_migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ mod builder;
pub use builder::MigrationsBuilder;

mod errors;
pub mod validations;

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -897,6 +898,9 @@ impl<'m> Migrations<'m> {
/// Run upward migrations on a temporary in-memory database from first to last, one by one.
/// Convenience method for testing.
///
/// See the [`validations`] module if you want to validate other things as well, like downward
/// migrations.
///
Copy link
Owner Author

@cljoly cljoly May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also change this to use the validation module, so the logic is not duplicated.

/// # Example
///
/// ```
Expand Down
2 changes: 1 addition & 1 deletion rusqlite_migration/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
mod builder;

mod core;
mod helpers;
pub(crate) mod helpers;
185 changes: 185 additions & 0 deletions rusqlite_migration/src/validations/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Clément Joly and contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Run a more complete set of validations (like requiring a downward migration to be present and
//! to apply cleanly). This is useful in a unit test, to validate the migrations.
//!
//! See also [`Migrations::validate`] for simple cases.
//!
//! # Example
//!
//! ```
//! #[cfg(test)]
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this and the test annotation here and in the validate() example.

//! mod tests {
//!
//! // … Other tests …
//!
//! #[test]
//! fn migrations_test() -> Result<(), dyn Error> {
//! Validations::everything().validate(migrations)?;
//! }
//! }
//! ```

use std::fmt::Display;

use rusqlite::Connection;

use super::Migrations;

#[cfg(test)]
mod tests;

/// Result for validations
pub type Result<'m, T, E = Error> = std::result::Result<T, E>;

/// Enum of possible validation errors.
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub enum Error {
/// Downward migrations were required for every upward migrations, but some are missing.
MissingDownwardMigrations(Vec<(usize, String)>),
/// Underlying rusqlite_migration error.
RusqliteMigration(crate::Error),
}

impl From<crate::Error> for Error {
fn from(value: crate::Error) -> Self {
Error::RusqliteMigration(value)
}
}

impl From<rusqlite::Error> for Error {
fn from(value: rusqlite::Error) -> Self {
Error::from(crate::Error::from(value))
}
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::MissingDownwardMigrations(_) => None,
Error::RusqliteMigration(error) => Some(error),
}
}
}

impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::RusqliteMigration(e) => write!(f, "underlying rusqlite migration error: {e}"),
Error::MissingDownwardMigrations(vs) => {
write!(
f,
"the following migrations do not have a corresponding downward migration: "
)?;
for (i, v) in vs {
write!(f, "{i}: {v}, ")?
}
Ok(())
}
}
}
}

#[derive(PartialEq, Debug)]
enum DownwardCheck {
No,
IfPresent,
Required,
}

/// Opt-in checks to validate migrations
pub struct Validations {
downward: DownwardCheck,
}

impl Validations {
/// Apply all possible checks, in their strictest setting. Please note that future versions of
/// the library will add more checks and so this might cause tests to fail when you upgrade the
/// library.
pub fn everything() -> Self {
Self {
downward: DownwardCheck::Required,
}
}

/// Always validate upward migrations
pub fn upward() -> Self {
Self {
downward: DownwardCheck::No,
}
}

/// Validate all downwards migrations found. Allow a downward migration to be missing.
pub fn check_downward_if_present(mut self) -> Self {
self.downward = DownwardCheck::IfPresent;
self
}

/// Validate all downwards migrations found. Error if a downward migration is missing.
pub fn require_downward(mut self) -> Self {
self.downward = DownwardCheck::Required;
self
}

/// Run the validations
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Place the example here instead of at the root of the module?

pub fn validate(&self, migrations: &Migrations) -> Result<()> {
// Let’s have all fields in scope, to ensure we don’t forgot to use any flags (or any
// future flags)
let Self { downward } = self;
let mut conn = Connection::open_in_memory()?;
let nbr_migrations = migrations.pending_migrations(&conn)? as usize;
if nbr_migrations == 0 {
log::debug!("no migrations defined, they are deemed valid");
return Ok(());
}

// https://mutants.rs/skip_calls.html#with_capacity
let mut missing_downward_migrations =
Vec::with_capacity(if *downward == DownwardCheck::Required {
nbr_migrations
} else {
0
});

// Always check upward migrations and check downward ones depending on flags
for i in 1..=nbr_migrations {
log::debug!("Checking migration number {i}");
migrations.to_version(&mut conn, i)?;
match downward {
DownwardCheck::No => (),
DownwardCheck::Required | DownwardCheck::IfPresent => {
if migrations.ms[i - 1].down.is_some() {
// Revert and reapply, to see if the revert applies cleanly
migrations.to_version(&mut conn, i - 1)?;
migrations.to_version(&mut conn, i)?;
} else if *downward == DownwardCheck::Required {
let m = &migrations.ms[i - 1];
missing_downward_migrations.push((i, format!("{m:?}")))
}
}
};
}

if missing_downward_migrations.is_empty() {
Ok(())
} else {
Err(Error::MissingDownwardMigrations(
missing_downward_migrations,
))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: rusqlite_migration/src/validations/tests.rs
expression: "Validations::everything().validate(&migrations).unwrap_err()"
snapshot_kind: text
---
the following migrations do not have a corresponding downward migration: 1: M { up: "CREATE TABLE m1(a, b); CREATE TABLE m2(a, b, c);", up_hook: None, down: None, down_hook: None, foreign_key_check: false, comment: None },
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
source: rusqlite_migration/src/validations/tests.rs
expression: v
snapshot_kind: text
---
Err(
RusqliteMigration(
RusqliteError {
query: "Invalid sql",
err: SqliteFailure(
Error {
code: Unknown,
extended_code: 1,
},
Some(
"near \"Invalid\": syntax error",
),
),
},
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: rusqlite_migration/src/validations/tests.rs
expression: "Validations::everything().validate(&migrations)"
snapshot_kind: text
---
Err(
MissingDownwardMigrations(
[
(
6,
"M { up: \"\\n CREATE TABLE fk1(a PRIMARY KEY);\\n CREATE TABLE fk2(\\n a,\\n FOREIGN KEY(a) REFERENCES fk1(a)\\n );\\n INSERT INTO fk1 (a) VALUES ('foo');\\n INSERT INTO fk2 (a) VALUES ('foo');\\n \", up_hook: None, down: None, down_hook: None, foreign_key_check: true, comment: None }",
),
],
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: rusqlite_migration/src/validations/tests.rs
expression: "Validations::everything().validate(&migrations)"
snapshot_kind: text
---
Err(
MissingDownwardMigrations(
[
(
4,
"M { up: \"CREATE TABLE t2(b);\", up_hook: None, down: None, down_hook: None, foreign_key_check: false, comment: None }",
),
],
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: rusqlite_migration/src/validations/tests.rs
expression: "Validations::everything().validate(&migrations)"
snapshot_kind: text
---
Err(
MissingDownwardMigrations(
[
(
1,
"M { up: \"CREATE TABLE m1(a, b); CREATE TABLE m2(a, b, c);\", up_hook: None, down: None, down_hook: None, foreign_key_check: false, comment: None }",
),
],
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: rusqlite_migration/src/validations/tests.rs
expression: "Validations::everything().validate(&migrations)"
snapshot_kind: text
---
Err(
MissingDownwardMigrations(
[
(
3,
"M { up: \"ALTER TABLE t1 RENAME COLUMN b TO c;\", up_hook: None, down: None, down_hook: None, foreign_key_check: false, comment: None }",
),
(
4,
"M { up: \"CREATE TABLE t2(b);\", up_hook: None, down: None, down_hook: None, foreign_key_check: false, comment: None }",
),
(
6,
"M { up: \"\\n CREATE TABLE fk1(a PRIMARY KEY);\\n CREATE TABLE fk2(\\n a,\\n FOREIGN KEY(a) REFERENCES fk1(a)\\n );\\n INSERT INTO fk1 (a) VALUES ('foo');\\n INSERT INTO fk2 (a) VALUES ('foo');\\n \", up_hook: None, down: None, down_hook: None, foreign_key_check: true, comment: None }",
),
],
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: rusqlite_migration/src/validations/tests.rs
expression: e
snapshot_kind: text
---
underlying rusqlite migration error: rusqlite_migrate error: RusqliteError { query: "", err: InvalidQuery }
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: rusqlite_migration/src/validations/tests.rs
expression: v.unwrap_err().source()
snapshot_kind: text
---
Some(
RusqliteError {
query: "Invalid sql",
err: SqliteFailure(
Error {
code: Unknown,
extended_code: 1,
},
Some(
"near \"Invalid\": syntax error",
),
),
},
)
Loading
Loading