homelab/services/ha-sync/internal/db/db.go
Dan V deb6c38d7b chore: commit homelab setup — deployment, services, orchestration, skill
- Add .gitignore: exclude compiled binaries, build artifacts, and Helm
  values files containing real secrets (authentik, prometheus)
- Add all Kubernetes deployment manifests (deployment/)
- Add services source code: ha-sync, device-inventory, games-console,
  paperclip, parts-inventory
- Add Ansible orchestration: playbooks, roles, inventory, cloud-init
- Add hardware specs, execution plans, scripts, HOMELAB.md
- Add skills/homelab/SKILL.md + skills/install.sh to preserve Copilot skill
- Remove previously-tracked inventory-cli binary from git index

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 08:10:32 +02:00

129 lines
4.4 KiB
Go

package db
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func Connect(dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("db open: %w", err)
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("db ping: %w", err)
}
if err := Migrate(db); err != nil {
db.Close()
return nil, fmt.Errorf("db migrate: %w", err)
}
return db, nil
}
func Migrate(db *sql.DB) error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS sync_iterations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
sync_pair VARCHAR(255) NOT NULL,
direction VARCHAR(64) NOT NULL,
src VARCHAR(512) NOT NULL,
dest VARCHAR(512) NOT NULL,
started_at DATETIME(3) NOT NULL,
ended_at DATETIME(3),
status ENUM('running','success','partial_failure','failed') NOT NULL DEFAULT 'running',
dry_run TINYINT(1) NOT NULL DEFAULT 0,
files_created INT DEFAULT 0,
files_updated INT DEFAULT 0,
files_deleted INT DEFAULT 0,
files_skipped INT DEFAULT 0,
files_failed INT DEFAULT 0,
total_bytes_transferred BIGINT DEFAULT 0,
error_message TEXT,
INDEX idx_pair (sync_pair),
INDEX idx_started (started_at),
INDEX idx_dry_run (dry_run)
)`,
`CREATE TABLE IF NOT EXISTS sync_operations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
iteration_id BIGINT NOT NULL,
dry_run TINYINT(1) NOT NULL DEFAULT 0,
operation ENUM('create','update','delete') NOT NULL,
filepath VARCHAR(4096) NOT NULL,
size_before BIGINT,
size_after BIGINT,
md5_before VARCHAR(32),
md5_after VARCHAR(32),
started_at DATETIME(3) NOT NULL,
ended_at DATETIME(3),
status ENUM('success','fail') NOT NULL,
error_message VARCHAR(4096),
INDEX idx_iteration (iteration_id),
CONSTRAINT fk_iteration FOREIGN KEY (iteration_id) REFERENCES sync_iterations(id)
)`,
`CREATE TABLE IF NOT EXISTS sync_jobs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
pair VARCHAR(255) NOT NULL,
direction VARCHAR(64) NOT NULL,
src VARCHAR(512) NOT NULL,
dest VARCHAR(512) NOT NULL,
src_host VARCHAR(255) NOT NULL DEFAULT 'dell',
dest_host VARCHAR(255) NOT NULL DEFAULT 'hp',
cron_schedule VARCHAR(64) NOT NULL DEFAULT '*/15 * * * *',
lock_ttl_seconds INT NOT NULL DEFAULT 3600,
dry_run TINYINT(1) NOT NULL DEFAULT 0,
delete_missing TINYINT(1) NOT NULL DEFAULT 0,
workers INT NOT NULL DEFAULT 4,
mtime_threshold VARCHAR(32) NOT NULL DEFAULT '2s',
excludes TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
INDEX idx_jobs_pair (pair),
INDEX idx_jobs_enabled (enabled)
)`,
}
for _, s := range stmts {
if _, err := db.Exec(s); err != nil {
return fmt.Errorf("migrate: %w", err)
}
}
// Idempotent: add new columns to sync_operations if they don't exist.
for _, col := range []struct{ name, def string }{
{"src_host", "VARCHAR(255) DEFAULT NULL"},
{"dest_host", "VARCHAR(255) DEFAULT NULL"},
{"owner", "VARCHAR(255) DEFAULT NULL"},
} {
if err := addColumnIfNotExists(db, "sync_operations", col.name, col.def); err != nil {
return err
}
}
return nil
}
// addColumnIfNotExists adds a column to a table only if it does not already exist,
// avoiding the MySQL "duplicate column" error on repeated migrations.
func addColumnIfNotExists(db *sql.DB, table, column, definition string) error {
var count int
err := db.QueryRow(`
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?`, table, column).Scan(&count)
if err != nil {
return fmt.Errorf("check column %s.%s: %w", table, column, err)
}
if count > 0 {
return nil
}
_, err = db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, definition))
if err != nil {
return fmt.Errorf("add column %s.%s: %w", table, column, err)
}
return nil
}