sqlite.go

v1.4.3
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
	"go.bigb.es/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
	// Add credential_name column to existing tables (idempotent).
63
	addColumnIfNotExists(db, "modules", "credential_name", "TEXT NOT NULL DEFAULT ''")
64
	addColumnIfNotExists(db, "module_patterns", "credential_name", "TEXT NOT NULL DEFAULT ''")
65
66
	st := &Store{db: db, dialect: d}
67
	if err := st.refreshPatterns(); err != nil {
68
		db.Close()
69
		return nil, fmt.Errorf("load patterns: %w", err)
70
	}
71
72
	return st, nil
73
}
74
75
// Close closes the database connection.
76
func (st *Store) Close() error {
77
	return st.db.Close()
78
}
79
80
// ResolveModule looks up a module by name: exact match first, then patterns.
81
func (st *Store) ResolveModule(name string) (config.Module, bool) {
82
	// Try exact match from database.
83
	var mod config.Module
84
	var private int
85
	var credName string
86
	err := st.db.QueryRow(st.dialect.resolveModuleSQL, name).Scan(&mod.VCS, &mod.Repo, &mod.Web, &private, &credName)
87
	if err == nil {
88
		mod.Private = private != 0
89
		mod.CredentialName = credName
90
		return mod, true
91
	}
92
93
	// Try compiled patterns.
94
	st.mu.RLock()
95
	defer st.mu.RUnlock()
96
97
	for _, cp := range st.patterns {
98
		matches := cp.re.FindStringSubmatch(name)
99
		if matches == nil {
100
			continue
101
		}
102
103
		groups := matches[1:]
104
		return config.Module{
105
			VCS:            expandTemplate(cp.VCS, name, groups),
106
			Repo:           expandTemplate(cp.Repo, name, groups),
107
			Web:            expandTemplate(cp.Web, name, groups),
108
			Private:        cp.Private,
109
			CredentialName: cp.CredentialName,
110
		}, true
111
	}
112
113
	return config.Module{}, false
114
}
115
116
// ListModules returns all modules.
117
func (st *Store) ListModules() ([]ModuleRow, error) {
118
	rows, err := st.db.Query("SELECT name, vcs, repo, web, private, credential_name, created_at FROM modules ORDER BY name")
119
	if err != nil {
120
		return nil, err
121
	}
122
	defer rows.Close()
123
124
	var modules []ModuleRow
125
	for rows.Next() {
126
		var m ModuleRow
127
		var private int
128
		if err := rows.Scan(&m.Name, &m.VCS, &m.Repo, &m.Web, &private, &m.CredentialName, &m.CreatedAt); err != nil {
129
			return nil, err
130
		}
131
		m.Private = private != 0
132
		modules = append(modules, m)
133
	}
134
	return modules, rows.Err()
135
}
136
137
// AddModule adds a module to the database.
138
func (st *Store) AddModule(m ModuleRow) error {
139
	private := 0
140
	if m.Private {
141
		private = 1
142
	}
143
144
	_, err := st.db.Exec(st.dialect.addModuleSQL, m.Name, m.VCS, m.Repo, m.Web, private, m.CredentialName)
145
	return err
146
}
147
148
// UpdateModule updates a module's fields (except name, which is the PK).
149
// RenameModule changes a module's name (primary key).
150
func (st *Store) RenameModule(oldName, newName string) error {
151
	result, err := st.db.Exec(st.dialect.renameModuleSQL, newName, oldName)
152
	if err != nil {
153
		return err
154
	}
155
	n, err := result.RowsAffected()
156
	if err != nil {
157
		return err
158
	}
159
	if n == 0 {
160
		return sql.ErrNoRows
161
	}
162
	return nil
163
}
164
165
func (st *Store) UpdateModule(m ModuleRow) error {
166
	private := 0
167
	if m.Private {
168
		private = 1
169
	}
170
171
	result, err := st.db.Exec(st.dialect.updateModuleSQL, m.VCS, m.Repo, m.Web, private, m.CredentialName, m.Name)
172
	if err != nil {
173
		return err
174
	}
175
176
	n, err := result.RowsAffected()
177
	if err != nil {
178
		return err
179
	}
180
	if n == 0 {
181
		return sql.ErrNoRows
182
	}
183
	return nil
184
}
185
186
// DeleteModule removes a module from the database.
187
func (st *Store) DeleteModule(name string) error {
188
	result, err := st.db.Exec(st.dialect.deleteModuleSQL, name)
189
	if err != nil {
190
		return err
191
	}
192
193
	n, err := result.RowsAffected()
194
	if err != nil {
195
		return err
196
	}
197
	if n == 0 {
198
		return sql.ErrNoRows
199
	}
200
	return nil
201
}
202
203
// ListPatterns returns all module patterns ordered by priority.
204
func (st *Store) ListPatterns() ([]PatternRow, error) {
205
	rows, err := st.db.Query("SELECT id, pattern, vcs, repo, web, private, priority, credential_name, created_at FROM module_patterns ORDER BY priority, id")
206
	if err != nil {
207
		return nil, err
208
	}
209
	defer rows.Close()
210
211
	var patterns []PatternRow
212
	for rows.Next() {
213
		var p PatternRow
214
		var private int
215
		if err := rows.Scan(&p.ID, &p.Pattern, &p.VCS, &p.Repo, &p.Web, &private, &p.Priority, &p.CredentialName, &p.CreatedAt); err != nil {
216
			return nil, err
217
		}
218
		p.Private = private != 0
219
		patterns = append(patterns, p)
220
	}
221
	return patterns, rows.Err()
222
}
223
224
// AddPattern adds a module pattern to the database and refreshes the cache.
225
func (st *Store) AddPattern(p PatternRow) error {
226
	private := 0
227
	if p.Private {
228
		private = 1
229
	}
230
231
	_, err := st.db.Exec(st.dialect.addPatternSQL, p.Pattern, p.VCS, p.Repo, p.Web, private, p.Priority, p.CredentialName)
232
	if err != nil {
233
		return err
234
	}
235
236
	return st.refreshPatterns()
237
}
238
239
// UpdatePattern updates a module pattern and refreshes the cache.
240
func (st *Store) UpdatePattern(p PatternRow) error {
241
	private := 0
242
	if p.Private {
243
		private = 1
244
	}
245
246
	result, err := st.db.Exec(st.dialect.updatePatternSQL, p.Pattern, p.VCS, p.Repo, p.Web, private, p.Priority, p.CredentialName, p.ID)
247
	if err != nil {
248
		return err
249
	}
250
251
	n, err := result.RowsAffected()
252
	if err != nil {
253
		return err
254
	}
255
	if n == 0 {
256
		return sql.ErrNoRows
257
	}
258
259
	return st.refreshPatterns()
260
}
261
262
// DeletePattern removes a module pattern and refreshes the cache.
263
func (st *Store) DeletePattern(id int64) error {
264
	result, err := st.db.Exec(st.dialect.deletePatternSQL, id)
265
	if err != nil {
266
		return err
267
	}
268
269
	n, err := result.RowsAffected()
270
	if err != nil {
271
		return err
272
	}
273
	if n == 0 {
274
		return sql.ErrNoRows
275
	}
276
277
	return st.refreshPatterns()
278
}
279
280
// ModuleCount returns the total number of configured modules (exact + patterns).
281
func (st *Store) ModuleCount() int {
282
	var count int
283
	st.db.QueryRow("SELECT COUNT(*) FROM modules").Scan(&count)
284
285
	st.mu.RLock()
286
	count += len(st.patterns)
287
	st.mu.RUnlock()
288
289
	return count
290
}
291
292
func (st *Store) refreshPatterns() error {
293
	rows, err := st.db.Query("SELECT id, pattern, vcs, repo, web, private, priority, credential_name FROM module_patterns ORDER BY priority, id")
294
	if err != nil {
295
		return err
296
	}
297
	defer rows.Close()
298
299
	var patterns []compiledPattern
300
	for rows.Next() {
301
		var cp compiledPattern
302
		var private int
303
		if err := rows.Scan(&cp.ID, &cp.Pattern, &cp.VCS, &cp.Repo, &cp.Web, &private, &cp.Priority, &cp.CredentialName); err != nil {
304
			return err
305
		}
306
		cp.Private = private != 0
307
308
		re, err := regexp.Compile("^" + cp.Pattern + "$")
309
		if err != nil {
310
			return fmt.Errorf("compile pattern %q: %w", cp.Pattern, err)
311
		}
312
		cp.re = re
313
314
		patterns = append(patterns, cp)
315
	}
316
317
	st.mu.Lock()
318
	st.patterns = patterns
319
	st.mu.Unlock()
320
321
	return rows.Err()
322
}
323
324
func expandTemplate(tmpl, name string, groups []string) string {
325
	s := strings.ReplaceAll(tmpl, "{name}", name)
326
	for i, g := range groups {
327
		s = strings.ReplaceAll(s, fmt.Sprintf("{%d}", i+1), g)
328
	}
329
	return s
330
}
331
332
// addColumnIfNotExists runs ALTER TABLE ADD COLUMN and ignores errors
333
// indicating the column already exists (for idempotent migrations).
334
func addColumnIfNotExists(db *sql.DB, table, column, colDef string) {
335
	_, _ = db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, colDef))
336
}
337
338
// ListCredentials returns all credentials ordered by name.
339
func (st *Store) ListCredentials() ([]CredentialRow, error) {
340
	rows, err := st.db.Query(st.dialect.listCredentialsSQL)
341
	if err != nil {
342
		return nil, err
343
	}
344
	defer rows.Close()
345
346
	var creds []CredentialRow
347
	for rows.Next() {
348
		var c CredentialRow
349
		if err := rows.Scan(&c.Name, &c.Type, &c.Data, &c.CreatedAt); err != nil {
350
			return nil, err
351
		}
352
		creds = append(creds, c)
353
	}
354
	return creds, rows.Err()
355
}
356
357
// GetCredential returns a credential by name.
358
func (st *Store) GetCredential(name string) (CredentialRow, error) {
359
	var c CredentialRow
360
	err := st.db.QueryRow(st.dialect.getCredentialSQL, name).Scan(&c.Name, &c.Type, &c.Data, &c.CreatedAt)
361
	return c, err
362
}
363
364
// AddCredential inserts a new credential.
365
func (st *Store) AddCredential(c CredentialRow) error {
366
	_, err := st.db.Exec(st.dialect.addCredentialSQL, c.Name, c.Type, c.Data)
367
	return err
368
}
369
370
// UpdateCredential updates an existing credential's type and data.
371
func (st *Store) UpdateCredential(c CredentialRow) error {
372
	result, err := st.db.Exec(st.dialect.updateCredentialSQL, c.Type, c.Data, c.Name)
373
	if err != nil {
374
		return err
375
	}
376
	n, err := result.RowsAffected()
377
	if err != nil {
378
		return err
379
	}
380
	if n == 0 {
381
		return sql.ErrNoRows
382
	}
383
	return nil
384
}
385
386
// DeleteCredential removes a credential by name.
387
// It returns an error if the credential is referenced by any module or pattern.
388
func (st *Store) DeleteCredential(name string) error {
389
	// Check for references in modules.
390
	var count int
391
	st.db.QueryRow("SELECT COUNT(*) FROM modules WHERE credential_name = ?", name).Scan(&count)
392
	if count > 0 {
393
		return fmt.Errorf("credential %q is referenced by %d module(s)", name, count)
394
	}
395
396
	// Check for references in patterns.
397
	st.db.QueryRow("SELECT COUNT(*) FROM module_patterns WHERE credential_name = ?", name).Scan(&count)
398
	if count > 0 {
399
		return fmt.Errorf("credential %q is referenced by %d pattern(s)", name, count)
400
	}
401
402
	result, err := st.db.Exec(st.dialect.deleteCredentialSQL, name)
403
	if err != nil {
404
		return err
405
	}
406
	n, err := result.RowsAffected()
407
	if err != nil {
408
		return err
409
	}
410
	if n == 0 {
411
		return sql.ErrNoRows
412
	}
413
	return nil
414
}
415
416
// CredentialCount returns the total number of credentials.
417
func (st *Store) CredentialCount() int {
418
	var count int
419
	st.db.QueryRow("SELECT COUNT(*) FROM credentials").Scan(&count)
420
	return count
421
}
422

Source Files