Permalink
Browse files

Add `--verify-schema` to CLI, for use in CI and production

While the config file and auto-updating schema file is a nice quality of
life change, it's too easy to accidentally have the contents of
src/schema.rs not reflect reality, but have CI pass because the file
gets changed when the test setup runs.

This adds a new flag, `--verify-schema` which causes any command that
would update the schema file specified in your config to error if the
contents changed as a result of that command. It's expected that this
command would always be run in CI and production deploys.
  • Loading branch information...
sgrif committed Sep 18, 2018
1 parent 15fa328 commit 096a75efd5a1cec59358a5062d1e4243ef4612b8
@@ -12,6 +12,11 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
### Added
* Diesel CLI can be configured to error if a command would result in changes
to your schema file by passing `--locked-schema`. This is intended for use
in CI and production deploys, to ensure that the committed schema file is
up to date.
* A helper trait has been added for implementing `ToSql` for PG composite types.
See [`WriteTuple`][write-tuple-1-4-0] for details.
@@ -182,6 +182,17 @@ pub fn build_cli() -> App<'static, 'static> {
.global(true)
.takes_value(true);
let locked_schema_arg = Arg::with_name("LOCKED_SCHEMA")
.long("locked-schema")
.help("Require that the schema file is up to date")
.long_help(
"When `print_schema.file` is specified in your config file, this \
flag will cause Diesel CLI to error if any command would result in \
changes to that file. It is recommended that you use this flag when \
running migrations in CI or production.",
)
.global(true);
App::new("diesel")
.version(env!("CARGO_PKG_VERSION"))
.setting(AppSettings::VersionlessSubcommands)
@@ -190,6 +201,7 @@ pub fn build_cli() -> App<'static, 'static> {
)
.arg(database_arg)
.arg(config_arg)
.arg(locked_schema_arg)
.subcommand(migration_subcommand)
.subcommand(setup_subcommand)
.subcommand(database_subcommand)
@@ -324,7 +324,7 @@ fn should_redo_migration_in_transaction(_t: &Any) -> bool {
#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))]
fn handle_error<E: Display, T>(error: E) -> T {
println!("{}", error);
eprintln!("{}", error);
::std::process::exit(1);
}
@@ -394,20 +394,40 @@ fn run_infer_schema(matches: &ArgMatches) -> Result<(), Box<Error>> {
config.import_types = Some(types);
}
run_print_schema(&database_url, &config)?;
run_print_schema(&database_url, &config, &mut stdout())?;
Ok(())
}
fn regenerate_schema_if_file_specified(matches: &ArgMatches) -> Result<(), Box<Error>> {
use std::io::Read;
let config = Config::read(matches)?;
if let Some(ref path) = config.print_schema.file {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let database_url = database::database_url(matches);
let mut file = fs::File::create(path)?;
print_schema::output_schema(&database_url, &config.print_schema, file, path)?;
if matches.is_present("LOCKED_SCHEMA") {
let mut buf = Vec::new();
print_schema::run_print_schema(&database_url, &config.print_schema, &mut buf)?;
let mut old_buf = Vec::new();
let mut file = fs::File::open(path)?;
file.read_to_end(&mut old_buf)?;
if buf != old_buf {
return Err(format!(
"Command would result in changes to {}. \
Rerun the command locally, and commit the changes.",
path.display()
).into());
}
} else {
let mut file = fs::File::create(path)?;
print_schema::output_schema(&database_url, &config.print_schema, file, path)?;
}
}
Ok(())
}
@@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer};
use std::error::Error;
use std::fmt::{self, Display, Formatter, Write};
use std::fs::File;
use std::io::{self, stdout, Write as IoWrite};
use std::io::{self, Write as IoWrite};
use std::path::Path;
use std::process::Command;
use tempfile::NamedTempFile;
@@ -35,9 +35,10 @@ impl Filtering {
}
}
pub fn run_print_schema(
pub fn run_print_schema<W: IoWrite>(
database_url: &str,
config: &config::PrintSchema,
output: &mut W,
) -> Result<(), Box<Error>> {
let tempfile = NamedTempFile::new()?;
let file = tempfile.reopen()?;
@@ -46,7 +47,7 @@ pub fn run_print_schema(
// patch "replaces" our tempfile, meaning the old handle
// does not include the patched output.
let mut file = File::open(tempfile.path())?;
io::copy(&mut file, &mut stdout())?;
io::copy(&mut file, output)?;
Ok(())
}
@@ -132,5 +132,5 @@ fn error_migrations_fails() {
let result = p.command("migration").arg("redo").run();
assert!(!result.is_success());
assert!(result.stdout().contains("Failed with: "));
assert!(result.stderr().contains("Failed with: "));
}
@@ -107,7 +107,7 @@ fn empty_migrations_are_not_valid() {
assert!(!result.is_success());
assert!(
result
.stdout()
.stderr()
.contains("Failed with: Attempted to run an empty migration.")
);
}
@@ -129,7 +129,7 @@ fn error_migrations_fails() {
let result = p.command("migration").arg("run").run();
assert!(!result.is_success());
assert!(result.stdout().contains("Failed with: "));
assert!(result.stderr().contains("Failed with: "));
}
#[test]
@@ -446,3 +446,58 @@ fn migrations_can_be_run_with_no_cargo_toml() {
);
assert!(db.table_exists("users"));
}
#[test]
fn verify_schema_errors_if_schema_file_would_change() {
let p = project("migration_run_verify_schema_errors")
.folder("migrations")
.file(
"diesel.toml",
r#"
[print_schema]
file = "src/my_schema.rs"
"#,
)
.build();
// Make sure the project is setup
p.command("setup").run();
p.create_migration(
"12345_create_users_table",
"CREATE TABLE users (id INTEGER PRIMARY KEY)",
"DROP TABLE users",
);
assert!(!p.has_file("src/my_schema.rs"));
let result = p.command("migration").arg("run").run();
assert!(result.is_success(), "Result was unsuccessful {:?}", result);
assert!(p.has_file("src/my_schema.rs"));
p.create_migration(
"12346_create_posts_table",
"CREATE TABLE posts (id INTEGER PRIMARY KEY)",
"DROP TABLE posts",
);
let result = p.command("migration")
.arg("run")
.arg("--locked-schema")
.run();
assert!(
!result.is_success(),
"Result was successful, expected to fail {:?}",
result
);
assert!(
result
.stderr()
.contains("Command would result in changes to src/my_schema.rs"),
"Unexpected stderr {}",
result.stderr()
);
assert!(p.has_file("src/my_schema.rs"));
}

0 comments on commit 096a75e

Please sign in to comment.