135 lines
3.2 KiB
Go
135 lines
3.2 KiB
Go
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
|
|
}
|