- Add dependency-free in-process schema migrator (internal/store/migrate.go) that applies sql/schema/*.sql in order, tracking versions in schema_migrations. Gated by MIGRATE_ON_START and MIGRATIONS_DIR (default sql/schema). - Fix migration ordering: 002 now creates the 'vector' extension before defining the user_style_profiles table (previously the extension was only created later in 003). - Bound concurrent background enrichment with a 4-slot semaphore so bursts of live getStyleProfile calls cannot overwhelm Ollama. - buildStyleProfile degrades gracefully when Ollama is down: stores a zero embedding and a keyword-based fallback summary instead of failing the whole request. - Add a /readyz side server (READYZ_PORT, default 9090) that checks DB connectivity and Ollama availability. Co-Authored-By: Claude (AI assistant) <noreply@anthropic.com>
106 lines
2.9 KiB
Go
106 lines
2.9 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Migrate applies every *.sql file in dir (in filename order). Each file is expected to
|
|
// contain a "-- +goose up" section (between that marker and "-- +goose down", or EOF) which
|
|
// is executed inside a single transaction. Applied versions are recorded in
|
|
// schema_migrations so re-running is a no-op.
|
|
func Migrate(ctx context.Context, db *sql.DB, dir string) error {
|
|
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
version TEXT PRIMARY KEY,
|
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
)`); err != nil {
|
|
return fmt.Errorf("create schema_migrations table: %w", err)
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return fmt.Errorf("read migrations dir %q: %w", dir, err)
|
|
}
|
|
|
|
type migration struct{ name, up string }
|
|
migrations := make([]migration, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
|
|
continue
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
|
if err != nil {
|
|
return fmt.Errorf("read migration %q: %w", e.Name(), err)
|
|
}
|
|
migrations = append(migrations, migration{name: e.Name(), up: extractUpSQL(string(raw))})
|
|
}
|
|
sort.Slice(migrations, func(i, j int) bool { return migrations[i].name < migrations[j].name })
|
|
|
|
for _, m := range migrations {
|
|
if strings.TrimSpace(m.up) == "" {
|
|
continue
|
|
}
|
|
applied, err := versionApplied(ctx, db, m.name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if applied {
|
|
continue
|
|
}
|
|
if err := applyMigration(ctx, db, m.name, m.up); err != nil {
|
|
return fmt.Errorf("apply migration %q: %w", m.name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractUpSQL returns the SQL between the "-- +goose up" marker and the next
|
|
// "-- +goose down" marker (or end of file).
|
|
func extractUpSQL(content string) string {
|
|
var b strings.Builder
|
|
inUp := false
|
|
for _, line := range strings.Split(content, "\n") {
|
|
switch strings.TrimSpace(line) {
|
|
case "-- +goose up":
|
|
inUp = true
|
|
continue
|
|
case "-- +goose down":
|
|
inUp = false
|
|
continue
|
|
}
|
|
if inUp {
|
|
b.WriteString(line)
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func versionApplied(ctx context.Context, db *sql.DB, version string) (bool, error) {
|
|
var count int
|
|
if err := db.QueryRowContext(ctx, `SELECT count(*) FROM schema_migrations WHERE version = $1`, version).Scan(&count); err != nil {
|
|
return false, fmt.Errorf("check migration %q: %w", version, err)
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func applyMigration(ctx context.Context, db *sql.DB, version, up string) error {
|
|
tx, err := db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.ExecContext(ctx, up); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `INSERT INTO schema_migrations (version) VALUES ($1)`, version); err != nil {
|
|
_ = tx.Rollback()
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|