Files
docker-agent/pkg/sqliteutil/sqlite.go
T
David Gageot d7e406c9bf Enable modernize and perfsprint linters, fix all findings
Add modernize and perfsprint to .golangci.yml and resolve all lint
issues across 97 files:

- Replace fmt.Errorf with errors.New where no format verbs are used
- Replace fmt.Sprintf(%d) with strconv.Itoa for int conversions
- Replace redundant fmt.Sprintf(%s) with direct string values
- Use slices.Contains instead of manual loops (SkillsConfig.HasLocal)
- Use omitzero instead of omitempty on struct fields (time.Time, SkillsConfig)
- Inline boolPtr helpers to new(bool) and remove unused functions
- Replace string concatenation in loops with strings.Builder
- Use fmt.Fprintf instead of WriteString(fmt.Sprintf(...))

Assisted-By: docker-agent
2026-03-10 11:57:30 +01:00

91 lines
2.9 KiB
Go

package sqliteutil
import (
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"modernc.org/sqlite"
sqlite3 "modernc.org/sqlite/lib"
)
// OpenDB opens a SQLite database with recommended pragmas for concurrency and foreign key support.
// It configures the connection pool for serialized writes (MaxOpenConns=1).
func OpenDB(path string) (*sql.DB, error) {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, fmt.Errorf("cannot create database directory %q: %w", dir, err)
}
// Add query parameters for better concurrency handling and data integrity
// _pragma=busy_timeout(5000): Wait up to 5 seconds if database is locked
// _pragma=journal_mode(WAL): Enable Write-Ahead Logging for better concurrent access
// _pragma=foreign_keys(1): Enable foreign key constraints (critical for ON DELETE CASCADE)
dsn := path + "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"
db, err := sql.Open("sqlite", dsn)
if err != nil {
if IsCantOpenError(err) {
return nil, DiagnoseDBOpenError(path, err)
}
return nil, err
}
// Configure connection pool to serialize writes (SQLite limitation)
// This prevents "database is locked" errors from concurrent writes
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0)
// Verify connection works (this will trigger file creation/open)
if err := db.Ping(); err != nil {
db.Close()
if IsCantOpenError(err) {
return nil, DiagnoseDBOpenError(path, err)
}
return nil, err
}
return db, nil
}
// IsCantOpenError checks if the error is a SQLite CANTOPEN error (code 14).
func IsCantOpenError(err error) bool {
if sqliteErr, ok := errors.AsType[*sqlite.Error](err); ok {
return sqliteErr.Code() == sqlite3.SQLITE_CANTOPEN
}
return false
}
// IsNoSuchColumnError checks if the error is due to a missing column in SQLite.
// This typically happens when querying a column that doesn't exist in the schema.
func IsNoSuchColumnError(err error) bool {
if sqliteErr, ok := errors.AsType[*sqlite.Error](err); ok {
// SQLITE_ERROR (1) is the generic SQL error code used for "no such column"
return sqliteErr.Code() == sqlite3.SQLITE_ERROR
}
return false
}
// DiagnoseDBOpenError provides a more helpful error message when SQLite
// fails to open/create a database file.
func DiagnoseDBOpenError(path string, originalErr error) error {
dir := filepath.Dir(path)
info, err := os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("cannot create database at %q: directory %q does not exist", path, dir)
}
return fmt.Errorf("cannot create database at %q: %w", path, err)
}
if !info.IsDir() {
return fmt.Errorf("cannot create database at %q: %q is not a directory", path, dir)
}
return fmt.Errorf("cannot create database at %q: permission denied or file cannot be created in %q (original error: %w)", path, dir, originalErr)
}