package storage import ( "context" "database/sql" "errors" "fmt" "io" "net/url" "os" "path/filepath" "strings" "sync" "sync/atomic" "syscall" "time" "tug/internal/storage/internal/attach" ) const ( busyTimeoutMillis = 5000 initializationLockSuffix = ".init.lock" ) type Store struct { sql *sql.DB path string now func() time.Time newID func() (string, error) closed atomic.Bool closeOnce sync.Once closeErr error attachMu sync.Mutex attached []attachment } func Open(ctx context.Context, databasePath string) (*Store, error) { if strings.TrimSpace(databasePath) == "" { return nil, invalid("open storage", fmt.Errorf("database path is empty")) } absPath, err := filepath.Abs(databasePath) if err != nil { return nil, invalid("open storage", fmt.Errorf("absolute database path: %w", err)) } initializationLock, err := acquireInitializationLock(ctx, absPath) if err != nil { return nil, err } defer initializationLock.Close() if err := createDatabaseFile(absPath); err != nil { return nil, err } sqlDB, err := openSQLite(ctx, absPath) if err != nil { return nil, err } migrations, err := loadMigrations(embeddedMigrations, "migrations") if err != nil { sqlDB.Close() return nil, fmt.Errorf("load embedded migrations: %w", err) } if err := applyMigrations(ctx, sqlDB, migrations, time.Now()); err != nil { sqlDB.Close() return nil, err } return &Store{ sql: sqlDB, path: absPath, now: time.Now, newID: newUUID, }, nil } func (s *Store) Attach(ctx context.Context, attacher attach.Attacher) error { const operation = "attach storage" if attacher == nil { return invalid(operation, fmt.Errorf("attacher is nil")) } if s == nil || s.sql == nil { return fmt.Errorf("%s: %w", operation, ErrClosed) } s.attachMu.Lock() defer s.attachMu.Unlock() if err := s.checkOpen(operation); err != nil { return err } initializationLock, err := acquireInitializationLock(ctx, s.path) if err != nil { return fmt.Errorf("%s: %w", operation, err) } defer initializationLock.Close() attached, err := runAttacher(ctx, s.path, attacher) if err != nil { return fmt.Errorf("%s: %w", operation, err) } if err := s.checkOpen(operation); err != nil { attached.Close() return err } s.attached = append(s.attached, attached) return nil } type attachment struct { closer io.Closer pools []*sql.DB } func (a attachment) Close() error { errs := []error{a.closer.Close()} for _, pool := range a.pools { errs = append(errs, pool.Close()) } return errors.Join(errs...) } func runAttacher(ctx context.Context, absPath string, attacher attach.Attacher) (attachment, error) { var mu sync.Mutex active := true var opened []*sql.DB pool := attach.NewPool(func(ctx context.Context) (*sql.DB, error) { mu.Lock() defer mu.Unlock() if !active { return nil, fmt.Errorf("open attached sqlite: %w", ErrClosed) } db, err := openSQLite(ctx, absPath) if err != nil { return nil, err } opened = append(opened, db) return db, nil }) closer, err := attacher.Attach(ctx, pool) mu.Lock() active = false mu.Unlock() if err == nil && closer == nil { err = errors.New("attacher returned a nil closer") } if err != nil { if closer != nil { closer.Close() } for _, db := range opened { db.Close() } return attachment{}, err } return attachment{closer: closer, pools: opened}, nil } func openSQLite(ctx context.Context, absPath string) (*sql.DB, error) { sqlDB, err := sql.Open("sqlite", sqliteURL(absPath)) if err != nil { return nil, classify("open sqlite", err) } if err := sqlDB.PingContext(ctx); err != nil { sqlDB.Close() return nil, classify("ping sqlite", err) } if err := verifyPragmas(ctx, sqlDB); err != nil { sqlDB.Close() return nil, err } return sqlDB, nil } func sqliteURL(absPath string) string { databaseURL := &url.URL{Scheme: "file", Path: filepath.ToSlash(absPath)} query := url.Values{} query.Add("_pragma", "foreign_keys(1)") query.Add("_pragma", "journal_mode(WAL)") query.Add("_pragma", fmt.Sprintf("busy_timeout(%d)", busyTimeoutMillis)) query.Set("_txlock", "immediate") databaseURL.RawQuery = query.Encode() return databaseURL.String() } func createDatabaseFile(absPath string) error { file, err := os.OpenFile(absPath, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600) if errors.Is(err, os.ErrExist) { return nil } if err != nil { return classify("create database file", err) } return classify("create database file", file.Close()) } type initializationLock struct { file *os.File } func acquireInitializationLock(ctx context.Context, databasePath string) (*initializationLock, error) { file, err := os.OpenFile(databasePath+initializationLockSuffix, os.O_CREATE|os.O_RDWR, 0o600) if err != nil { return nil, classify("open storage initialization lock", err) } for { err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) if err == nil { return &initializationLock{file: file}, nil } if err != syscall.EWOULDBLOCK && err != syscall.EAGAIN && err != syscall.EINTR { file.Close() return nil, classify("acquire storage initialization lock", err) } select { case <-ctx.Done(): file.Close() return nil, fmt.Errorf("acquire storage initialization lock: %w", ctx.Err()) case <-time.After(10 * time.Millisecond): } } } func (lock *initializationLock) Close() error { if lock == nil || lock.file == nil { return nil } unlockErr := syscall.Flock(int(lock.file.Fd()), syscall.LOCK_UN) closeErr := lock.file.Close() if unlockErr != nil { return classify("release storage initialization lock", unlockErr) } return classify("close storage initialization lock", closeErr) } func verifyPragmas(ctx context.Context, db *sql.DB) error { var foreignKeys int if err := db.QueryRowContext(ctx, `PRAGMA foreign_keys`).Scan(&foreignKeys); err != nil { return classify("read foreign_keys pragma", err) } if foreignKeys != 1 { return fmt.Errorf("foreign_keys pragma is %d, want 1: %w", foreignKeys, ErrSchemaInvalid) } var journalMode string if err := db.QueryRowContext(ctx, `PRAGMA journal_mode`).Scan(&journalMode); err != nil { return classify("read journal_mode pragma", err) } if !strings.EqualFold(journalMode, "wal") { return fmt.Errorf("journal_mode pragma is %q, want wal: %w", journalMode, ErrSchemaInvalid) } var busyTimeout int if err := db.QueryRowContext(ctx, `PRAGMA busy_timeout`).Scan(&busyTimeout); err != nil { return classify("read busy_timeout pragma", err) } if busyTimeout != busyTimeoutMillis { return fmt.Errorf("busy_timeout pragma is %d, want %d: %w", busyTimeout, busyTimeoutMillis, ErrSchemaInvalid) } return nil } func (s *Store) Close() error { if s == nil { return nil } s.closeOnce.Do(func() { s.closed.Store(true) s.attachMu.Lock() attached := s.attached s.attached = nil s.attachMu.Unlock() var errs []error for i := len(attached) - 1; i >= 0; i-- { errs = append(errs, attached[i].Close()) } if s.sql != nil { errs = append(errs, s.sql.Close()) } s.closeErr = errors.Join(errs...) }) return s.closeErr } func (s *Store) checkOpen(operation string) error { if s == nil || s.closed.Load() { return fmt.Errorf("%s: %w", operation, ErrClosed) } return nil }