// Package store keeps pages, snapshots and revisions in SQLite. // // Every observed state of a file is a snapshot. A revision is a public // version: number 1 is the baseline, never announced, and 2.. are the ones // the author published. Silent snapshots are history only and never leave // the database. package store import ( "context" "database/sql" "errors" "fmt" "net/url" "os" "path/filepath" "time" _ "modernc.org/sqlite" ) // Store is a Patchwork database. type Store struct { Queries db *sql.DB } // Open opens or creates the database at path and migrates it. func Open(path string) (*Store, error) { if dir := filepath.Dir(path); dir != "" { if err := os.MkdirAll(dir, 0o755); err != nil { return nil, err } } q := url.Values{} for _, p := range []string{"busy_timeout(5000)", "journal_mode(WAL)", "foreign_keys(1)", "synchronous(NORMAL)", "temp_store(MEMORY)"} { q.Add("_pragma", p) } db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(path)+"?"+q.Encode()) if err != nil { return nil, err } // One connection: SQLite has a single writer anyway, and this keeps // every read consistent with the last write without lock juggling. db.SetMaxOpenConns(1) s := &Store{Queries: Queries{q: db}, db: db} if err := s.migrate(); err != nil { db.Close() return nil, fmt.Errorf("migrate %s: %w", path, err) } return s, nil } // Close closes the database. func (s *Store) Close() error { return s.db.Close() } func (s *Store) migrate() error { var v int if err := s.db.QueryRow(`PRAGMA user_version`).Scan(&v); err != nil { return err } if v > len(migrations) { return fmt.Errorf("database schema %d is newer than this binary (%d)", v, len(migrations)) } for ; v < len(migrations); v++ { tx, err := s.db.Begin() if err != nil { return err } if _, err := tx.Exec(migrations[v]); err != nil { tx.Rollback() return err } if _, err := tx.Exec(fmt.Sprintf(`PRAGMA user_version = %d`, v+1)); err != nil { tx.Rollback() return err } if err := tx.Commit(); err != nil { return err } } return nil } // Tx runs fn in a transaction and commits when it returns nil. With dryRun // the transaction is always rolled back, so fn can show what it would do. func (s *Store) Tx(ctx context.Context, dryRun bool, fn func(q *Queries) error) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } if err := fn(&Queries{q: tx}); err != nil { tx.Rollback() return err } if dryRun { return tx.Rollback() } return tx.Commit() } // Backup writes a consistent copy of the database to path. func (s *Store) Backup(ctx context.Context, path string) error { if _, err := os.Stat(path); err == nil { return fmt.Errorf("%s already exists", path) } _, err := s.db.ExecContext(ctx, `VACUUM INTO ?`, path) return err } type querier interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } // Queries run against the database or inside a transaction. type Queries struct{ q querier } // ErrNotFound is returned when a lookup matches nothing. var ErrNotFound = errors.New("not found") const timeLayout = time.RFC3339Nano func fmtTime(t time.Time) string { return t.UTC().Format(timeLayout) } func fmtTimePtr(t *time.Time) any { if t == nil { return nil } return fmtTime(*t) } func parseTime(s string) (time.Time, error) { return time.Parse(timeLayout, s) } func parseTimePtr(ns sql.NullString) (*time.Time, error) { if !ns.Valid { return nil, nil } t, err := parseTime(ns.String) if err != nil { return nil, err } return &t, nil }