Changeset 0.17.1 (#76)

This commit is contained in:
2026-02-28 01:40:31 +00:00
parent c9141a6896
commit 856dc9b0ac
64 changed files with 8037 additions and 1657 deletions

View File

@@ -16,26 +16,31 @@ var schemaVersion string = "none"
// SchemaVersion returns the current schema version string.
func SchemaVersion() string { return schemaVersion }
// Migrate runs all pending migrations. It creates the schema_migrations
// tracking table if it doesn't exist, then applies each .sql file that
// hasn't been applied yet, in order.
// Migrate runs all pending migrations for the current dialect.
// Migration files live in migrations/<dialect>/ subdirectories.
// Falls back to the root migrations/ directory if no subdirectory exists
// (backward compatible with pre-v0.17.1 postgres-only layouts).
func Migrate() error {
if DB == nil {
return fmt.Errorf("database not connected")
}
// Ensure tracking table exists
_, err := DB.Exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
// Create tracking table — syntax compatible with both drivers.
createMigrations := `CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TEXT DEFAULT (datetime('now'))
)`
if IsPostgres() {
createMigrations = `CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ DEFAULT NOW()
)
`)
if err != nil {
)`
}
if _, err := DB.Exec(createMigrations); err != nil {
return fmt.Errorf("create migrations table: %w", err)
}
// Find migration files
// Locate migration files for current dialect.
migrationsDir := findMigrationsDir()
if migrationsDir == "" {
log.Println("⚠ No migrations directory found — skipping schema migration")
@@ -47,7 +52,6 @@ func Migrate() error {
return fmt.Errorf("read migrations dir: %w", err)
}
// Collect and sort .sql files
var files []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
@@ -62,14 +66,15 @@ func Migrate() error {
}
// Compat: rename old numeric-only version entries to full filenames.
// Earlier extractVersion used regex ^(\d+), recording "001" instead of
// "001_v09_schema.sql". Fix them in-place so CI's db-migrate.sh matches.
for _, file := range files {
prefix := strings.SplitN(file, "_", 2)[0] // "001"
DB.Exec("UPDATE schema_migrations SET version = $1 WHERE version = $2 AND version != $1", file, prefix)
prefix := strings.SplitN(file, "_", 2)[0]
DB.Exec("UPDATE schema_migrations SET version = ? WHERE version = ? AND version != ?", file, prefix, file)
if IsPostgres() {
DB.Exec("UPDATE schema_migrations SET version = $1 WHERE version = $2 AND version != $1", file, prefix)
}
}
// Apply pending migrations
// Apply pending migrations.
applied := 0
for _, file := range files {
version := extractVersion(file)
@@ -77,29 +82,36 @@ func Migrate() error {
continue
}
// Check if already applied
var exists bool
DB.QueryRow("SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = $1)", version).Scan(&exists)
if IsPostgres() {
DB.QueryRow("SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = $1)", version).Scan(&exists)
} else {
DB.QueryRow("SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = ?)", version).Scan(&exists)
}
if exists {
schemaVersion = version
continue
}
// Read and execute
path := filepath.Join(migrationsDir, file)
sql, err := os.ReadFile(path)
sqlBytes, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", file, err)
}
log.Printf(" Applying migration %s...", file)
if _, err := DB.Exec(string(sql)); err != nil {
if _, err := DB.Exec(string(sqlBytes)); err != nil {
return fmt.Errorf("apply %s: %w", file, err)
}
// Record
if _, err := DB.Exec("INSERT INTO schema_migrations (version) VALUES ($1)", version); err != nil {
return fmt.Errorf("record %s: %w", file, err)
if IsPostgres() {
if _, err := DB.Exec("INSERT INTO schema_migrations (version) VALUES ($1)", version); err != nil {
return fmt.Errorf("record %s: %w", file, err)
}
} else {
if _, err := DB.Exec("INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil {
return fmt.Errorf("record %s: %w", file, err)
}
}
schemaVersion = version
@@ -117,38 +129,49 @@ func Migrate() error {
// extractVersion returns the filename as the version key if it's a valid
// migration file (starts with digit, ends with .sql).
// "001_v09_schema.sql" → "001_v09_schema.sql" (matches db-migrate.sh convention)
func extractVersion(filename string) string {
// Use full filename as version to match db-migrate.sh convention
if !strings.HasSuffix(filename, ".sql") {
return ""
}
// Must start with a digit (e.g., 001_v09_schema.sql)
if len(filename) == 0 || filename[0] < '0' || filename[0] > '9' {
return ""
}
return filename
}
// findMigrationsDir locates the migrations directory.
// Checks relative to the binary, then relative to the source file.
// findMigrationsDir locates the dialect-specific migrations directory.
// Looks for migrations/<dialect>/ first, then falls back to migrations/.
func findMigrationsDir() string {
candidates := []string{
dialectDir := CurrentDialect.String() // "postgres" or "sqlite"
// Candidates: dialect-specific first, then generic.
bases := []string{
"database/migrations",
"server/database/migrations",
"../database/migrations",
}
// Also check relative to this source file (for tests)
// Also check relative to this source file (for tests).
_, thisFile, _, ok := runtime.Caller(0)
if ok {
dir := filepath.Dir(thisFile)
candidates = append(candidates, filepath.Join(dir, "migrations"))
bases = append(bases, filepath.Join(dir, "migrations"))
}
for _, c := range candidates {
if info, err := os.Stat(c); err == nil && info.IsDir() {
return c
// Try dialect-specific subdir first.
for _, base := range bases {
candidate := filepath.Join(base, dialectDir)
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
return candidate
}
}
// Fall back to root migrations/ (backward compat for postgres).
if IsPostgres() {
for _, base := range bases {
if info, err := os.Stat(base); err == nil && info.IsDir() {
return base
}
}
}