|
| 1 | +use sqlx::migrate::Migrator; |
1 | 2 | use sqlx_sqlite_conn_mgr::{Error, SqliteDatabase, SqliteDatabaseConfig}; |
2 | 3 | use std::sync::Arc; |
| 4 | +use tempfile::TempDir; |
3 | 5 |
|
4 | 6 | #[tokio::test] |
5 | 7 | async fn test_concurrent_reads() { |
@@ -383,3 +385,175 @@ async fn test_concurrent_reads_and_writes() { |
383 | 385 |
|
384 | 386 | db.remove().await.unwrap(); |
385 | 387 | } |
| 388 | + |
| 389 | +/// Helper to create a temp directory with migration files. |
| 390 | +/// Returns (TempDir, Migrator) - TempDir must be kept alive for Migrator to work. |
| 391 | +async fn create_migrations(migrations: &[(&str, &str)]) -> (TempDir, Migrator) { |
| 392 | + let dir = TempDir::new().unwrap(); |
| 393 | + |
| 394 | + for (i, (name, sql)) in migrations.iter().enumerate() { |
| 395 | + let filename = format!("{:04}_{}.sql", i + 1, name.replace(' ', "_")); |
| 396 | + std::fs::write(dir.path().join(filename), sql).unwrap(); |
| 397 | + } |
| 398 | + |
| 399 | + let migrator = Migrator::new(dir.path()).await.unwrap(); |
| 400 | + (dir, migrator) |
| 401 | +} |
| 402 | + |
| 403 | +#[tokio::test] |
| 404 | +async fn test_run_migrations_creates_tables() { |
| 405 | + let path = std::env::current_dir() |
| 406 | + .unwrap() |
| 407 | + .join("test_migrations_creates.db"); |
| 408 | + let db = SqliteDatabase::connect(&path, None).await.unwrap(); |
| 409 | + |
| 410 | + let (_dir, migrator) = create_migrations(&[( |
| 411 | + "create_users", |
| 412 | + "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", |
| 413 | + )]) |
| 414 | + .await; |
| 415 | + |
| 416 | + db.run_migrations(&migrator).await.unwrap(); |
| 417 | + |
| 418 | + // Verify table exists |
| 419 | + let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM sqlite_master WHERE name = 'users'") |
| 420 | + .fetch_one(db.read_pool().unwrap()) |
| 421 | + .await |
| 422 | + .unwrap(); |
| 423 | + |
| 424 | + assert_eq!(count, 1, "users table should exist"); |
| 425 | + |
| 426 | + db.remove().await.unwrap(); |
| 427 | +} |
| 428 | + |
| 429 | +#[tokio::test] |
| 430 | +async fn test_run_migrations_multiple() { |
| 431 | + let path = std::env::current_dir() |
| 432 | + .unwrap() |
| 433 | + .join("test_migrations_multi.db"); |
| 434 | + let db = SqliteDatabase::connect(&path, None).await.unwrap(); |
| 435 | + |
| 436 | + let (_dir, migrator) = create_migrations(&[ |
| 437 | + ( |
| 438 | + "create_users", |
| 439 | + "CREATE TABLE users (id INTEGER PRIMARY KEY);", |
| 440 | + ), |
| 441 | + ( |
| 442 | + "create_posts", |
| 443 | + "CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER);", |
| 444 | + ), |
| 445 | + ( |
| 446 | + "add_index", |
| 447 | + "CREATE INDEX idx_posts_user ON posts(user_id);", |
| 448 | + ), |
| 449 | + ]) |
| 450 | + .await; |
| 451 | + |
| 452 | + db.run_migrations(&migrator).await.unwrap(); |
| 453 | + |
| 454 | + // Verify all migrations applied |
| 455 | + let (count,): (i64,) = sqlx::query_as( |
| 456 | + "SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_sqlx_%'", |
| 457 | + ) |
| 458 | + .fetch_one(db.read_pool().unwrap()) |
| 459 | + .await |
| 460 | + .unwrap(); |
| 461 | + |
| 462 | + assert_eq!(count, 3, "should have 2 tables + 1 index"); |
| 463 | + |
| 464 | + db.remove().await.unwrap(); |
| 465 | +} |
| 466 | + |
| 467 | +#[tokio::test] |
| 468 | +async fn test_run_migrations_idempotent() { |
| 469 | + let path = std::env::current_dir() |
| 470 | + .unwrap() |
| 471 | + .join("test_migrations_idempotent.db"); |
| 472 | + let db = SqliteDatabase::connect(&path, None).await.unwrap(); |
| 473 | + |
| 474 | + let (_dir, migrator) = create_migrations(&[( |
| 475 | + "create_items", |
| 476 | + "CREATE TABLE items (id INTEGER PRIMARY KEY);", |
| 477 | + )]) |
| 478 | + .await; |
| 479 | + |
| 480 | + // Run twice - second should be no-op |
| 481 | + db.run_migrations(&migrator).await.unwrap(); |
| 482 | + db.run_migrations(&migrator).await.unwrap(); |
| 483 | + |
| 484 | + // Verify table exists (no duplicate error) |
| 485 | + let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM sqlite_master WHERE name = 'items'") |
| 486 | + .fetch_one(db.read_pool().unwrap()) |
| 487 | + .await |
| 488 | + .unwrap(); |
| 489 | + |
| 490 | + assert_eq!(count, 1); |
| 491 | + |
| 492 | + db.remove().await.unwrap(); |
| 493 | +} |
| 494 | + |
| 495 | +#[tokio::test] |
| 496 | +async fn test_run_migrations_tracks_in_sqlx_table() { |
| 497 | + let path = std::env::current_dir() |
| 498 | + .unwrap() |
| 499 | + .join("test_migrations_tracking.db"); |
| 500 | + let db = SqliteDatabase::connect(&path, None).await.unwrap(); |
| 501 | + |
| 502 | + let (_dir, migrator) = create_migrations(&[ |
| 503 | + ("first", "CREATE TABLE t1 (id INTEGER);"), |
| 504 | + ("second", "CREATE TABLE t2 (id INTEGER);"), |
| 505 | + ]) |
| 506 | + .await; |
| 507 | + |
| 508 | + db.run_migrations(&migrator).await.unwrap(); |
| 509 | + |
| 510 | + // Verify _sqlx_migrations table has 2 records |
| 511 | + let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_migrations") |
| 512 | + .fetch_one(db.read_pool().unwrap()) |
| 513 | + .await |
| 514 | + .unwrap(); |
| 515 | + |
| 516 | + assert_eq!(count, 2, "should track 2 applied migrations"); |
| 517 | + |
| 518 | + db.remove().await.unwrap(); |
| 519 | +} |
| 520 | + |
| 521 | +#[tokio::test] |
| 522 | +async fn test_run_migrations_on_closed_db_errors() { |
| 523 | + let path = std::env::current_dir() |
| 524 | + .unwrap() |
| 525 | + .join("test_migrations_closed.db"); |
| 526 | + let db = SqliteDatabase::connect(&path, None).await.unwrap(); |
| 527 | + let db_ref = Arc::clone(&db); |
| 528 | + |
| 529 | + db.close().await.unwrap(); |
| 530 | + |
| 531 | + let (_dir, migrator) = create_migrations(&[("noop", "SELECT 1;")]).await; |
| 532 | + let result = db_ref.run_migrations(&migrator).await; |
| 533 | + |
| 534 | + assert!(result.is_err()); |
| 535 | + assert!(matches!(result.unwrap_err(), Error::DatabaseClosed)); |
| 536 | + |
| 537 | + let _ = std::fs::remove_file(&path); |
| 538 | +} |
| 539 | + |
| 540 | +#[tokio::test] |
| 541 | +async fn test_run_migrations_initializes_wal() { |
| 542 | + let path = std::env::current_dir() |
| 543 | + .unwrap() |
| 544 | + .join("test_migrations_wal.db"); |
| 545 | + let db = SqliteDatabase::connect(&path, None).await.unwrap(); |
| 546 | + |
| 547 | + let (_dir, migrator) = create_migrations(&[("init", "CREATE TABLE t (id INTEGER);")]).await; |
| 548 | + db.run_migrations(&migrator).await.unwrap(); |
| 549 | + |
| 550 | + // Check WAL mode is set |
| 551 | + let (mode,): (String,) = sqlx::query_as("PRAGMA journal_mode") |
| 552 | + .fetch_one(db.read_pool().unwrap()) |
| 553 | + .await |
| 554 | + .unwrap(); |
| 555 | + |
| 556 | + assert_eq!(mode.to_lowercase(), "wal"); |
| 557 | + |
| 558 | + db.remove().await.unwrap(); |
| 559 | +} |
0 commit comments