sqlite.go

v1.0.1
Doc Versions Source
1
package store
2
3
import (
4
	"database/sql"
5
	"fmt"
6
	"regexp"
7
	"strings"
8
	"sync"
9
10
	_ "modernc.org/sqlite"
11
12
	"example.com/curator/internal/config"
13
)
14
15
// Store is a database-backed module store implementing ModuleResolver.
16
// It supports both SQLite and PostgreSQL backends.
17
type Store struct {
18
	db      *sql.DB
19
	dialect dialect
20
21
	// Compiled pattern cache for fast resolution.
22
	mu       sync.RWMutex
23
	patterns []compiledPattern
24
}
25
26
type compiledPattern struct {
27
	PatternRow
28
	re *regexp.Regexp
29
}
30
31
// Open opens a database with the given driver and DSN, then runs migrations.
32
// Supported drivers: "sqlite", "postgres".
33
//
34
// For SQLite the DSN is a file path (WAL mode and busy timeout are set automatically).
35
// For PostgreSQL the DSN is a connection string (e.g. "postgres://user:pass@host/db?sslmode=disable").
36
func Open(driver, dsn string) (*Store, error) {
37
	d, err := dialectFor(driver)
38
	if err != nil {
39
		return nil, err
40
	}
41
42
	// SQLite: append pragmas if not already present.
43
	if driver == "sqlite" && !strings.Contains(dsn, "_pragma") {
44
		dsn = dsn + "?_pragma=journal_mode(wal)&_pragma=busy_timeout(5000)"
45
	}
46
47
	db, err := sql.Open(d.driverName, dsn)
48
	if err != nil {
49
		return nil, fmt.Errorf("open database: %w", err)
50
	}
51
52
	if err := db.Ping(); err != nil {
53
		db.Close()
54
		return nil, fmt.Errorf("ping database: %w", err)
55
	}
56
57
	if _, err := db.Exec(d.migrationSQL); err != nil {
58
		db.Close()
59
		return nil, fmt.Errorf("migrate database: %w", err)
60
	}
61
62
	st := &Store{db: db, dialect: d}
63
	if err := st.refreshPatterns(); err != nil {
64
		db.Close()
65
		return nil, fmt.Errorf("load patterns: %w", err)
66
	}
67
68
	return st, nil
69
}
70
71
// Close closes the database connection.
72
func (st *Store) Close() error {
73
	return st.db.Close()
74
}
75
76
// ResolveModule looks up a module by name: exact match first, then patterns.
77
func (st *Store) ResolveModule(name string) (config.Module, bool) {
78
	// Try exact match from database.
79
	var mod config.Module
80
	var private int
81
	err := st.db.QueryRow(st.dialect.resolveModuleSQL, name).Scan(&mod.VCS, &mod.Repo, &mod.Web, &private)
82
	if err == nil {
83
		mod.Private = private != 0
84
		return mod, true
85
	}
86
87
	// Try compiled patterns.
88
	st.mu.RLock()
89
	defer st.mu.RUnlock()
90
91
	for _, cp := range st.patterns {
92
		matches := cp.re.FindStringSubmatch(name)
93
		if matches == nil {
94
			continue
95
		}
96
97
		groups := matches[1:]
98
		return config.Module{
99
			VCS:     expandTemplate(cp.VCS, name, groups),
100
			Repo:    expandTemplate(cp.Repo, name, groups),
101
			Web:     expandTemplate(cp.Web, name, groups),
102
			Private: cp.Private,
103
		}, true
104
	}
105
106
	return config.Module{}, false
107
}
108
109
// ListModules returns all modules.
110
func (st *Store) ListModules() ([]ModuleRow, error) {
111
	rows, err := st.db.Query("SELECT name, vcs, repo, web, private, created_at FROM modules ORDER BY name")
112
	if err != nil {
113
		return nil, err
114
	}
115
	defer rows.Close()
116
117
	var modules []ModuleRow
118
	for rows.Next() {
119
		var m ModuleRow
120
		var private int
121
		if err := rows.Scan(&m.Name, &m.VCS, &m.Repo, &m.Web, &private, &m.CreatedAt); err != nil {
122
			return nil, err
123
		}
124
		m.Private = private != 0
125
		modules = append(modules, m)
126
	}
127
	return modules, rows.Err()
128
}
129
130
// AddModule adds a module to the database.
131
func (st *Store) AddModule(m ModuleRow) error {
132
	private := 0
133
	if m.Private {
134
		private = 1
135
	}
136
137
	_, err := st.db.Exec(st.dialect.addModuleSQL, m.Name, m.VCS, m.Repo, m.Web, private)
138
	return err
139
}
140
141
// DeleteModule removes a module from the database.
142
func (st *Store) DeleteModule(name string) error {
143
	result, err := st.db.Exec(st.dialect.deleteModuleSQL, name)
144
	if err != nil {
145
		return err
146
	}
147
148
	n, err := result.RowsAffected()
149
	if err != nil {
150
		return err
151
	}
152
	if n == 0 {
153
		return sql.ErrNoRows
154
	}
155
	return nil
156
}
157
158
// ListPatterns returns all module patterns ordered by priority.
159
func (st *Store) ListPatterns() ([]PatternRow, error) {
160
	rows, err := st.db.Query("SELECT id, pattern, vcs, repo, web, private, priority, created_at FROM module_patterns ORDER BY priority, id")
161
	if err != nil {
162
		return nil, err
163
	}
164
	defer rows.Close()
165
166
	var patterns []PatternRow
167
	for rows.Next() {
168
		var p PatternRow
169
		var private int
170
		if err := rows.Scan(&p.ID, &p.Pattern, &p.VCS, &p.Repo, &p.Web, &private, &p.Priority, &p.CreatedAt); err != nil {
171
			return nil, err
172
		}
173
		p.Private = private != 0
174
		patterns = append(patterns, p)
175
	}
176
	return patterns, rows.Err()
177
}
178
179
// AddPattern adds a module pattern to the database and refreshes the cache.
180
func (st *Store) AddPattern(p PatternRow) error {
181
	private := 0
182
	if p.Private {
183
		private = 1
184
	}
185
186
	_, err := st.db.Exec(st.dialect.addPatternSQL, p.Pattern, p.VCS, p.Repo, p.Web, private, p.Priority)
187
	if err != nil {
188
		return err
189
	}
190
191
	return st.refreshPatterns()
192
}
193
194
// DeletePattern removes a module pattern and refreshes the cache.
195
func (st *Store) DeletePattern(id int64) error {
196
	result, err := st.db.Exec(st.dialect.deletePatternSQL, id)
197
	if err != nil {
198
		return err
199
	}
200
201
	n, err := result.RowsAffected()
202
	if err != nil {
203
		return err
204
	}
205
	if n == 0 {
206
		return sql.ErrNoRows
207
	}
208
209
	return st.refreshPatterns()
210
}
211
212
// ModuleCount returns the total number of configured modules (exact + patterns).
213
func (st *Store) ModuleCount() int {
214
	var count int
215
	st.db.QueryRow("SELECT COUNT(*) FROM modules").Scan(&count)
216
217
	st.mu.RLock()
218
	count += len(st.patterns)
219
	st.mu.RUnlock()
220
221
	return count
222
}
223
224
func (st *Store) refreshPatterns() error {
225
	rows, err := st.db.Query("SELECT id, pattern, vcs, repo, web, private, priority FROM module_patterns ORDER BY priority, id")
226
	if err != nil {
227
		return err
228
	}
229
	defer rows.Close()
230
231
	var patterns []compiledPattern
232
	for rows.Next() {
233
		var cp compiledPattern
234
		var private int
235
		if err := rows.Scan(&cp.ID, &cp.Pattern, &cp.VCS, &cp.Repo, &cp.Web, &private, &cp.Priority); err != nil {
236
			return err
237
		}
238
		cp.Private = private != 0
239
240
		re, err := regexp.Compile("^" + cp.Pattern + "$")
241
		if err != nil {
242
			return fmt.Errorf("compile pattern %q: %w", cp.Pattern, err)
243
		}
244
		cp.re = re
245
246
		patterns = append(patterns, cp)
247
	}
248
249
	st.mu.Lock()
250
	st.patterns = patterns
251
	st.mu.Unlock()
252
253
	return rows.Err()
254
}
255
256
func expandTemplate(tmpl, name string, groups []string) string {
257
	s := strings.ReplaceAll(tmpl, "{name}", name)
258
	for i, g := range groups {
259
		s = strings.ReplaceAll(s, fmt.Sprintf("{%d}", i+1), g)
260
	}
261
	return s
262
}
263

Source Files