b6f8de1dcc
Cleanup pass over the repo so CI can enforce lint going forward
without the only-new-issues escape hatch:
* gofumpt -w across the tree (31 hits, all formatting)
* misspell --fix (25 hits, US-locale spelling) — but reverted on
api.JobCancelled = "cancelled" since that literal is the wire +
DB CHECK constraint value, plus matched the case in store/fleet.go
back to "cancelled" and added //nolint:misspell on both for the
next time someone reaches for the auto-fix
* Wrap every `defer rows.Close()` / `defer stmt.Close()` /
`defer res.Body.Close()` in `defer func() { _ = .Close() }()`
to satisfy errcheck without losing the close itself
* websocket.Dial callers (1 prod, 4 tests) now capture + close the
upgrade response Body — coder/websocket can return res with a nil
Body on success, so the test deferred-closes guard against that
* Annotate the two genuine-by-design nilerr cases with //nolint
comments explaining why nil-on-error is the contract (cookie
missing = no session; ctx cancelled mid-backoff = clean shutdown)
* Add brief godoc on the 10 exported const groups + types that
revive flagged (api.HostOS/HostArch/JobKind/JobStatus/LogStream/
ErrorCode, restic.EventKind, store.Role, web.FS)
* Drop the unused (*Server).userByID method
* Inline the unparam baseView(active) — every UI page is under
the dashboard primary nav today
Result: `golangci-lint run ./...` reports 0 issues. CI lint job
no longer needs only-new-issues: true; X-06 follow-up entry in
tasks.md removed.
101 lines
2.8 KiB
Go
101 lines
2.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"embed"
|
|
"fmt"
|
|
"io/fs"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
//go:embed migrations/*.sql
|
|
var migrationsFS embed.FS
|
|
|
|
// migration is one ordered SQL file from migrations/.
|
|
type migration struct {
|
|
version int // parsed from filename prefix (0001, 0002, …)
|
|
name string // full filename, for error messages
|
|
sql string
|
|
}
|
|
|
|
// loadMigrations reads every migrations/*.sql file in lexical order
|
|
// and returns them. Filenames must look like NNNN_name.sql; the
|
|
// numeric prefix is the version.
|
|
func loadMigrations() ([]migration, error) {
|
|
entries, err := fs.ReadDir(migrationsFS, "migrations")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read migrations dir: %w", err)
|
|
}
|
|
out := make([]migration, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
|
|
continue
|
|
}
|
|
var v int
|
|
// Allow up to 6 digits (we will never need that many but it
|
|
// costs nothing to be permissive).
|
|
if _, err := fmt.Sscanf(e.Name(), "%d_", &v); err != nil {
|
|
return nil, fmt.Errorf("migration %q: cannot parse version prefix: %w", e.Name(), err)
|
|
}
|
|
body, err := fs.ReadFile(migrationsFS, "migrations/"+e.Name())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s: %w", e.Name(), err)
|
|
}
|
|
out = append(out, migration{version: v, name: e.Name(), sql: string(body)})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].version < out[j].version })
|
|
return out, nil
|
|
}
|
|
|
|
// migrate brings the db up to the highest known version. It is
|
|
// idempotent: running it on an already-current db is a no-op. There
|
|
// is no rollback path; we move forward only.
|
|
func migrate(ctx context.Context, db *sql.DB) error {
|
|
if _, err := db.ExecContext(ctx, `
|
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
version INTEGER PRIMARY KEY,
|
|
applied_at TEXT NOT NULL
|
|
)
|
|
`); err != nil {
|
|
return fmt.Errorf("create schema_version: %w", err)
|
|
}
|
|
|
|
migs, err := loadMigrations()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, m := range migs {
|
|
var applied int
|
|
row := db.QueryRowContext(ctx,
|
|
`SELECT COUNT(*) FROM schema_version WHERE version = ?`, m.version)
|
|
if err := row.Scan(&applied); err != nil {
|
|
return fmt.Errorf("check version %d: %w", m.version, err)
|
|
}
|
|
if applied > 0 {
|
|
continue
|
|
}
|
|
|
|
tx, err := db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("begin tx for migration %s: %w", m.name, err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, m.sql); err != nil {
|
|
_ = tx.Rollback()
|
|
return fmt.Errorf("apply %s: %w", m.name, err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx,
|
|
`INSERT INTO schema_version (version, applied_at) VALUES (?, datetime('now'))`,
|
|
m.version); err != nil {
|
|
_ = tx.Rollback()
|
|
return fmt.Errorf("record %s: %w", m.name, err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit %s: %w", m.name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|