Skip to content

Commit ae22270

Browse files
committed
Add a config file for Diesel CLI, autorun print-schema
As discussed in diesel-rs#1621 and elsewhere, we'd like to replace `infer_schema!` with a config file that re-writes your schema file automatically when it changes. This file will eventually be expanded to support most of the CLI args to `print-schema`, and gain additional options to further customize the output. However, this is the minimal first step. I would like to change the default file generated by `diesel setup` to include `file = "src/schema.rs"`, but before we do that I think we should change the behavior on tables with no PK to print a warning that it's being skipped instead of panicking.
1 parent 76978b5 commit ae22270

10 files changed

Lines changed: 255 additions & 17 deletions

File tree

diesel_cli/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ path = "src/main.rs"
1717
[dependencies]
1818
chrono = "0.4"
1919
clap = "2.27"
20+
clippy = { optional = true, version = "=0.0.185" }
2021
diesel = { version = "~1.2.0", default-features = false }
2122
dotenv = ">=0.8, <0.11"
2223
infer_schema_internals = "~1.2.0"
23-
clippy = { optional = true, version = "=0.0.185" }
2424
migrations_internals = "~1.2.0"
25+
serde = { version = "1.0.0", features = ["derive"] }
26+
toml = "0.4.6"
2527
url = { version = "1.4.0", optional = true }
2628

2729
[dev-dependencies]

diesel_cli/build.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use std::env;
2+
3+
fn main() {
4+
if env::var("CARGO_PKG_VERSION").unwrap() == "1.3.0" {
5+
panic!(
6+
"Did you remember to publish documentation on the config file? \
7+
If not go do it. And then delete this build script."
8+
);
9+
}
10+
}

diesel_cli/src/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,25 @@ pub fn build_cli() -> App<'static, 'static> {
122122
.help("Render documentation comments for tables and columns"),
123123
);
124124

125+
let config_arg = Arg::with_name("CONFIG_FILE")
126+
.long("config-file")
127+
.help(
128+
"The location of the configuration file to use. Falls back to the \
129+
`DIESEL_CONFIG_FILE` environment variable if unspecified. Defaults \
130+
to `diesel.toml` in your project root. See \
131+
diesel.rs/guides/configuring-diesel-cli for documentation on this file.",
132+
)
133+
.global(true)
134+
.takes_value(true);
135+
125136
App::new("diesel")
126137
.version(env!("CARGO_PKG_VERSION"))
127138
.setting(AppSettings::VersionlessSubcommands)
128139
.after_help(
129140
"You can also run `diesel SUBCOMMAND -h` to get more information about that subcommand.",
130141
)
131142
.arg(database_arg)
143+
.arg(config_arg)
132144
.subcommand(migration_subcommand)
133145
.subcommand(setup_subcommand)
134146
.subcommand(database_subcommand)

diesel_cli/src/config.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use clap::ArgMatches;
2+
use std::env;
3+
use std::error::Error;
4+
use std::fs;
5+
use std::io::Read;
6+
use std::path::PathBuf;
7+
use toml;
8+
9+
use super::{find_project_root, handle_error};
10+
11+
#[derive(Deserialize)]
12+
pub struct Config {
13+
#[serde(default)]
14+
pub print_schema: PrintSchema,
15+
}
16+
17+
impl Config {
18+
pub fn file_path(matches: &ArgMatches) -> PathBuf {
19+
matches
20+
.value_of("CONFIG_FILE")
21+
.map(PathBuf::from)
22+
.or_else(|| env::var_os("DIESEL_CONFIG_FILE").map(PathBuf::from))
23+
.unwrap_or_else(|| {
24+
find_project_root()
25+
.unwrap_or_else(handle_error)
26+
.join("diesel.toml")
27+
})
28+
}
29+
30+
pub fn read(matches: &ArgMatches) -> Result<Self, Box<Error>> {
31+
let path = Self::file_path(matches);
32+
let mut bytes = Vec::new();
33+
fs::File::open(path)?.read_to_end(&mut bytes)?;
34+
toml::from_slice(&bytes).map_err(Into::into)
35+
}
36+
}
37+
38+
#[derive(Default, Deserialize)]
39+
pub struct PrintSchema {
40+
pub file: Option<PathBuf>,
41+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# For documentation on how to configure this file,
2+
# see diesel.rs/guides/configuring-diesel-cli

diesel_cli/src/main.rs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ extern crate diesel;
1717
extern crate dotenv;
1818
extern crate infer_schema_internals;
1919
extern crate migrations_internals;
20+
#[macro_use]
21+
extern crate serde;
22+
extern crate toml;
2023
#[cfg(feature = "url")]
2124
extern crate url;
2225

26+
mod config;
2327
mod database_error;
2428
#[macro_use]
2529
mod database;
@@ -32,10 +36,12 @@ use chrono::*;
3236
use clap::{ArgMatches, Shell};
3337
use migrations_internals::{self as migrations, MigrationConnection};
3438
use std::any::Any;
39+
use std::error::Error;
3540
use std::io::stdout;
3641
use std::path::{Path, PathBuf};
3742
use std::{env, fs};
3843

44+
use self::config::Config;
3945
use self::database_error::{DatabaseError, DatabaseResult};
4046
use migrations_internals::TIMESTAMP_FORMAT;
4147

@@ -46,7 +52,7 @@ fn main() {
4652
let matches = cli::build_cli().get_matches();
4753

4854
match matches.subcommand() {
49-
("migration", Some(matches)) => run_migration_command(matches),
55+
("migration", Some(matches)) => run_migration_command(matches).unwrap_or_else(handle_error),
5056
("setup", Some(matches)) => run_setup_command(matches),
5157
("database", Some(matches)) => run_database_command(matches),
5258
("bash-completion", Some(matches)) => generate_bash_completion_command(matches),
@@ -55,35 +61,37 @@ fn main() {
5561
}
5662
}
5763

58-
fn run_migration_command(matches: &ArgMatches) {
64+
fn run_migration_command(matches: &ArgMatches) -> Result<(), Box<Error>> {
5965
match matches.subcommand() {
6066
("run", Some(_)) => {
6167
let database_url = database::database_url(matches);
6268
let dir = migrations_dir(matches);
6369
call_with_conn!(
6470
database_url,
6571
migrations::run_pending_migrations_in_directory(&dir, &mut stdout())
66-
).unwrap_or_else(handle_error);
72+
)?;
73+
regenerate_schema_if_file_specified(matches)?;
6774
}
6875
("revert", Some(_)) => {
6976
let database_url = database::database_url(matches);
7077
let dir = migrations_dir(matches);
7178
call_with_conn!(
7279
database_url,
7380
migrations::revert_latest_migration_in_directory(&dir)
74-
).unwrap_or_else(handle_error);
81+
)?;
82+
regenerate_schema_if_file_specified(matches)?;
7583
}
7684
("redo", Some(_)) => {
7785
let database_url = database::database_url(matches);
7886
let dir = migrations_dir(matches);
7987
call_with_conn!(database_url, redo_latest_migration(&dir));
88+
regenerate_schema_if_file_specified(matches)?;
8089
}
8190
("list", Some(_)) => {
8291
let database_url = database::database_url(matches);
8392
let dir = migrations_dir(matches);
8493
let mut migrations =
85-
call_with_conn!(database_url, migrations::mark_migrations_in_directory(&dir))
86-
.unwrap_or_else(handle_error);
94+
call_with_conn!(database_url, migrations::mark_migrations_in_directory(&dir))?;
8795

8896
migrations.sort_by_key(|&(ref m, _)| m.version().to_string());
8997

@@ -101,8 +109,8 @@ fn run_migration_command(matches: &ArgMatches) {
101109
}
102110
("pending", Some(_)) => {
103111
let database_url = database::database_url(matches);
104-
let result = call_with_conn!(database_url, migrations::any_pending_migrations);
105-
println!("{:?}", result.unwrap());
112+
let result = call_with_conn!(database_url, migrations::any_pending_migrations)?;
113+
println!("{:?}", result);
106114
}
107115
("generate", Some(args)) => {
108116
use std::io::Write;
@@ -111,10 +119,10 @@ fn run_migration_command(matches: &ArgMatches) {
111119
let version = migration_version(args);
112120
let versioned_name = format!("{}_{}", version, migration_name);
113121
let migration_dir = migrations_dir(matches).join(versioned_name);
114-
fs::create_dir(&migration_dir).unwrap();
122+
fs::create_dir(&migration_dir)?;
115123

116124
let migration_dir_relative =
117-
convert_absolute_path_to_relative(&migration_dir, &env::current_dir().unwrap());
125+
convert_absolute_path_to_relative(&migration_dir, &env::current_dir()?);
118126

119127
let up_path = migration_dir.join("up.sql");
120128
println!(
@@ -129,12 +137,13 @@ fn run_migration_command(matches: &ArgMatches) {
129137
"Creating {}",
130138
migration_dir_relative.join("down.sql").display()
131139
);
132-
let mut down = fs::File::create(down_path).unwrap();
133-
down.write_all(b"-- This file should undo anything in `up.sql`")
134-
.unwrap();
140+
let mut down = fs::File::create(down_path)?;
141+
down.write_all(b"-- This file should undo anything in `up.sql`")?;
135142
}
136143
_ => unreachable!("The cli parser should prevent reaching here"),
137-
}
144+
};
145+
146+
Ok(())
138147
}
139148

140149
use std::fmt::Display;
@@ -165,6 +174,7 @@ fn migrations_dir(matches: &ArgMatches) -> PathBuf {
165174

166175
fn run_setup_command(matches: &ArgMatches) {
167176
let migrations_dir = create_migrations_dir(matches).unwrap_or_else(handle_error);
177+
create_config_file(matches).unwrap_or_else(handle_error);
168178

169179
database::setup_database(matches, &migrations_dir).unwrap_or_else(handle_error);
170180
}
@@ -187,6 +197,17 @@ fn create_migrations_dir(matches: &ArgMatches) -> DatabaseResult<PathBuf> {
187197
Ok(dir.to_owned())
188198
}
189199

200+
fn create_config_file(matches: &ArgMatches) -> DatabaseResult<()> {
201+
use std::io::Write;
202+
let path = Config::file_path(matches);
203+
if !path.exists() {
204+
let mut file = fs::File::create(path)?;
205+
file.write_all(include_bytes!("default_files/diesel.toml"))?;
206+
}
207+
208+
Ok(())
209+
}
210+
190211
fn run_database_command(matches: &ArgMatches) {
191212
match matches.subcommand() {
192213
("setup", Some(args)) => {
@@ -327,6 +348,22 @@ fn run_infer_schema(matches: &ArgMatches) {
327348
).map_err(handle_error::<_, ()>);
328349
}
329350

351+
fn regenerate_schema_if_file_specified(matches: &ArgMatches) -> Result<(), Box<Error>> {
352+
use print_schema::*;
353+
354+
let config = Config::read(matches)?;
355+
if let Some(path) = config.print_schema.file {
356+
if let Some(parent) = path.parent() {
357+
fs::create_dir_all(parent)?;
358+
}
359+
360+
let database_url = database::database_url(matches);
361+
let mut file = fs::File::create(path)?;
362+
print_schema::output_schema(&database_url, None, &Filtering::None, false, &mut file)?;
363+
}
364+
Ok(())
365+
}
366+
330367
#[cfg(test)]
331368
mod tests {
332369
extern crate tempdir;

diesel_cli/src/print_schema.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use infer_schema_internals::*;
22
use std::error::Error;
33
use std::fmt::{self, Display, Formatter, Write};
4+
use std::io::{self, stdout};
45

56
pub enum Filtering {
67
Whitelist(Vec<TableName>),
@@ -25,6 +26,22 @@ pub fn run_print_schema(
2526
schema_name: Option<&str>,
2627
filtering: &Filtering,
2728
include_docs: bool,
29+
) -> Result<(), Box<Error>> {
30+
output_schema(
31+
database_url,
32+
schema_name,
33+
filtering,
34+
include_docs,
35+
&mut stdout(),
36+
)
37+
}
38+
39+
pub fn output_schema<W: io::Write>(
40+
database_url: &str,
41+
schema_name: Option<&str>,
42+
filtering: &Filtering,
43+
include_docs: bool,
44+
out: &mut W,
2845
) -> Result<(), Box<Error>> {
2946
let table_names = load_table_names(database_url, schema_name)?
3047
.into_iter()
@@ -44,9 +61,9 @@ pub fn run_print_schema(
4461
};
4562

4663
if let Some(schema_name) = schema_name {
47-
print!("{}", ModuleDefinition(schema_name, definitions));
64+
write!(out, "{}", ModuleDefinition(schema_name, definitions))?;
4865
} else {
49-
print!("{}", definitions);
66+
write!(out, "{}", definitions)?;
5067
}
5168
Ok(())
5269
}

diesel_cli/tests/migration_run.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,38 @@ fn migration_run_runs_pending_migrations_custom_migration_dir_2() {
311311
);
312312
assert!(db.table_exists("users"));
313313
}
314+
315+
#[test]
316+
fn migration_run_updates_schema_if_config_present() {
317+
let p = project("migration_run_updates_schema_if_config_present")
318+
.folder("migrations")
319+
.file(
320+
"diesel.toml",
321+
r#"
322+
[print_schema]
323+
file = "src/my_schema.rs"
324+
"#,
325+
)
326+
.build();
327+
328+
// Make sure the project is setup
329+
p.command("setup").run();
330+
331+
p.create_migration(
332+
"12345_create_users_table",
333+
"CREATE TABLE users (id INTEGER PRIMARY KEY)",
334+
"DROP TABLE users",
335+
);
336+
337+
assert!(!p.has_file("src/my_schema.rs"));
338+
339+
let result = p.command("migration").arg("run").run();
340+
341+
assert!(result.is_success(), "Result was unsuccessful {:?}", result);
342+
assert!(
343+
result.stdout().contains("Running migration 12345"),
344+
"Unexpected stdout {}",
345+
result.stdout()
346+
);
347+
assert!(p.has_file("src/my_schema.rs"));
348+
}

0 commit comments

Comments
 (0)