package repository import ( "context" "embed" "fmt" "log" "strings" "time" "github.com/jmoiron/sqlx" _ "github.com/microsoft/go-mssqldb" ) //go:embed migrations/*.sql var migrations embed.FS func Connect(dsn string) (*sqlx.DB, error) { // Extract base DSN without the database name for initial connection to master masterDSN := strings.Replace(dsn, "database=workorders", "database=master", 1) // Retry loop — MSSQL takes time to start var db *sqlx.DB var err error for i := range 20 { db, err = sqlx.Open("sqlserver", masterDSN) if err == nil { if pingErr := db.PingContext(context.Background()); pingErr == nil { break } db.Close() } log.Printf("waiting for MSSQL (%d/20)...", i+1) time.Sleep(3 * time.Second) } if err != nil { return nil, fmt.Errorf("connect to master: %w", err) } // Create the workorders database if it doesn't exist _, err = db.Exec(` IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'workorders') CREATE DATABASE workorders; `) if err != nil { return nil, fmt.Errorf("create database: %w", err) } db.Close() // Reconnect to the workorders database db, err = sqlx.Open("sqlserver", dsn) if err != nil { return nil, fmt.Errorf("connect to workorders: %w", err) } if err = db.PingContext(context.Background()); err != nil { return nil, fmt.Errorf("ping workorders: %w", err) } db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) return db, nil } func RunMigrations(db *sqlx.DB) error { // Create migrations tracking table _, err := db.Exec(` IF NOT EXISTS ( SELECT * FROM sys.tables WHERE name = 'schema_migrations' ) CREATE TABLE schema_migrations ( filename NVARCHAR(255) PRIMARY KEY, applied_at DATETIME2 NOT NULL DEFAULT GETUTCDATE() ); `) if err != nil { return fmt.Errorf("create schema_migrations: %w", err) } entries, err := migrations.ReadDir("migrations") if err != nil { return fmt.Errorf("read migrations dir: %w", err) } for _, entry := range entries { if !strings.HasSuffix(entry.Name(), ".sql") { continue } var count int _ = db.QueryRow(`SELECT COUNT(1) FROM schema_migrations WHERE filename = @p1`, entry.Name()).Scan(&count) if count > 0 { continue } content, err := migrations.ReadFile("migrations/" + entry.Name()) if err != nil { return fmt.Errorf("read %s: %w", entry.Name(), err) } // Split on GO statements for multi-batch SQL statements := splitSQL(string(content)) for _, stmt := range statements { stmt = strings.TrimSpace(stmt) if stmt == "" { continue } if _, err := db.Exec(stmt); err != nil { return fmt.Errorf("migration %s: %w", entry.Name(), err) } } _, err = db.Exec(`INSERT INTO schema_migrations (filename) VALUES (@p1)`, entry.Name()) if err != nil { return fmt.Errorf("record migration %s: %w", entry.Name(), err) } log.Printf("applied migration: %s", entry.Name()) } return nil } func splitSQL(sql string) []string { var stmts []string for _, part := range strings.Split(sql, "\nGO") { if s := strings.TrimSpace(part); s != "" { stmts = append(stmts, s) } } if len(stmts) == 0 { stmts = []string{sql} } return stmts }