Files
cloud-ip-validator/internal/db/db.go
T
2026-08-21 07:34:45 +03:00

86 lines
2.3 KiB
Go

// Package db owns the SQLite connection, schema migrations, and all queries
// used by the Control API. It is the only package in the system that talks
// to the database directly — agents and probers never connect to it.
package db
import (
"context"
"database/sql"
_ "embed"
"fmt"
"time"
_ "modernc.org/sqlite"
)
//go:embed migrations/0001_init.sql
var initSchema string
type DB struct {
*sql.DB
}
// Open opens (creating if necessary) the SQLite database at path, applies
// pragmas suited to a single-writer WAL workload, and runs any pending
// schema migrations.
func Open(ctx context.Context, path string) (*DB, error) {
sqlDB, err := sql.Open("sqlite", path+"?_pragma=busy_timeout(5000)")
if err != nil {
return nil, fmt.Errorf("open sqlite: %w", err)
}
// Control API is the sole writer; one connection avoids SQLITE_BUSY
// entirely for writes while still allowing concurrent reads via WAL.
sqlDB.SetMaxOpenConns(1)
for _, pragma := range []string{
"PRAGMA journal_mode=WAL",
"PRAGMA synchronous=NORMAL",
"PRAGMA foreign_keys=ON",
"PRAGMA busy_timeout=5000",
} {
if _, err := sqlDB.ExecContext(ctx, pragma); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("apply pragma %q: %w", pragma, err)
}
}
d := &DB{DB: sqlDB}
if err := d.migrate(ctx); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return d, nil
}
// migrate applies the embedded schema exactly once, tracked via
// PRAGMA user_version so repeated startups are no-ops.
func (d *DB) migrate(ctx context.Context) error {
var version int
if err := d.QueryRowContext(ctx, "PRAGMA user_version").Scan(&version); err != nil {
return fmt.Errorf("read user_version: %w", err)
}
if version >= 1 {
return nil
}
tx, err := d.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, initSchema); err != nil {
return fmt.Errorf("apply 0001_init.sql: %w", err)
}
if _, err := tx.ExecContext(ctx, "PRAGMA user_version=1"); err != nil {
return fmt.Errorf("set user_version: %w", err)
}
return tx.Commit()
}
// Now returns the current time truncated to millisecond precision, the
// granularity used consistently for all timestamp columns.
func Now() time.Time {
return time.Now().UTC().Truncate(time.Millisecond)
}