1+ use sqlx:: migrate:: Migrator ;
12use sqlx_sqlite_conn_mgr:: { Error , SqliteDatabase , SqliteDatabaseConfig } ;
23use std:: sync:: Arc ;
4+ use tempfile:: TempDir ;
35
46#[ tokio:: test]
57async fn test_concurrent_reads ( ) {
@@ -30,6 +32,7 @@ async fn test_concurrent_reads() {
3032 . fetch_one ( db. read_pool ( ) . unwrap ( ) )
3133 . await
3234 . unwrap ( ) ;
35+
3336 assert_eq ! ( count, 12 ) ;
3437
3538 active. fetch_sub ( 1 , Ordering :: SeqCst ) ;
@@ -292,6 +295,7 @@ async fn test_write_serialization() {
292295 . execute ( & mut * w)
293296 . await
294297 . unwrap ( ) ;
298+
295299 active. fetch_sub ( 1 , Ordering :: SeqCst ) ;
296300 } )
297301 } )
@@ -348,6 +352,7 @@ async fn test_concurrent_reads_and_writes() {
348352 . execute ( & mut * w)
349353 . await
350354 . unwrap ( ) ;
355+
351356 write_active. store ( false , Ordering :: SeqCst ) ;
352357 } )
353358 } ;
@@ -366,6 +371,7 @@ async fn test_concurrent_reads_and_writes() {
366371 . fetch_one ( db. read_pool ( ) . unwrap ( ) )
367372 . await
368373 . unwrap ( ) ;
374+
369375 if write_active. load ( Ordering :: SeqCst ) {
370376 read_during_write. store ( true , Ordering :: SeqCst ) ;
371377 }
@@ -383,3 +389,155 @@ async fn test_concurrent_reads_and_writes() {
383389
384390 db. remove ( ) . await . unwrap ( ) ;
385391}
392+
393+ /// Helper to create a temp directory with migration files.
394+ /// Returns (TempDir, Migrator) - TempDir must be kept alive for Migrator to work.
395+ async fn create_migrations ( migrations : & [ ( & str , & str ) ] ) -> ( TempDir , Migrator ) {
396+ let dir = TempDir :: new ( ) . unwrap ( ) ;
397+
398+ for ( i, ( name, sql) ) in migrations. iter ( ) . enumerate ( ) {
399+ let filename = format ! ( "{:04}_{}.sql" , i + 1 , name. replace( ' ' , "_" ) ) ;
400+ std:: fs:: write ( dir. path ( ) . join ( filename) , sql) . unwrap ( ) ;
401+ }
402+
403+ let migrator = Migrator :: new ( dir. path ( ) ) . await . unwrap ( ) ;
404+ ( dir, migrator)
405+ }
406+
407+ #[ tokio:: test]
408+ async fn test_run_migrations_creates_schema ( ) {
409+ let path = std:: env:: current_dir ( )
410+ . unwrap ( )
411+ . join ( "test_migrations_multi.db" ) ;
412+
413+ let db = SqliteDatabase :: connect ( & path, None ) . await . unwrap ( ) ;
414+
415+ let ( _dir, migrator) = create_migrations ( & [
416+ (
417+ "create_users" ,
418+ "CREATE TABLE users (id INTEGER PRIMARY KEY);" ,
419+ ) ,
420+ (
421+ "create_posts" ,
422+ "CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER);" ,
423+ ) ,
424+ (
425+ "add_index" ,
426+ "CREATE INDEX idx_posts_user ON posts(user_id);" ,
427+ ) ,
428+ ] )
429+ . await ;
430+
431+ db. run_migrations ( & migrator) . await . unwrap ( ) ;
432+
433+ // Verify all migrations applied
434+ let ( count, ) : ( i64 , ) = sqlx:: query_as (
435+ "SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table', 'index') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_sqlx_%'" ,
436+ )
437+ . fetch_one ( db. read_pool ( ) . unwrap ( ) )
438+ . await
439+ . unwrap ( ) ;
440+
441+ assert_eq ! ( count, 3 , "should have 2 tables + 1 index" ) ;
442+
443+ db. remove ( ) . await . unwrap ( ) ;
444+ }
445+
446+ #[ tokio:: test]
447+ async fn test_run_migrations_idempotent ( ) {
448+ let path = std:: env:: current_dir ( )
449+ . unwrap ( )
450+ . join ( "test_migrations_idempotent.db" ) ;
451+
452+ let db = SqliteDatabase :: connect ( & path, None ) . await . unwrap ( ) ;
453+
454+ let ( _dir, migrator) = create_migrations ( & [ (
455+ "create_items" ,
456+ "CREATE TABLE items (id INTEGER PRIMARY KEY);" ,
457+ ) ] )
458+ . await ;
459+
460+ // Run twice - second should be no-op
461+ db. run_migrations ( & migrator) . await . unwrap ( ) ;
462+ db. run_migrations ( & migrator) . await . unwrap ( ) ;
463+
464+ // Verify table exists (no duplicate error)
465+ let ( count, ) : ( i64 , ) = sqlx:: query_as ( "SELECT COUNT(*) FROM sqlite_master WHERE name = 'items'" )
466+ . fetch_one ( db. read_pool ( ) . unwrap ( ) )
467+ . await
468+ . unwrap ( ) ;
469+
470+ assert_eq ! ( count, 1 ) ;
471+
472+ db. remove ( ) . await . unwrap ( ) ;
473+ }
474+
475+ #[ tokio:: test]
476+ async fn test_run_migrations_tracks_in_sqlx_table ( ) {
477+ let path = std:: env:: current_dir ( )
478+ . unwrap ( )
479+ . join ( "test_migrations_tracking.db" ) ;
480+
481+ let db = SqliteDatabase :: connect ( & path, None ) . await . unwrap ( ) ;
482+
483+ let ( _dir, migrator) = create_migrations ( & [
484+ ( "first" , "CREATE TABLE t1 (id INTEGER);" ) ,
485+ ( "second" , "CREATE TABLE t2 (id INTEGER);" ) ,
486+ ] )
487+ . await ;
488+
489+ db. run_migrations ( & migrator) . await . unwrap ( ) ;
490+
491+ // Verify _sqlx_migrations table has 2 records
492+ let ( count, ) : ( i64 , ) = sqlx:: query_as ( "SELECT COUNT(*) FROM _sqlx_migrations" )
493+ . fetch_one ( db. read_pool ( ) . unwrap ( ) )
494+ . await
495+ . unwrap ( ) ;
496+
497+ assert_eq ! ( count, 2 , "should track 2 applied migrations" ) ;
498+
499+ db. remove ( ) . await . unwrap ( ) ;
500+ }
501+
502+ #[ tokio:: test]
503+ async fn test_run_migrations_on_closed_db_errors ( ) {
504+ let path = std:: env:: current_dir ( )
505+ . unwrap ( )
506+ . join ( "test_migrations_closed.db" ) ;
507+
508+ let db = SqliteDatabase :: connect ( & path, None ) . await . unwrap ( ) ;
509+ let db_ref = Arc :: clone ( & db) ;
510+
511+ db. close ( ) . await . unwrap ( ) ;
512+
513+ let ( _dir, migrator) = create_migrations ( & [ ( "noop" , "SELECT 1;" ) ] ) . await ;
514+ let result = db_ref. run_migrations ( & migrator) . await ;
515+
516+ assert ! ( result. is_err( ) ) ;
517+ assert ! ( matches!( result. unwrap_err( ) , Error :: DatabaseClosed ) ) ;
518+
519+ let _ = std:: fs:: remove_file ( & path) ;
520+ }
521+
522+ #[ tokio:: test]
523+ async fn test_run_migrations_with_invalid_sql_fails ( ) {
524+ let path = std:: env:: current_dir ( )
525+ . unwrap ( )
526+ . join ( "test_migrations_invalid.db" ) ;
527+
528+ let db = SqliteDatabase :: connect ( & path, None ) . await . unwrap ( ) ;
529+
530+ // Create migration with invalid SQL syntax
531+ let ( _dir, migrator) = create_migrations ( & [
532+ ( "valid" , "CREATE TABLE users (id INTEGER PRIMARY KEY);" ) ,
533+ ( "invalid" , "THIS IS NOT VALID SQL SYNTAX" ) ,
534+ ] )
535+ . await ;
536+
537+ let result = db. run_migrations ( & migrator) . await ;
538+
539+ assert ! ( result. is_err( ) ) ;
540+ assert ! ( matches!( result. unwrap_err( ) , Error :: Migration ( _) ) ) ;
541+
542+ db. remove ( ) . await . unwrap ( ) ;
543+ }
0 commit comments