Remediate all lint findings under the canonical golangci-lint config
Fix every finding surfaced by the canonical .golangci.yml with golangci-lint v2.12.2 (refs #61), behavior-preserving throughout: - err113: dynamic errors replaced with package-level sentinels and %w wrapping; direct comparisons converted to errors.Is - goprintffuncname: printf-style helpers renamed with an f suffix (ui.Writer message methods, cli.ReportErrorf, database.Fatalf, vaultik stdoutf) and all call sites updated - revive: stuttering type names renamed (blob.Handler, blob.WithReader, blob.ChunkPosition, storage.URL, storage.Info), doc comments added, unused parameters blanked, package comments added - contextcheck/noctx: ctx threaded through blob.Packer (AddChunk/Flush/FinalizeBlob/PackChunks) and scanner call sites; context-aware exec and sql variants used - funlen/cyclop/gocognit/nestif/dupl: oversized or duplicated functions split into focused helpers across production and test code - paralleltest/tparallel/thelper/usetesting/testpackage: tests parallelized where safe (global log.Initialize kept in the serial phase), helpers marked, t.TempDir adopted, external test packages where only exported API is used - gosec: integer conversions clamped or justified, header timeouts added, remaining findings suppressed with per-site justifications - mnd/goconst/lll/wsl_v5/nlreturn/noinlineerr/errcheck and other mechanical findings fixed directly Remove the deprecated log.LogOptions alias (callers migrated to log.Options). make check is green.
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -22,10 +23,15 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
// Register the pure-Go sqlite driver.
|
||||
_ "modernc.org/sqlite"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
)
|
||||
|
||||
// errInvalidMigrationFilename is returned when an embedded migration file
|
||||
// does not follow the "<version>[_<description>].sql" naming pattern.
|
||||
var errInvalidMigrationFilename = errors.New("invalid migration filename")
|
||||
|
||||
//go:embed schema/*.sql
|
||||
var schemaFS embed.FS
|
||||
|
||||
@@ -51,7 +57,7 @@ type DB struct {
|
||||
func ParseMigrationVersion(filename string) (int, error) {
|
||||
name := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
if name == "" {
|
||||
return 0, fmt.Errorf("invalid migration filename %q: empty name", filename)
|
||||
return 0, fmt.Errorf("%w %q: empty name", errInvalidMigrationFilename, filename)
|
||||
}
|
||||
|
||||
// Split on underscore to separate version from description.
|
||||
@@ -62,15 +68,17 @@ func ParseMigrationVersion(filename string) (int, error) {
|
||||
}
|
||||
|
||||
if versionStr == "" {
|
||||
return 0, fmt.Errorf("invalid migration filename %q: empty version prefix", filename)
|
||||
return 0, fmt.Errorf(
|
||||
"%w %q: empty version prefix", errInvalidMigrationFilename, filename,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the version is purely numeric.
|
||||
for _, ch := range versionStr {
|
||||
if ch < '0' || ch > '9' {
|
||||
return 0, fmt.Errorf(
|
||||
"invalid migration filename %q: version %q contains non-numeric character %q",
|
||||
filename, versionStr, string(ch),
|
||||
"%w %q: version %q contains non-numeric character %q",
|
||||
errInvalidMigrationFilename, filename, versionStr, string(ch),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -101,66 +109,87 @@ func New(ctx context.Context, path string) (*DB, error) {
|
||||
|
||||
conn, err := sql.Open(
|
||||
"sqlite",
|
||||
path+"?_journal_mode=WAL&_synchronous=NORMAL&_busy_timeout=10000&_locking_mode=NORMAL&_foreign_keys=ON",
|
||||
path+"?_journal_mode=WAL&_synchronous=NORMAL&_busy_timeout=10000"+
|
||||
"&_locking_mode=NORMAL&_foreign_keys=ON",
|
||||
)
|
||||
if err == nil {
|
||||
// Set connection pool settings
|
||||
// SQLite can handle multiple readers but only one writer at a time.
|
||||
// Setting MaxOpenConns to 1 ensures all writes are serialized through
|
||||
// a single connection, preventing SQLITE_BUSY errors.
|
||||
conn.SetMaxOpenConns(1)
|
||||
conn.SetMaxIdleConns(1)
|
||||
configureConnPool(conn)
|
||||
|
||||
err := conn.PingContext(ctx)
|
||||
err = conn.PingContext(ctx)
|
||||
if err == nil {
|
||||
// Success on first try
|
||||
log.Debug("Database opened successfully with WAL mode", "path", path)
|
||||
|
||||
// Enable foreign keys explicitly
|
||||
if _, err := conn.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
|
||||
log.Warn("Failed to enable foreign keys", "error", err)
|
||||
}
|
||||
|
||||
db := &DB{conn: conn, path: path}
|
||||
|
||||
err := applyMigrations(ctx, conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
|
||||
return nil, fmt.Errorf("applying migrations: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
return finishOpen(ctx, conn, path)
|
||||
}
|
||||
|
||||
log.Debug("Failed to ping database, closing connection", "path", path, "error", err)
|
||||
log.Debug(
|
||||
"Failed to ping database, closing connection",
|
||||
"path", path, "error", err,
|
||||
)
|
||||
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
// If first attempt failed, try with TRUNCATE mode to clear any locks
|
||||
return openWithRecovery(ctx, path)
|
||||
}
|
||||
|
||||
// configureConnPool serializes all database access through one connection.
|
||||
// SQLite can handle multiple readers but only one writer at a time; setting
|
||||
// MaxOpenConns to 1 ensures all writes go through a single connection,
|
||||
// preventing SQLITE_BUSY errors.
|
||||
func configureConnPool(conn *sql.DB) {
|
||||
conn.SetMaxOpenConns(1)
|
||||
conn.SetMaxIdleConns(1)
|
||||
}
|
||||
|
||||
// finishOpen enables foreign keys, wraps the connection, and applies any
|
||||
// pending migrations. On migration failure the connection is closed.
|
||||
func finishOpen(ctx context.Context, conn *sql.DB, path string) (*DB, error) {
|
||||
// Enable foreign keys explicitly
|
||||
_, err := conn.ExecContext(ctx, "PRAGMA foreign_keys = ON")
|
||||
if err != nil {
|
||||
log.Warn("Failed to enable foreign keys", "path", path, "error", err)
|
||||
}
|
||||
|
||||
db := &DB{conn: conn, path: path}
|
||||
|
||||
err = applyMigrations(ctx, conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
|
||||
return nil, fmt.Errorf("applying migrations: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// openWithRecovery retries opening the database in TRUNCATE journal mode to
|
||||
// clear stale locks, then switches back to WAL mode.
|
||||
func openWithRecovery(ctx context.Context, path string) (*DB, error) {
|
||||
log.Info(
|
||||
"Database appears locked, attempting recovery with TRUNCATE mode",
|
||||
"path", path,
|
||||
)
|
||||
|
||||
conn, err = sql.Open(
|
||||
conn, err := sql.Open(
|
||||
"sqlite",
|
||||
path+"?_journal_mode=TRUNCATE&_synchronous=NORMAL&_busy_timeout=10000&_foreign_keys=ON",
|
||||
path+"?_journal_mode=TRUNCATE&_synchronous=NORMAL&_busy_timeout=10000"+
|
||||
"&_foreign_keys=ON",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening database in recovery mode: %w", err)
|
||||
}
|
||||
|
||||
// Set connection pool settings
|
||||
// SQLite can handle multiple readers but only one writer at a time.
|
||||
// Setting MaxOpenConns to 1 ensures all writes are serialized through
|
||||
// a single connection, preventing SQLITE_BUSY errors.
|
||||
conn.SetMaxOpenConns(1)
|
||||
conn.SetMaxIdleConns(1)
|
||||
configureConnPool(conn)
|
||||
|
||||
if err := conn.PingContext(ctx); err != nil {
|
||||
log.Debug("Failed to ping database in recovery mode, closing", "path", path, "error", err)
|
||||
err = conn.PingContext(ctx)
|
||||
if err != nil {
|
||||
log.Debug(
|
||||
"Failed to ping database in recovery mode, closing",
|
||||
"path", path, "error", err,
|
||||
)
|
||||
|
||||
_ = conn.Close()
|
||||
|
||||
@@ -175,20 +204,14 @@ func New(ctx context.Context, path string) (*DB, error) {
|
||||
// Switch back to WAL mode
|
||||
log.Debug("Switching database back to WAL mode", "path", path)
|
||||
|
||||
if _, err := conn.ExecContext(ctx, "PRAGMA journal_mode=WAL"); err != nil {
|
||||
_, err = conn.ExecContext(ctx, "PRAGMA journal_mode=WAL")
|
||||
if err != nil {
|
||||
log.Warn("Failed to switch back to WAL mode", "path", path, "error", err)
|
||||
}
|
||||
|
||||
// Ensure foreign keys are enabled
|
||||
if _, err := conn.ExecContext(ctx, "PRAGMA foreign_keys=ON"); err != nil {
|
||||
log.Warn("Failed to enable foreign keys", "path", path, "error", err)
|
||||
}
|
||||
|
||||
db := &DB{conn: conn, path: path}
|
||||
if err := applyMigrations(ctx, conn); err != nil {
|
||||
_ = conn.Close()
|
||||
|
||||
return nil, fmt.Errorf("applying migrations: %w", err)
|
||||
db, err := finishOpen(ctx, conn, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debug("Database connection established successfully", "path", path)
|
||||
@@ -196,6 +219,13 @@ func New(ctx context.Context, path string) (*DB, error) {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// NewTestDB creates an in-memory SQLite database for testing purposes.
|
||||
// The database is automatically initialized with the schema and is ready
|
||||
// for use. Each call creates a new independent database instance.
|
||||
func NewTestDB() (*DB, error) {
|
||||
return New(context.Background(), ":memory:")
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
// It ensures all pending operations are completed before closing.
|
||||
// Returns an error if the database connection cannot be closed properly.
|
||||
@@ -253,10 +283,11 @@ func (db *DB) ExecWithLog(
|
||||
return db.conn.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// QueryRowWithLog executes a query that returns at most one row with SQL logging.
|
||||
// This is useful for queries that modify data and return values (e.g., INSERT ... RETURNING).
|
||||
// SQLite handles its own locking internally.
|
||||
// The query and args parameters follow the same format as sql.DB.QueryRowContext.
|
||||
// QueryRowWithLog executes a query that returns at most one row with SQL
|
||||
// logging. This is useful for queries that modify data and return values
|
||||
// (e.g., INSERT ... RETURNING). SQLite handles its own locking internally.
|
||||
// The query and args parameters follow the same format as
|
||||
// sql.DB.QueryRowContext.
|
||||
func (db *DB) QueryRowWithLog(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
@@ -323,7 +354,8 @@ func bootstrapMigrationsTable(ctx context.Context, db *sql.DB) error {
|
||||
// the schema_migrations table via 000.sql, then iterates through remaining
|
||||
// migration files in order.
|
||||
func applyMigrations(ctx context.Context, db *sql.DB) error {
|
||||
if err := bootstrapMigrationsTable(ctx, db); err != nil {
|
||||
err := bootstrapMigrationsTable(ctx, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -383,15 +415,8 @@ func applyMigrations(ctx context.Context, db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewTestDB creates an in-memory SQLite database for testing purposes.
|
||||
// The database is automatically initialized with the schema and is ready for use.
|
||||
// Each call creates a new independent database instance.
|
||||
func NewTestDB() (*DB, error) {
|
||||
return New(context.Background(), ":memory:")
|
||||
}
|
||||
|
||||
// repeatPlaceholder generates a string of ", ?" repeated n times for IN clause construction.
|
||||
// For example, repeatPlaceholder(2) returns ", ?, ?".
|
||||
// repeatPlaceholder generates a string of ", ?" repeated n times for IN
|
||||
// clause construction. For example, repeatPlaceholder(2) returns ", ?, ?".
|
||||
func repeatPlaceholder(n int) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
@@ -401,12 +426,14 @@ func repeatPlaceholder(n int) string {
|
||||
}
|
||||
|
||||
// LogSQL logs SQL queries and their arguments when debug mode is enabled.
|
||||
// Debug mode is activated by setting the GODEBUG environment variable to include "vaultik".
|
||||
// This is useful for troubleshooting database operations and understanding query patterns.
|
||||
// Debug mode is activated by setting the GODEBUG environment variable to
|
||||
// include "vaultik". This is useful for troubleshooting database operations
|
||||
// and understanding query patterns.
|
||||
//
|
||||
// The operation parameter describes the type of SQL operation (e.g., "Execute", "Query").
|
||||
// The query parameter is the SQL statement being executed.
|
||||
// The args parameter contains the query arguments that will be interpolated.
|
||||
// The operation parameter describes the type of SQL operation (e.g.,
|
||||
// "Execute", "Query"). The query parameter is the SQL statement being
|
||||
// executed. The args parameter contains the query arguments that will be
|
||||
// interpolated.
|
||||
func LogSQL(operation, query string, args ...any) {
|
||||
if strings.Contains(os.Getenv("GODEBUG"), "vaultik") {
|
||||
log.Debug(
|
||||
|
||||
Reference in New Issue
Block a user