8use clap::{Args, Parser};
10 migrations::{Action, Migration},
13use serde::{Deserialize, Serialize};
16#[clap(name = "Reshape", version, about)]
26 Migration(MigrationCommand),
29 about = "Output the query your application should use to select the right schema",
32 SchemaQuery(FindMigrationsOptions),
35 about = "Deprecated. Use `reshape schema-query` instead",
38 GenerateSchemaQuery(FindMigrationsOptions),
41 about = "Deprecated. Use `reshape migration start` instead",
44 Migrate(MigrateOptions),
46 about = "Deprecated. Use `reshape migration complete` instead",
49 Complete(ConnectionOptions),
51 about = "Deprecated. Use `reshape migration abort` instead",
54 Abort(ConnectionOptions),
58#[clap(about = "Commands for managing migrations", display_order = 1)]
59enum MigrationCommand {
61 about = "Starts a new migration, applying any migrations which haven't yet been applied",
64 Start(MigrateOptions),
66 #[clap(about = "Completes an in-progress migration", display_order = 2)]
67 Complete(ConnectionOptions),
70 about = "Aborts an in-progress migration without losing any data",
73 Abort(ConnectionOptions),
77struct MigrateOptions {
82 connection_options: ConnectionOptions,
84 find_migrations_options: FindMigrationsOptions,
88struct ConnectionOptions {
91 #[clap(long, default_value = "localhost")]
93 #[clap(long, default_value = "5432")]
95 #[clap(long, short, default_value = "postgres")]
97 #[clap(long, short, default_value = "postgres")]
99 #[clap(long, short, default_value = "postgres")]
104struct FindMigrationsOptions {
105 #[clap(long, default_value = "migrations")]
109fn main() -> anyhow::Result<()> {
110 let opts: Opts = Opts::parse();
114fn run(opts: Opts) -> anyhow::Result<()> {
116 Command::Migration(MigrationCommand::Start(opts)) | Command::Migrate(opts) => {
117 let mut reshape = reshape_from_connection_options(&opts.connection_options)?;
118 let migrations = find_migrations(&opts.find_migrations_options)?;
119 reshape.migrate(migrations)?;
121 // Automatically complete migration if --complete flag is set
128 Command::Migration(MigrationCommand::Complete(opts)) | Command::Complete(opts) => {
129 let mut reshape = reshape_from_connection_options(&opts)?;
132 Command::Migration(MigrationCommand::Abort(opts)) | Command::Abort(opts) => {
133 let mut reshape = reshape_from_connection_options(&opts)?;
136 Command::SchemaQuery(opts) | Command::GenerateSchemaQuery(opts) => {
137 let migrations = find_migrations(&opts)?;
138 let query = migrations
140 .map(|migration| reshape::schema_query_for_migration(&migration.name));
141 println!("{}", query.unwrap_or_else(|| "".to_string()));
148fn reshape_from_connection_options(opts: &ConnectionOptions) -> anyhow::Result<Reshape> {
149 // Load environment variables from .env file if it exists
150 dotenv::dotenv().ok();
152 let url_env = std::env::var("DB_URL").ok();
153 let url = url_env.as_ref().or_else(|| opts.url.as_ref());
155 // Use the connection URL if it has been set
156 if let Some(url) = url {
157 return Reshape::new(url);
160 let host_env = std::env::var("DB_HOST").ok();
161 let host = host_env.as_ref().unwrap_or_else(|| &opts.host);
163 let port = std::env::var("DB_PORT")
165 .and_then(|port| port.parse::<u16>().ok())
166 .unwrap_or(opts.port);
168 let username_env = std::env::var("DB_USERNAME").ok();
169 let username = username_env.as_ref().unwrap_or_else(|| &opts.username);
171 let password_env = std::env::var("DB_PASSWORD").ok();
172 let password = password_env.as_ref().unwrap_or_else(|| &opts.password);
174 let database_env = std::env::var("DB_NAME").ok();
175 let database = database_env.as_ref().unwrap_or_else(|| &opts.database);
177 Reshape::new_with_options(host, port, database, username, password)
180fn find_migrations(opts: &FindMigrationsOptions) -> anyhow::Result<Vec<Migration>> {
181 let search_paths = opts
185 // Filter out all directories that don't exist
186 .filter(|path| path.exists());
188 // Find all files in the search paths
189 let mut file_paths = Vec::new();
190 for search_path in search_paths {
191 let entries = fs::read_dir(search_path)?;
192 for entry in entries {
193 let path = entry?.path();
194 file_paths.push(path);
198 // Sort all files by their file names (without extension)
199 // The files are sorted naturally, e.g. "1_test_migration" < "10_test_migration"
200 file_paths.sort_unstable_by(|path1, path2| {
201 let file1 = path1.as_path().file_stem().unwrap().to_str().unwrap();
202 let file2 = path2.as_path().file_stem().unwrap().to_str().unwrap();
204 lexical_sort::natural_cmp(file1, file2)
210 let mut file = File::open(path)?;
213 let mut data = String::new();
214 file.read_to_string(&mut data)?;
219 result.and_then(|(path, data)| {
220 let extension = path.extension().and_then(|ext| ext.to_str()).unwrap();
222 decode_migration_file(&data, extension).with_context(|| {
223 format!("failed to parse migration file {}", path.display())
226 let file_name = path.file_stem().and_then(|name| name.to_str()).unwrap();
228 name: file_migration.name.unwrap_or_else(|| file_name.to_string()),
229 description: file_migration.description,
230 actions: file_migration.actions,
237fn decode_migration_file(data: &str, extension: &str) -> anyhow::Result<FileMigration> {
238 let migration: FileMigration = match extension {
239 "json" => serde_json::from_str(data)?,
240 "toml" => toml::from_str(data)?,
242 return Err(anyhow::anyhow!(
243 "unrecognized file extension '{}'",
252#[derive(Serialize, Deserialize)]
253struct FileMigration {
254 name: Option<String>,
255 description: Option<String>,
256 actions: Vec<Box<dyn Action>>,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list