sqlite.go

v1.2.0
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
	// 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
// DeleteModule removes a module from the database.
149
func (st *Store) DeleteModule(name string) error {
150
	result, err := st.db.Exec(st.dialect.deleteModuleSQL, name)
151
	if err != nil {
152
		return err
153
	}
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
// ListPatterns returns all module patterns ordered by priority.
166
func (st *Store) ListPatterns() ([]PatternRow, error) {
167
	rows, err := st.db.Query("SELECT id, pattern, vcs, repo, web, private, priority, credential_name, created_at FROM module_patterns ORDER BY priority, id")
168
	if err != nil {
169
		return nil, err
170
	}
171
	defer rows.Close()
172
173
	var patterns []PatternRow
174
	for rows.Next() {
175
		var p PatternRow
176
		var private int
177
		if err := rows.Scan(&p.ID, &p.Pattern, &p.VCS, &p.Repo, &p.Web, &private, &p.Priority, &p.CredentialName, &p.CreatedAt); err != nil {
178
			return nil, err
179
		}
180
		p.Private = private != 0
181
		patterns = append(patterns, p)
182
	}
183
	return patterns, rows.Err()
184
}
185
186
// AddPattern adds a module pattern to the database and refreshes the cache.
187
func (st *Store) AddPattern(p PatternRow) error {
188
	private := 0
189
	if p.Private {
190
		private = 1
191
	}
192
193
	_, err := st.db.Exec(st.dialect.addPatternSQL, p.Pattern, p.VCS, p.Repo, p.Web, private, p.Priority, p.CredentialName)
194
	if err != nil {
195
		return err
196
	}
197
198
	return st.refreshPatterns()
199
}
200
201
// DeletePattern removes a module pattern and refreshes the cache.
202
func (st *Store) DeletePattern(id int64) error {
203
	result, err := st.db.Exec(st.dialect.deletePatternSQL, id)
204
	if err != nil {
205
		return err
206
	}
207
208
	n, err := result.RowsAffected()
209
	if err != nil {
210
		return err
211
	}
212
	if n == 0 {
213
		return sql.ErrNoRows
214
	}
215
216
	return st.refreshPatterns()
217
}
218
219
// ModuleCount returns the total number of configured modules (exact + patterns).
220
func (st *Store) ModuleCount() int {
221
	var count int
222
	st.db.QueryRow("SELECT COUNT(*) FROM modules").Scan(&count)
223
224
	st.mu.RLock()
225
	count += len(st.patterns)
226
	st.mu.RUnlock()
227
228
	return count
229
}
230
231
func (st *Store) refreshPatterns() error {
232
	rows, err := st.db.Query("SELECT id, pattern, vcs, repo, web, private, priority, credential_name FROM module_patterns ORDER BY priority, id")
233
	if err != nil {
234
		return err
235
	}
236
	defer rows.Close()
237
238
	var patterns []compiledPattern
239
	for rows.Next() {
240
		var cp compiledPattern
241
		var private int
242
		if err := rows.Scan(&cp.ID, &cp.Pattern, &cp.VCS, &cp.Repo, &cp.Web, &private, &cp.Priority, &cp.CredentialName); err != nil {
243
			return err
244
		}
245
		cp.Private = private != 0
246
247
		re, err := regexp.Compile("^" + cp.Pattern + "$")
248
		if err != nil {
249
			return fmt.Errorf("compile pattern %q: %w", cp.Pattern, err)
250
		}
251
		cp.re = re
252
253
		patterns = append(patterns, cp)
254
	}
255
256
	st.mu.Lock()
257
	st.patterns = patterns
258
	st.mu.Unlock()
259
260
	return rows.Err()
261
}
262
263
func expandTemplate(tmpl, name string, groups []string) string {
264
	s := strings.ReplaceAll(tmpl, "{name}", name)
265
	for i, g := range groups {
266
		s = strings.ReplaceAll(s, fmt.Sprintf("{%d}", i+1), g)
267
	}
268
	return s
269
}
270
271
// addColumnIfNotExists runs ALTER TABLE ADD COLUMN and ignores errors
272
// indicating the column already exists (for idempotent migrations).
273
func addColumnIfNotExists(db *sql.DB, table, column, colDef string) {
274
	_, _ = db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, colDef))
275
}
276
277
// ListCredentials returns all credentials ordered by name.
278
func (st *Store) ListCredentials() ([]CredentialRow, error) {
279
	rows, err := st.db.Query(st.dialect.listCredentialsSQL)
280
	if err != nil {
281
		return nil, err
282
	}
283
	defer rows.Close()
284
285
	var creds []CredentialRow
286
	for rows.Next() {
287
		var c CredentialRow
288
		if err := rows.Scan(&c.Name, &c.Type, &c.Data, &c.CreatedAt); err != nil {
289
			return nil, err
290
		}
291
		creds = append(creds, c)
292
	}
293
	return creds, rows.Err()
294
}
295
296
// GetCredential returns a credential by name.
297
func (st *Store) GetCredential(name string) (CredentialRow, error) {
298
	var c CredentialRow
299
	err := st.db.QueryRow(st.dialect.getCredentialSQL, name).Scan(&c.Name, &c.Type, &c.Data, &c.CreatedAt)
300
	return c, err
301
}
302
303
// AddCredential inserts a new credential.
304
func (st *Store) AddCredential(c CredentialRow) error {
305
	_, err := st.db.Exec(st.dialect.addCredentialSQL, c.Name, c.Type, c.Data)
306
	return err
307
}
308
309
// UpdateCredential updates an existing credential's type and data.
310
func (st *Store) UpdateCredential(c CredentialRow) error {
311
	result, err := st.db.Exec(st.dialect.updateCredentialSQL, c.Type, c.Data, c.Name)
312
	if err != nil {
313
		return err
314
	}
315
	n, err := result.RowsAffected()
316
	if err != nil {
317
		return err
318
	}
319
	if n == 0 {
320
		return sql.ErrNoRows
321
	}
322
	return nil
323
}
324
325
// DeleteCredential removes a credential by name.
326
// It returns an error if the credential is referenced by any module or pattern.
327
func (st *Store) DeleteCredential(name string) error {
328
	// Check for references in modules.
329
	var count int
330
	st.db.QueryRow("SELECT COUNT(*) FROM modules WHERE credential_name = ?", name).Scan(&count)
331
	if count > 0 {
332
		return fmt.Errorf("credential %q is referenced by %d module(s)", name, count)
333
	}
334
335
	// Check for references in patterns.
336
	st.db.QueryRow("SELECT COUNT(*) FROM module_patterns WHERE credential_name = ?", name).Scan(&count)
337
	if count > 0 {
338
		return fmt.Errorf("credential %q is referenced by %d pattern(s)", name, count)
339
	}
340
341
	result, err := st.db.Exec(st.dialect.deleteCredentialSQL, name)
342
	if err != nil {
343
		return err
344
	}
345
	n, err := result.RowsAffected()
346
	if err != nil {
347
		return err
348
	}
349
	if n == 0 {
350
		return sql.ErrNoRows
351
	}
352
	return nil
353
}
354
355
// CredentialCount returns the total number of credentials.
356
func (st *Store) CredentialCount() int {
357
	var count int
358
	st.db.QueryRow("SELECT COUNT(*) FROM credentials").Scan(&count)
359
	return count
360
}
361

Source Files