chore: auto-commit (57 archivos)
- frontend/functions/core/format_datetime_short.md - frontend/functions/core/format_datetime_short.test.ts - frontend/functions/core/format_datetime_short.ts - frontend/functions/core/format_duration.md - frontend/functions/core/format_duration.test.ts - frontend/functions/core/format_duration.ts - frontend/functions/core/month_grid.md - frontend/functions/core/month_grid.test.ts - frontend/functions/core/month_grid.ts - frontend/functions/core/string_hash_palette.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ApplyMigrations reads SQL files matching glob from fsys, sorts them
|
||||
// lexicographically (NNN_name.sql order), and executes every statement
|
||||
// found in each file against conn.
|
||||
//
|
||||
// If glob is empty, it defaults to "migrations/*.sql".
|
||||
//
|
||||
// Each statement is executed individually. Errors that look like
|
||||
// idempotent SQLite errors (duplicate column, already exists) are
|
||||
// silently ignored so that migrations can be replayed safely against
|
||||
// a database that was partially migrated.
|
||||
//
|
||||
// NOTE: the internal statement parser splits on ";" at the end of a
|
||||
// trimmed line. This is intentionally simple and will break if SQL
|
||||
// strings contain a literal semicolon at end-of-line. Avoid using
|
||||
// such patterns in migration files.
|
||||
func ApplyMigrations(conn *sql.DB, fsys fs.FS, glob string) error {
|
||||
if glob == "" {
|
||||
glob = "migrations/*.sql"
|
||||
}
|
||||
files, err := fs.Glob(fsys, glob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Strings(files)
|
||||
for _, f := range files {
|
||||
b, err := fs.ReadFile(fsys, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, stmt := range splitSQLStatements(string(b)) {
|
||||
s := strings.TrimSpace(stmt)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := conn.Exec(s); err != nil {
|
||||
if isIdempotentMigrationError(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s: %w", f, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitSQLStatements splits a SQL script into individual statements.
|
||||
// Lines starting with "--" (comments) and blank lines are skipped.
|
||||
// A statement ends when a trimmed line ends with ";".
|
||||
func splitSQLStatements(s string) []string {
|
||||
out := []string{}
|
||||
cur := strings.Builder{}
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
trim := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trim, "--") || trim == "" {
|
||||
continue
|
||||
}
|
||||
cur.WriteString(line)
|
||||
cur.WriteString("\n")
|
||||
if strings.HasSuffix(trim, ";") {
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// isIdempotentMigrationError returns true for SQLite errors that arise
|
||||
// from re-applying a migration that was already applied (duplicate
|
||||
// column, table/index already exists).
|
||||
func isIdempotentMigrationError(err error) bool {
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, "duplicate column") ||
|
||||
strings.Contains(msg, "already exists")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user