Skip to content

Commit c474591

Browse files
CodingAnarchyclaude
andcommitted
feat(testing): implement template database cloning for MySQL
Add template database cloning optimization to avoid running migrations for every test. When multiple tests use the same migrations, a template database is created once and cloned for each test, significantly speeding up test runs. Implementation details: - Add migrations_hash() to compute SHA256 of migration checksums - Add template_db_name() to generate template database names - Extend TestContext with from_template field to track cloning - Modify setup_test_db() to skip migrations when cloned from template - MySQL: Use mysqldump/mysql for fast cloning with in-process fallback - Add _sqlx_test_templates tracking table with GET_LOCK synchronization - Add SQLX_TEST_NO_TEMPLATE env var to opt out of template cloning - Add comprehensive tests for template functionality - Add template tests to MySQL and MariaDB CI jobs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e8384f2 commit c474591

File tree

7 files changed

+645
-15
lines changed

7 files changed

+645
-15
lines changed

.github/workflows/sqlx.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,17 @@ jobs:
371371
SQLX_OFFLINE_DIR: .sqlx
372372
RUSTFLAGS: --cfg mysql_${{ matrix.mysql }}
373373
374+
# Run template database cloning tests
375+
- run: >
376+
cargo test
377+
--test mysql-template
378+
--no-default-features
379+
--features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }}
380+
env:
381+
DATABASE_URL: mysql://root:password@localhost:3306/sqlx?ssl-mode=disabled
382+
SQLX_OFFLINE_DIR: .sqlx
383+
RUSTFLAGS: --cfg mysql_${{ matrix.mysql }}
384+
374385
# MySQL 5.7 supports TLS but not TLSv1.3 as required by RusTLS.
375386
- if: ${{ !(matrix.mysql == '5_7' && matrix.tls == 'rustls') }}
376387
run: >
@@ -472,6 +483,17 @@ jobs:
472483
SQLX_OFFLINE_DIR: .sqlx
473484
RUSTFLAGS: --cfg mariadb="${{ matrix.mariadb }}"
474485
486+
# Run template database cloning tests
487+
- run: >
488+
cargo test
489+
--test mysql-template
490+
--no-default-features
491+
--features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }}
492+
env:
493+
DATABASE_URL: mysql://root:password@localhost:3306/sqlx
494+
SQLX_OFFLINE_DIR: .sqlx
495+
RUSTFLAGS: --cfg mariadb="${{ matrix.mariadb }}"
496+
475497
# Remove test artifacts
476498
- run: cargo clean -p sqlx
477499

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,11 @@ name = "mysql-test-attr"
391391
path = "tests/mysql/test-attr.rs"
392392
required-features = ["mysql", "macros", "migrate"]
393393

394+
[[test]]
395+
name = "mysql-template"
396+
path = "tests/mysql/template.rs"
397+
required-features = ["mysql", "macros", "migrate"]
398+
394399
[[test]]
395400
name = "mysql-migrate"
396401
path = "tests/mysql/migrate.rs"

sqlx-core/src/testing/mod.rs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use std::future::Future;
22
use std::time::Duration;
33

4-
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
4+
use base64::{
5+
engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD},
6+
Engine as _,
7+
};
58
pub use fixtures::FixtureSnapshot;
6-
use sha2::{Digest, Sha512};
9+
use sha2::{Digest, Sha256, Sha512};
710

811
use crate::connection::{ConnectOptions, Connection};
912
use crate::database::Database;
@@ -14,6 +17,35 @@ use crate::pool::{Pool, PoolConnection, PoolOptions};
1417

1518
mod fixtures;
1619

20+
/// Compute a combined hash of all migrations for template invalidation.
21+
///
22+
/// This hash is used to name template databases. When migrations change,
23+
/// a new hash is generated, resulting in a new template being created.
24+
pub fn migrations_hash(migrator: &Migrator) -> String {
25+
let mut hasher = Sha256::new();
26+
27+
for migration in migrator.iter() {
28+
// Include version, type, and checksum in the hash
29+
hasher.update(migration.version.to_le_bytes());
30+
hasher.update([migration.migration_type as u8]);
31+
hasher.update(&*migration.checksum);
32+
}
33+
34+
let hash = hasher.finalize();
35+
// Use first 16 bytes (128 bits) for a reasonably short but unique name
36+
URL_SAFE_NO_PAD.encode(&hash[..16])
37+
}
38+
39+
/// Generate a template database name from a migrations hash.
40+
///
41+
/// Template names follow the pattern `_sqlx_template_<hash>` and are kept
42+
/// under 63 characters to respect database identifier limits.
43+
pub fn template_db_name(migrations_hash: &str) -> String {
44+
// Replace any characters that might cause issues in database names
45+
let safe_hash = migrations_hash.replace(['-', '+', '/'], "_");
46+
format!("_sqlx_template_{}", safe_hash)
47+
}
48+
1749
pub trait TestSupport: Database {
1850
/// Get parameters to construct a `Pool` suitable for testing.
1951
///
@@ -82,6 +114,9 @@ pub struct TestContext<DB: Database> {
82114
pub pool_opts: PoolOptions<DB>,
83115
pub connect_opts: <DB::Connection as Connection>::Options,
84116
pub db_name: String,
117+
/// Whether this test database was created from a template.
118+
/// When true, migrations have already been applied and should be skipped.
119+
pub from_template: bool,
85120
}
86121

87122
impl<DB, Fut> TestFn for fn(Pool<DB>) -> Fut
@@ -226,7 +261,7 @@ where
226261
.await
227262
.expect("failed to connect to setup test database");
228263

229-
setup_test_db::<DB>(&test_context.connect_opts, &args).await;
264+
setup_test_db::<DB>(&test_context.connect_opts, &args, test_context.from_template).await;
230265

231266
let res = test_fn(test_context.pool_opts, test_context.connect_opts).await;
232267

@@ -246,6 +281,7 @@ where
246281
async fn setup_test_db<DB: Database>(
247282
copts: &<DB::Connection as Connection>::Options,
248283
args: &TestArgs,
284+
from_template: bool,
249285
) where
250286
DB::Connection: Migrate + Sized,
251287
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
@@ -255,11 +291,15 @@ async fn setup_test_db<DB: Database>(
255291
.await
256292
.expect("failed to connect to test database");
257293

258-
if let Some(migrator) = args.migrator {
259-
migrator
260-
.run_direct(None, &mut conn)
261-
.await
262-
.expect("failed to apply migrations");
294+
// Skip migrations if the database was cloned from a template
295+
// (migrations were already applied to the template)
296+
if !from_template {
297+
if let Some(migrator) = args.migrator {
298+
migrator
299+
.run_direct(None, &mut conn)
300+
.await
301+
.expect("failed to apply migrations");
302+
}
263303
}
264304

265305
for fixture in args.fixtures {

0 commit comments

Comments
 (0)