Skip to content

Commit f2bb464

Browse files
authored
Support naming migrations sequentially (#2602)
* Support naming migrations sequentially and inferring naming scheme * Document new options and how naming is inferred * Only account for up migrations when inferring ordering
1 parent c70cfaf commit f2bb464

File tree

4 files changed

+104
-6
lines changed

4 files changed

+104
-6
lines changed

sqlx-cli/src/lib.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ pub async fn run(opt: Opt) -> Result<()> {
2727
source,
2828
description,
2929
reversible,
30-
} => migrate::add(&source, &description, reversible).await?,
30+
sequential,
31+
timestamp,
32+
} => migrate::add(&source, &description, reversible, sequential, timestamp).await?,
3133
MigrateCommand::Run {
3234
source,
3335
dry_run,

sqlx-cli/src/migrate.rs

+69-3
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,75 @@ fn create_file(
3737
Ok(())
3838
}
3939

40+
enum MigrationOrdering {
41+
Timestamp(String),
42+
Sequential(String),
43+
}
44+
45+
impl MigrationOrdering {
46+
fn timestamp() -> MigrationOrdering {
47+
Self::Timestamp(Utc::now().format("%Y%m%d%H%M%S").to_string())
48+
}
49+
50+
fn sequential(version: i64) -> MigrationOrdering {
51+
Self::Sequential(format!("{:04}", version))
52+
}
53+
54+
fn file_prefix(&self) -> &str {
55+
match self {
56+
MigrationOrdering::Timestamp(prefix) => prefix,
57+
MigrationOrdering::Sequential(prefix) => prefix,
58+
}
59+
}
60+
61+
fn infer(sequential: bool, timestamp: bool, migrator: &Migrator) -> Self {
62+
match (timestamp, sequential) {
63+
(true, true) => panic!("Impossible to specify both timestamp and sequential mode"),
64+
(true, false) => MigrationOrdering::timestamp(),
65+
(false, true) => MigrationOrdering::sequential(
66+
migrator
67+
.iter()
68+
.last()
69+
.map_or(1, |last_migration| last_migration.version + 1),
70+
),
71+
(false, false) => {
72+
// inferring the naming scheme
73+
let migrations = migrator
74+
.iter()
75+
.filter(|migration| migration.migration_type.is_up_migration())
76+
.rev()
77+
.take(2)
78+
.collect::<Vec<_>>();
79+
if let [last, pre_last] = &migrations[..] {
80+
// there are at least two migrations, compare the last twothere's only one existing migration
81+
if last.version - pre_last.version == 1 {
82+
// their version numbers differ by 1, infer sequential
83+
MigrationOrdering::sequential(last.version + 1)
84+
} else {
85+
MigrationOrdering::timestamp()
86+
}
87+
} else if let [last] = &migrations[..] {
88+
// there is only one existing migration
89+
if last.version == 0 || last.version == 1 {
90+
// infer sequential if the version number is 0 or 1
91+
MigrationOrdering::sequential(last.version + 1)
92+
} else {
93+
MigrationOrdering::timestamp()
94+
}
95+
} else {
96+
MigrationOrdering::timestamp()
97+
}
98+
}
99+
}
100+
}
101+
}
102+
40103
pub async fn add(
41104
migration_source: &str,
42105
description: &str,
43106
reversible: bool,
107+
sequential: bool,
108+
timestamp: bool,
44109
) -> anyhow::Result<()> {
45110
fs::create_dir_all(migration_source).context("Unable to create migrations directory")?;
46111

@@ -50,15 +115,16 @@ pub async fn add(
50115
.unwrap_or(false);
51116

52117
let migrator = Migrator::new(Path::new(migration_source)).await?;
53-
// This checks if all existing migrations are of the same type as the reverisble flag passed
118+
// This checks if all existing migrations are of the same type as the reversible flag passed
54119
for migration in migrator.iter() {
55120
if migration.migration_type.is_reversible() != reversible {
56121
bail!(MigrateError::InvalidMixReversibleAndSimple);
57122
}
58123
}
59124

60-
let dt = Utc::now();
61-
let file_prefix = dt.format("%Y%m%d%H%M%S").to_string();
125+
let ordering = MigrationOrdering::infer(sequential, timestamp, &migrator);
126+
let file_prefix = ordering.file_prefix();
127+
62128
if reversible {
63129
create_file(
64130
migration_source,

sqlx-cli/src/opt.rs

+24-2
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,22 @@ pub struct MigrateOpt {
109109

110110
#[derive(Parser, Debug)]
111111
pub enum MigrateCommand {
112-
/// Create a new migration with the given description,
113-
/// and the current time as the version.
112+
/// Create a new migration with the given description.
113+
///
114+
/// A version number will be automatically assigned to the migration.
115+
///
116+
/// For convenience, this command will attempt to detect if sequential versioning is in use,
117+
/// and if so, continue the sequence.
118+
///
119+
/// Sequential versioning is inferred if:
120+
///
121+
/// * The version numbers of the last two migrations differ by exactly 1, or:
122+
///
123+
/// * only one migration exists and its version number is either 0 or 1.
124+
///
125+
/// Otherwise timestamp versioning is assumed.
126+
///
127+
/// This behavior can overridden by `--sequential` or `--timestamp`, respectively.
114128
Add {
115129
description: String,
116130

@@ -121,6 +135,14 @@ pub enum MigrateCommand {
121135
/// else creates a single sql file
122136
#[clap(short)]
123137
reversible: bool,
138+
139+
/// If set, use timestamp versioning for the new migration. Conflicts with `--sequential`.
140+
#[clap(short, long)]
141+
timestamp: bool,
142+
143+
/// If set, use timestamp versioning for the new migration. Conflicts with `--timestamp`.
144+
#[clap(short, long, conflicts_with = "timestamp")]
145+
sequential: bool,
124146
},
125147

126148
/// Run all pending migrations.

sqlx-core/src/migrate/migration_type.rs

+8
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ impl MigrationType {
3232
}
3333
}
3434

35+
pub fn is_up_migration(&self) -> bool {
36+
match self {
37+
MigrationType::Simple => true,
38+
MigrationType::ReversibleUp => true,
39+
MigrationType::ReversibleDown => false,
40+
}
41+
}
42+
3543
pub fn is_down_migration(&self) -> bool {
3644
match self {
3745
MigrationType::Simple => false,

0 commit comments

Comments
 (0)