Skip to content

Commit 40aef6d

Browse files
authored
feat: no tx migration (#3181)
* test: add a failing test * feat: add no_tx to migration struct * feat: execute migration with no tx block * fix: expected string literal compilation error * test: update no tx to content comment * refactor: use the sql comment instead of file name semantics * docs: remove no_tx from file format comment * fix: remove filename matches * fix: messed up merge * refactor: dedupe migration * fix: move comment to where it makes sense * fix: linter error
1 parent 25efb2f commit 40aef6d

File tree

9 files changed

+77
-28
lines changed

9 files changed

+77
-28
lines changed

sqlx-core/src/migrate/migration.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct Migration {
1111
pub migration_type: MigrationType,
1212
pub sql: Cow<'static, str>,
1313
pub checksum: Cow<'static, [u8]>,
14+
pub no_tx: bool,
1415
}
1516

1617
impl Migration {
@@ -19,6 +20,7 @@ impl Migration {
1920
description: Cow<'static, str>,
2021
migration_type: MigrationType,
2122
sql: Cow<'static, str>,
23+
no_tx: bool,
2224
) -> Self {
2325
let checksum = Cow::Owned(Vec::from(Sha384::digest(sql.as_bytes()).as_slice()));
2426

@@ -28,6 +30,7 @@ impl Migration {
2830
migration_type,
2931
sql,
3032
checksum,
33+
no_tx,
3134
}
3235
}
3336
}

sqlx-core/src/migrate/migrator.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub struct Migrator {
2121
pub ignore_missing: bool,
2222
#[doc(hidden)]
2323
pub locking: bool,
24+
#[doc(hidden)]
25+
pub no_tx: bool,
2426
}
2527

2628
fn validate_applied_migrations(
@@ -47,6 +49,7 @@ impl Migrator {
4749
pub const DEFAULT: Migrator = Migrator {
4850
migrations: Cow::Borrowed(&[]),
4951
ignore_missing: false,
52+
no_tx: false,
5053
locking: true,
5154
};
5255

sqlx-core/src/migrate/source.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ pub fn resolve_blocking(path: PathBuf) -> Result<Vec<(Migration, PathBuf)>, Reso
9797
let parts = file_name.splitn(2, '_').collect::<Vec<_>>();
9898

9999
if parts.len() != 2 || !parts[1].ends_with(".sql") {
100-
// not of the format: <VERSION>_<DESCRIPTION>.sql; ignore
100+
// not of the format: <VERSION>_<DESCRIPTION>.<REVERSIBLE_DIRECTION>.sql; ignore
101101
continue;
102102
}
103103

@@ -108,6 +108,7 @@ pub fn resolve_blocking(path: PathBuf) -> Result<Vec<(Migration, PathBuf)>, Reso
108108
})?;
109109

110110
let migration_type = MigrationType::from_filename(parts[1]);
111+
111112
// remove the `.sql` and replace `_` with ` `
112113
let description = parts[1]
113114
.trim_end_matches(migration_type.suffix())
@@ -122,12 +123,16 @@ pub fn resolve_blocking(path: PathBuf) -> Result<Vec<(Migration, PathBuf)>, Reso
122123
source: Some(e),
123124
})?;
124125

126+
// opt-out of migration transaction
127+
let no_tx = sql.starts_with("-- no-transaction");
128+
125129
migrations.push((
126130
Migration::new(
127131
version,
128132
Cow::Owned(description),
129133
migration_type,
130134
Cow::Owned(sql),
135+
no_tx,
131136
),
132137
entry_path,
133138
));

sqlx-macros-core/src/migrate.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ impl ToTokens for QuoteMigration {
3636
description,
3737
migration_type,
3838
checksum,
39+
no_tx,
3940
..
4041
} = &self.migration;
4142

@@ -69,6 +70,7 @@ impl ToTokens for QuoteMigration {
6970
description: ::std::borrow::Cow::Borrowed(#description),
7071
migration_type: #migration_type,
7172
sql: ::std::borrow::Cow::Borrowed(#sql),
73+
no_tx: #no_tx,
7274
checksum: ::std::borrow::Cow::Borrowed(&[
7375
#(#checksum),*
7476
]),

sqlx-postgres/src/migrate.rs

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -208,38 +208,25 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
208208
migration: &'m Migration,
209209
) -> BoxFuture<'m, Result<Duration, MigrateError>> {
210210
Box::pin(async move {
211-
let mut tx = self.begin().await?;
212211
let start = Instant::now();
213212

214-
// Use a single transaction for the actual migration script and the essential bookeeping so we never
215-
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
216-
// The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for
217-
// data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1
218-
// and update it once the actual transaction completed.
219-
let _ = tx
220-
.execute(&*migration.sql)
221-
.await
222-
.map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?;
223-
224-
// language=SQL
225-
let _ = query(
226-
r#"
227-
INSERT INTO _sqlx_migrations ( version, description, success, checksum, execution_time )
228-
VALUES ( $1, $2, TRUE, $3, -1 )
229-
"#,
230-
)
231-
.bind(migration.version)
232-
.bind(&*migration.description)
233-
.bind(&*migration.checksum)
234-
.execute(&mut *tx)
235-
.await?;
236-
237-
tx.commit().await?;
213+
// execute migration queries
214+
if migration.no_tx {
215+
execute_migration(self, migration).await?;
216+
} else {
217+
// Use a single transaction for the actual migration script and the essential bookeeping so we never
218+
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
219+
// The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for
220+
// data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1
221+
// and update it once the actual transaction completed.
222+
let mut tx = self.begin().await?;
223+
execute_migration(&mut tx, migration).await?;
224+
tx.commit().await?;
225+
}
238226

239227
// Update `elapsed_time`.
240228
// NOTE: The process may disconnect/die at this point, so the elapsed time value might be lost. We accept
241229
// this small risk since this value is not super important.
242-
243230
let elapsed = start.elapsed();
244231

245232
// language=SQL
@@ -286,6 +273,31 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
286273
}
287274
}
288275

276+
async fn execute_migration(
277+
conn: &mut PgConnection,
278+
migration: &Migration,
279+
) -> Result<(), MigrateError> {
280+
let _ = conn
281+
.execute(&*migration.sql)
282+
.await
283+
.map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?;
284+
285+
// language=SQL
286+
let _ = query(
287+
r#"
288+
INSERT INTO _sqlx_migrations ( version, description, success, checksum, execution_time )
289+
VALUES ( $1, $2, TRUE, $3, -1 )
290+
"#,
291+
)
292+
.bind(migration.version)
293+
.bind(&*migration.description)
294+
.bind(&*migration.checksum)
295+
.execute(conn)
296+
.await?;
297+
298+
Ok(())
299+
}
300+
289301
async fn current_database(conn: &mut PgConnection) -> Result<String, MigrateError> {
290302
// language=SQL
291303
Ok(query_scalar("SELECT current_database()")

src/macros/test.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ use sqlx::{PgPool, Row};
133133
# migrations: Cow::Borrowed(&[]),
134134
# ignore_missing: false,
135135
# locking: true,
136+
# no_tx: false
136137
# };
137138
# }
138139

tests/postgres/migrate.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,28 @@ async fn reversible(mut conn: PoolConnection<Postgres>) -> anyhow::Result<()> {
6666
Ok(())
6767
}
6868

69+
#[sqlx::test(migrations = false)]
70+
async fn no_tx(mut conn: PoolConnection<Postgres>) -> anyhow::Result<()> {
71+
clean_up(&mut conn).await?;
72+
let migrator = Migrator::new(Path::new("tests/postgres/migrations_no_tx")).await?;
73+
74+
// run migration
75+
migrator.run(&mut conn).await?;
76+
77+
// check outcome
78+
let res: String = conn
79+
.fetch_one("SELECT datname FROM pg_database WHERE datname = 'test_db'")
80+
.await?
81+
.get(0);
82+
83+
assert_eq!(res, "test_db");
84+
85+
Ok(())
86+
}
87+
6988
/// Ensure that we have a clean initial state.
7089
async fn clean_up(conn: &mut PgConnection) -> anyhow::Result<()> {
90+
conn.execute("DROP DATABASE IF EXISTS test_db").await.ok();
7191
conn.execute("DROP TABLE migrations_simple_test").await.ok();
7292
conn.execute("DROP TABLE migrations_reversible_test")
7393
.await
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- no-transaction
2+
3+
CREATE DATABASE test_db;

tests/postgres/test-attr.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async fn it_gets_posts_mixed_fixtures_path(pool: PgPool) -> sqlx::Result<()> {
127127
// This should apply migrations and then `../fixtures/postgres/users.sql` and `../fixtures/postgres/posts.sql`
128128
#[sqlx::test(
129129
migrations = "tests/postgres/migrations",
130-
fixtures(path = "../fixtures/postgres", scripts("users.sql", "posts"))
130+
fixtures("../fixtures/postgres/users.sql", "../fixtures/postgres/posts.sql")
131131
)]
132132
async fn it_gets_posts_custom_relative_fixtures_path(pool: PgPool) -> sqlx::Result<()> {
133133
let post_contents: Vec<String> =

0 commit comments

Comments
 (0)