package db import ( "context" "database/sql" "errors" "slices" "testing" ) // openMemory opens an isolated in-memory DB for a test. func openMemory(t *testing.T) *sql.DB { t.Helper() ctx := context.Background() db, err := Open(ctx, ":memory:", testLogger()) if err != nil { t.Fatalf("Open(:memory:) failed: %v", err) } t.Cleanup(func() { db.Close() }) return db } // registeredMigrations returns the ids currently recorded in schema_migrations. func registeredMigrations(t *testing.T, ctx context.Context, db *sql.DB) []string { t.Helper() rows, err := db.QueryContext(ctx, "SELECT id FROM schema_migrations") if err != nil { t.Fatalf("query schema_migrations: %v", err) } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { t.Fatalf("scan migration id: %v", err) } ids = append(ids, id) } if err := rows.Err(); err != nil { t.Fatalf("iterate schema_migrations: %v", err) } return ids } // upMigrationIDs returns the ids of the embedded up-migrations in apply order. func upMigrationIDs(t *testing.T) []string { t.Helper() migs, err := loadMigrations() if err != nil { t.Fatalf("loadMigrations: %v", err) } ids := make([]string, len(migs)) for i, m := range migs { ids[i] = m.id } return ids } // appMetaExists reports whether the app_meta table is present. func appMetaExists(t *testing.T, ctx context.Context, db *sql.DB) bool { t.Helper() var n int if err := db.QueryRowContext(ctx, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='app_meta'").Scan(&n); err != nil { t.Fatalf("check app_meta presence: %v", err) } return n == 1 } func TestMigrateUp_FirstApply_正常(t *testing.T) { t.Parallel() ctx := context.Background() db := openMemory(t) if err := MigrateUp(ctx, db, testLogger()); err != nil { t.Fatalf("MigrateUp first call: %v", err) } // Applies every embedded up-migration, in order. ids := registeredMigrations(t, ctx, db) if want := upMigrationIDs(t); !slices.Equal(ids, want) { t.Fatalf("schema_migrations = %v, want %v", ids, want) } if !appMetaExists(t, ctx, db) { t.Fatal("app_meta table should exist after MigrateUp") } // applied_at must be recorded on the first migration row. var appliedAt string if err := db.QueryRowContext(ctx, "SELECT applied_at FROM schema_migrations WHERE id = ?", "0001_init").Scan(&appliedAt); err != nil { t.Fatalf("read applied_at: %v", err) } if appliedAt == "" { t.Fatal("applied_at should not be empty") } } func TestMigrateUp_Idempotent_正常(t *testing.T) { t.Parallel() ctx := context.Background() db := openMemory(t) if err := MigrateUp(ctx, db, testLogger()); err != nil { t.Fatalf("MigrateUp first call: %v", err) } // Second call must not re-execute or duplicate the records. if err := MigrateUp(ctx, db, testLogger()); err != nil { t.Fatalf("MigrateUp second call: %v", err) } want := upMigrationIDs(t) ids := registeredMigrations(t, ctx, db) if !slices.Equal(ids, want) { t.Fatalf("idempotent: schema_migrations = %v, want %v", ids, want) } if !appMetaExists(t, ctx, db) { t.Fatal("app_meta should still exist after idempotent re-run") } } func TestMigrateDown_ThenReUp_正常(t *testing.T) { t.Parallel() ctx := context.Background() db := openMemory(t) want := upMigrationIDs(t) if err := MigrateUp(ctx, db, testLogger()); err != nil { t.Fatalf("MigrateUp: %v", err) } // Roll back every applied migration. if err := MigrateDown(ctx, db, len(want), testLogger()); err != nil { t.Fatalf("MigrateDown(all): %v", err) } if appMetaExists(t, ctx, db) { t.Fatal("app_meta should be dropped after MigrateDown(all)") } if ids := registeredMigrations(t, ctx, db); len(ids) != 0 { t.Fatalf("schema_migrations should be empty after rollback, got %v", ids) } // Re-applying must work end to end again. if err := MigrateUp(ctx, db, testLogger()); err != nil { t.Fatalf("re-MigrateUp: %v", err) } if !appMetaExists(t, ctx, db) { t.Fatal("app_meta should exist again after re-MigrateUp") } if ids := registeredMigrations(t, ctx, db); !slices.Equal(ids, want) { t.Fatalf("schema_migrations = %v, want %v", ids, want) } } func TestMigrateDown_ZeroSteps_异常(t *testing.T) { t.Parallel() ctx := context.Background() db := openMemory(t) if err := MigrateUp(ctx, db, testLogger()); err != nil { t.Fatalf("MigrateUp: %v", err) } for _, steps := range []int{0, -1} { if err := MigrateDown(ctx, db, steps, testLogger()); !errors.Is(err, ErrInvalidSteps) { t.Fatalf("MigrateDown(%d) err = %v, want wrap of ErrInvalidSteps", steps, err) } } } func TestMigrateDown_StepsBeyondApplied_边界(t *testing.T) { t.Parallel() ctx := context.Background() db := openMemory(t) if err := MigrateUp(ctx, db, testLogger()); err != nil { t.Fatalf("MigrateUp: %v", err) } // More steps than applied must roll everything back without error. if err := MigrateDown(ctx, db, 10, testLogger()); err != nil { t.Fatalf("MigrateDown(10): %v", err) } if appMetaExists(t, ctx, db) { t.Fatal("app_meta should be dropped when rolling back all") } if ids := registeredMigrations(t, ctx, db); len(ids) != 0 { t.Fatalf("schema_migrations should be empty, got %v", ids) } } func TestAppMeta_ReadWrite_正常(t *testing.T) { t.Parallel() ctx := context.Background() db := openMemory(t) if err := MigrateUp(ctx, db, testLogger()); err != nil { t.Fatalf("MigrateUp: %v", err) } // Parameterized insert. if _, err := db.ExecContext(ctx, "INSERT INTO app_meta (key, value) VALUES (?, ?)", "schema_version", "1"); err != nil { t.Fatalf("insert app_meta: %v", err) } // Parameterized read. var value string if err := db.QueryRowContext(ctx, "SELECT value FROM app_meta WHERE key = ?", "schema_version").Scan(&value); err != nil { t.Fatalf("read app_meta: %v", err) } if value != "1" { t.Fatalf("value = %q, want %q", value, "1") } // Parameterized update. if _, err := db.ExecContext(ctx, "UPDATE app_meta SET value = ? WHERE key = ?", "2", "schema_version"); err != nil { t.Fatalf("update app_meta: %v", err) } if err := db.QueryRowContext(ctx, "SELECT value FROM app_meta WHERE key = ?", "schema_version").Scan(&value); err != nil { t.Fatalf("read updated app_meta: %v", err) } if value != "2" { t.Fatalf("value after update = %q, want %q", value, "2") } // Parameterized delete. if _, err := db.ExecContext(ctx, "DELETE FROM app_meta WHERE key = ?", "schema_version"); err != nil { t.Fatalf("delete app_meta: %v", err) } var n int if err := db.QueryRowContext(ctx, "SELECT count(*) FROM app_meta WHERE key = ?", "schema_version").Scan(&n); err != nil { t.Fatalf("count app_meta after delete: %v", err) } if n != 0 { t.Fatalf("expected 0 rows after delete, got %d", n) } }