1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145 |
// 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
}
|