// Package store — SQLite-витрина healthlog: доставки и (позже) разобранные // точки. Витрина производна от сырого архива и пересобирается из него. package store import ( "embed" "fmt" "net/url" "time" "github.com/jmoiron/sqlx" "github.com/pressly/goose/v3" _ "modernc.org/sqlite" // чистый Go-драйвер SQLite, без cgo ) //go:embed migrations/*.sql var migrationsFS embed.FS // Store — доступ к витрине. type Store struct { db *sqlx.DB } // Open открывает БД по пути и накатывает миграции. func Open(dbPath string) (*Store, error) { db, err := sqlx.Connect("sqlite", dsn(dbPath)) if err != nil { return nil, fmt.Errorf("open sqlite %q: %w", dbPath, err) } if err := migrate(db); err != nil { _ = db.Close() return nil, err } return &Store{db: db}, nil } // Close закрывает соединение с БД. func (s *Store) Close() error { if err := s.db.Close(); err != nil { return fmt.Errorf("close sqlite: %w", err) } return nil } // dsn собирает строку подключения: WAL для параллельного чтения во время // записи, busy_timeout — чтобы конкурентная запись ждала, а не падала. func dsn(path string) string { q := url.Values{} q.Add("_pragma", "journal_mode(WAL)") q.Add("_pragma", "busy_timeout(5000)") q.Add("_pragma", "foreign_keys(on)") return "file:" + path + "?" + q.Encode() } func migrate(db *sqlx.DB) error { goose.SetBaseFS(migrationsFS) goose.SetLogger(goose.NopLogger()) if err := goose.SetDialect("sqlite3"); err != nil { return fmt.Errorf("goose dialect: %w", err) } if err := goose.Up(db.DB, "migrations"); err != nil { return fmt.Errorf("goose up: %w", err) } return nil } // Now — единая точка генерации времени: UTC, секундная точность. // Секунды дают фиксированную ширину RFC 3339, а значит лексикографическая // сортировка TEXT совпадает с хронологией. func Now() time.Time { return time.Now().UTC().Truncate(time.Second) } // FormatTime приводит время к каноническому виду хранения: RFC 3339, UTC. func FormatTime(t time.Time) string { return t.UTC().Format(time.RFC3339) } // ParseTime разбирает метку времени из БД. func ParseTime(s string) (time.Time, error) { t, err := time.Parse(time.RFC3339, s) if err != nil { return time.Time{}, fmt.Errorf("parse time %q: %w", s, err) } return t.UTC(), nil }