Skip to content

Commit ceab804

Browse files
committed
feat(sqlite): support no_tx migrations
SQLite includes several SQL statements that are useful during migrations but must be executed outside of a transaction to take effect, such as `PRAGMA foreign_keys = ON|OFF` or `VACUUM`. Additionally, advanced migrations may want more precise control over how statements are grouped into transactions or savepoints to achieve the desired atomicity for different parts of the migration. While SQLx already supports marking migrations to run outside explicit transactions through a `-- no-transaction` comment, this feature is currently only available for `PgConnection`'s `Migrate` implementation, leaving SQLite and MySQL without this capability. Although it's possible to work around this limitation by implementing custom migration logic instead of executing `Migrator#run`, this comes at a cost of significantly reduced developer ergonomics: code that relies on the default migration logic, such as `#[sqlx::test]` or `cargo sqlx database setup`, won't support these migrations. These changes extend `SqliteConnection`'s `Migrate` implementation to support `no_tx` migrations in the same way as PostgreSQL, addressing this feature gap. I also considered implementing the same functionality for MySQL, but since I haven't found a practical use case for it yet, and every non-transaction-friendly statement I could think about in MySQL triggers implicit commits anyway, I determined it wasn't necessary at this time and could be considered an overreach.
1 parent 5053a99 commit ceab804

1 file changed

Lines changed: 69 additions & 40 deletions

File tree

sqlx-sqlite/src/migrate.rs

Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -160,38 +160,24 @@ CREATE TABLE IF NOT EXISTS {table_name} (
160160
migration: &'e Migration,
161161
) -> BoxFuture<'e, Result<Duration, MigrateError>> {
162162
Box::pin(async move {
163-
let mut tx = self.begin().await?;
164163
let start = Instant::now();
165164

166-
// Use a single transaction for the actual migration script and the essential bookeeping so we never
167-
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
168-
// The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for
169-
// data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1
170-
// and update it once the actual transaction completed.
171-
let _ = tx
172-
.execute(migration.sql.clone())
173-
.await
174-
.map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?;
175-
176-
// language=SQL
177-
let _ = query(AssertSqlSafe(format!(
178-
r#"
179-
INSERT INTO {table_name} ( version, description, success, checksum, execution_time )
180-
VALUES ( ?1, ?2, TRUE, ?3, -1 )
181-
"#
182-
)))
183-
.bind(migration.version)
184-
.bind(&*migration.description)
185-
.bind(&*migration.checksum)
186-
.execute(&mut *tx)
187-
.await?;
188-
189-
tx.commit().await?;
165+
if migration.no_tx {
166+
execute_migration(self, table_name, migration).await?;
167+
} else {
168+
// Use a single transaction for the actual migration script and the essential bookkeeping so we never
169+
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
170+
// The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for
171+
// data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1
172+
// and update it once the actual transaction completed.
173+
let mut tx = self.begin().await?;
174+
execute_migration(&mut tx, table_name, migration).await?;
175+
tx.commit().await?;
176+
}
190177

191178
// Update `elapsed_time`.
192179
// NOTE: The process may disconnect/die at this point, so the elapsed time value might be lost. We accept
193180
// this small risk since this value is not super important.
194-
195181
let elapsed = start.elapsed();
196182

197183
// language=SQL
@@ -218,26 +204,69 @@ CREATE TABLE IF NOT EXISTS {table_name} (
218204
migration: &'e Migration,
219205
) -> BoxFuture<'e, Result<Duration, MigrateError>> {
220206
Box::pin(async move {
221-
// Use a single transaction for the actual migration script and the essential bookeeping so we never
222-
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
223-
let mut tx = self.begin().await?;
224207
let start = Instant::now();
225208

226-
let _ = tx.execute(migration.sql.clone()).await?;
227-
228-
// language=SQLite
229-
let _ = query(AssertSqlSafe(format!(
230-
r#"DELETE FROM {table_name} WHERE version = ?1"#
231-
)))
232-
.bind(migration.version)
233-
.execute(&mut *tx)
234-
.await?;
235-
236-
tx.commit().await?;
209+
if migration.no_tx {
210+
execute_migration(self, table_name, migration).await?;
211+
} else {
212+
let mut tx = self.begin().await?;
213+
revert_migration(&mut tx, table_name, migration).await?;
214+
tx.commit().await?;
215+
}
237216

238217
let elapsed = start.elapsed();
239218

240219
Ok(elapsed)
241220
})
242221
}
243222
}
223+
224+
async fn execute_migration(
225+
conn: &mut SqliteConnection,
226+
table_name: &str,
227+
migration: &Migration,
228+
) -> Result<(), MigrateError> {
229+
let _ = conn
230+
.execute(migration.sql.clone())
231+
.await
232+
.map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?;
233+
234+
// language=SQL
235+
let _ = query(AssertSqlSafe(format!(
236+
r#"
237+
INSERT INTO {table_name} ( version, description, success, checksum, execution_time )
238+
VALUES ( ?1, ?2, TRUE, ?3, -1 )
239+
"#
240+
)))
241+
.bind(migration.version)
242+
.bind(&*migration.description)
243+
.bind(&*migration.checksum)
244+
.execute(conn)
245+
.await?;
246+
247+
Ok(())
248+
}
249+
250+
async fn revert_migration(
251+
conn: &mut SqliteConnection,
252+
table_name: &str,
253+
migration: &Migration,
254+
) -> Result<(), MigrateError> {
255+
let _ = conn
256+
.execute(migration.sql.clone())
257+
.await
258+
.map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?;
259+
260+
// language=SQL
261+
let _ = query(AssertSqlSafe(format!(
262+
r#"
263+
DELETE FROM {table_name}
264+
WHERE version = ?1
265+
"#
266+
)))
267+
.bind(migration.version)
268+
.execute(conn)
269+
.await?;
270+
271+
Ok(())
272+
}

0 commit comments

Comments
 (0)