All checks were successful
Check / check (pull_request) Successful in 3m12s
Refactors the migration system to follow the pixa pattern: - Add 000.sql bootstrap migration that creates schema_migrations with INTEGER PRIMARY KEY version column - Go code no longer creates the migrations table inline; it reads and executes 000.sql as a bootstrap step before the normal migration loop - Export ParseMigrationVersion and ApplyMigrations for test use - Add legacy TEXT-to-INTEGER conversion for existing databases that stored migration versions as filenames (e.g. '001_initial.sql') - Wrap individual migration application in transactions for safety - Add comprehensive tests for version parsing, fresh database bootstrap, idempotent re-application, and legacy conversion
193 lines
4.8 KiB
Go
193 lines
4.8 KiB
Go
package database_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"sneak.berlin/go/upaas/internal/database"
|
|
)
|
|
|
|
func TestParseMigrationVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
filename string
|
|
wantVersion int
|
|
wantErr bool
|
|
}{
|
|
{filename: "000.sql", wantVersion: 0},
|
|
{filename: "001_initial.sql", wantVersion: 1},
|
|
{filename: "002_remove_container_id.sql", wantVersion: 2},
|
|
{filename: "007_add_resource_limits.sql", wantVersion: 7},
|
|
{filename: "100_large_version.sql", wantVersion: 100},
|
|
{filename: ".sql", wantErr: true},
|
|
{filename: "_foo.sql", wantErr: true},
|
|
{filename: "abc_foo.sql", wantErr: true},
|
|
{filename: "1a2_bad.sql", wantErr: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
version, err := database.ParseMigrationVersion(tt.filename)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantVersion, version)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApplyMigrationsFreshDatabase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, err := sql.Open("sqlite3", ":memory:?_foreign_keys=on")
|
|
require.NoError(t, err)
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
ctx := context.Background()
|
|
|
|
err = database.ApplyMigrations(ctx, db, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify schema_migrations table exists with INTEGER version column.
|
|
var version int
|
|
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT version FROM schema_migrations WHERE version = 0",
|
|
).Scan(&version)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, version)
|
|
|
|
// Verify that all migrations were recorded.
|
|
var count int
|
|
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM schema_migrations",
|
|
).Scan(&count)
|
|
require.NoError(t, err)
|
|
// 000 bootstrap + 001 through 007 = 8 entries.
|
|
assert.Equal(t, 8, count)
|
|
|
|
// Verify application tables were created by the migrations.
|
|
var tableCount int
|
|
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users'",
|
|
).Scan(&tableCount)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, tableCount)
|
|
}
|
|
|
|
func TestApplyMigrationsIdempotent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, err := sql.Open("sqlite3", ":memory:?_foreign_keys=on")
|
|
require.NoError(t, err)
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Apply twice — second run should be a no-op.
|
|
err = database.ApplyMigrations(ctx, db, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = database.ApplyMigrations(ctx, db, nil)
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM schema_migrations",
|
|
).Scan(&count)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 8, count)
|
|
}
|
|
|
|
func TestApplyMigrationsLegacyConversion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, err := sql.Open("sqlite3", ":memory:?_foreign_keys=on")
|
|
require.NoError(t, err)
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Simulate the old TEXT-based schema_migrations table.
|
|
_, err = db.ExecContext(ctx, `
|
|
CREATE TABLE schema_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`)
|
|
require.NoError(t, err)
|
|
|
|
// Insert legacy filename-based versions (as if migrations 001-007 were applied).
|
|
legacyVersions := []string{
|
|
"001_initial.sql",
|
|
"002_remove_container_id.sql",
|
|
"003_add_ports.sql",
|
|
"004_add_commit_url.sql",
|
|
"005_add_webhook_secret_hash.sql",
|
|
"006_add_previous_image_id.sql",
|
|
"007_add_resource_limits.sql",
|
|
}
|
|
|
|
for _, v := range legacyVersions {
|
|
_, err = db.ExecContext(ctx,
|
|
"INSERT INTO schema_migrations (version) VALUES (?)", v)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Also create the application tables that would exist on a real database
|
|
// so that the migrations (001-007) don't fail when re-applied.
|
|
// Actually, since the legacy versions will be converted and recognized
|
|
// as already-applied, the DDL migrations won't re-run. We just need
|
|
// to make sure ApplyMigrations succeeds.
|
|
|
|
// Run ApplyMigrations — this should convert legacy versions and
|
|
// skip all already-applied migrations.
|
|
err = database.ApplyMigrations(ctx, db, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify version 0 is now present (from bootstrap).
|
|
var zeroVersion int
|
|
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT version FROM schema_migrations WHERE version = 0",
|
|
).Scan(&zeroVersion)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, zeroVersion)
|
|
|
|
// Verify all 8 versions are present (0 through 7).
|
|
var count int
|
|
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM schema_migrations",
|
|
).Scan(&count)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 8, count)
|
|
|
|
// Verify versions are stored as integers, not text filenames.
|
|
var maxVersion int
|
|
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT MAX(version) FROM schema_migrations",
|
|
).Scan(&maxVersion)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 7, maxVersion)
|
|
}
|