Skip to content

Commit

Permalink
feat: return full foreign_key_check error
Browse files Browse the repository at this point in the history
The `foreign_key_check` pragma returns a list of all the violated
foreign key constraints[^1]:
> The foreign_key_check pragma returns one row output for each foreign
> key violation
Each ForeignKeyCheckError store one of those rows. However, only the
first row is stored.

Store all row returned in a vector in the returned error variant,
`Error::ForeignKeyCheck(…)`.

[^1]: https://www.sqlite.org/pragma.html#pragma_foreign_key_check
  • Loading branch information
cljoly committed Dec 7, 2023
1 parent 34153e9 commit 43daf95
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 24 deletions.
4 changes: 2 additions & 2 deletions rusqlite_migration/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub enum Error {
/// Something wrong with migration definitions
MigrationDefinition(MigrationDefinitionError),
/// The foreign key check failed
ForeignKeyCheck(ForeignKeyCheckError),
ForeignKeyCheck(Vec<ForeignKeyCheckError>),
/// Error returned by the migration hook
Hook(String),
/// Error returned when loading migrations from directory
Expand Down Expand Up @@ -80,7 +80,7 @@ impl std::error::Error for Error {
Error::RusqliteError { query: _, err } => Some(err),
Error::SpecifiedSchemaVersion(e) => Some(e),
Error::MigrationDefinition(e) => Some(e),
Error::ForeignKeyCheck(e) => Some(e),
Error::ForeignKeyCheck(vec) => Some(vec.get(0)?),
Error::Hook(_) | Error::FileLoad(_) => None,
#[cfg(feature = "async-tokio-rusqlite")]
Error::ConnectionClosed => None,
Expand Down
34 changes: 20 additions & 14 deletions rusqlite_migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ limitations under the License.
#![doc = include_str!(concat!(env!("OUT_DIR"), "/readme_for_rustdoc.md"))]

use log::{debug, info, trace, warn};
use rusqlite::{Connection, OptionalExtension, Transaction};
use rusqlite::{Connection, Transaction};

#[cfg(feature = "from-directory")]
use include_dir::Dir;
Expand Down Expand Up @@ -773,20 +773,26 @@ fn set_user_version(conn: &Connection, v: usize) -> Result<()> {
// Validate that no foreign keys are violated
fn validate_foreign_keys(conn: &Connection) -> Result<()> {
let pragma_fk_check = "PRAGMA foreign_key_check";
conn.query_row(pragma_fk_check, [], |row| {
Ok(ForeignKeyCheckError {
table: row.get(0)?,
rowid: row.get(1)?,
parent: row.get(2)?,
fkid: row.get(3)?,
let mut stmt = conn
.prepare_cached(pragma_fk_check)
.map_err(|e| Error::with_sql(e, pragma_fk_check))?;

let fk_errors = stmt
.query_map([], |row| {
Ok(ForeignKeyCheckError {
table: row.get(0)?,
rowid: row.get(1)?,
parent: row.get(2)?,
fkid: row.get(3)?,
})
})
})
.optional()
.map_err(|e| Error::with_sql(e, pragma_fk_check))
.and_then(|o| match o {
Some(e) => Err(Error::ForeignKeyCheck(e)),
None => Ok(()),
})
.map_err(|e| Error::with_sql(e, pragma_fk_check))?
.collect::<Result<Vec<_>, _>>()?;
if fk_errors.len() > 0 {
Err(crate::Error::ForeignKeyCheck(fk_errors))
} else {
Ok(())
}
}

impl<'u> FromIterator<M<'u>> for Migrations<'u> {
Expand Down
1 change: 1 addition & 0 deletions rusqlite_migration/src/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub fn m_invalid_fk() -> M<'static> {
FOREIGN KEY(a) REFERENCES fk1(a) \
); \
INSERT INTO fk2 (a) VALUES ('foo'); \
INSERT INTO fk2 (a) VALUES ('bar'); \
",
)
.foreign_key_check()
Expand Down
29 changes: 21 additions & 8 deletions rusqlite_migration/src/tests/synch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,13 @@ fn test_error_display() {
let err = Error::Hook(String::new());
assert_eq!("rusqlite_migrate error: Hook(\"\")", format!("{err}"));

let err = Error::ForeignKeyCheck(ForeignKeyCheckError {
let err = Error::ForeignKeyCheck(vec![ForeignKeyCheckError {
table: String::new(),
rowid: 1,
parent: String::new(),
fkid: 2,
});
assert_eq!("rusqlite_migrate error: ForeignKeyCheck(ForeignKeyCheckError { table: \"\", rowid: 1, parent: \"\", fkid: 2 })", format!("{err}"));
}]);
assert_eq!("rusqlite_migrate error: ForeignKeyCheck([ForeignKeyCheckError { table: \"\", rowid: 1, parent: \"\", fkid: 2 }])", format!("{err}"));

let err = Error::MigrationDefinition(MigrationDefinitionError::NoMigrationsDefined);
assert_eq!(
Expand Down Expand Up @@ -240,12 +240,12 @@ fn error_test_source() {
&MigrationDefinitionError::NoMigrationsDefined
);

let err = Error::ForeignKeyCheck(ForeignKeyCheckError {
let err = Error::ForeignKeyCheck(vec![ForeignKeyCheckError {
table: String::new(),
rowid: 1i64,
parent: String::new(),
fkid: 1i64,
});
}]);
assert_eq!(
std::error::Error::source(&err)
.and_then(|e| e.downcast_ref::<ForeignKeyCheckError>())
Expand Down Expand Up @@ -350,10 +350,23 @@ fn valid_fk_check_test() {
#[test]
fn invalid_fk_check_test() {
let migrations = Migrations::new(vec![m_invalid_fk()]);
assert!(matches!(
assert_eq!(
dbg!(migrations.validate()),
Err(Error::ForeignKeyCheck(_))
));
Err(Error::ForeignKeyCheck(vec![
ForeignKeyCheckError {
table: String::from("fk2"),
rowid: 1,
parent: String::from("fk1"),
fkid: 0,
},
ForeignKeyCheckError {
table: String::from("fk2"),
rowid: 2,
parent: String::from("fk1"),
fkid: 0,
},
],),)
);
}

#[test]
Expand Down

0 comments on commit 43daf95

Please sign in to comment.