diff --git a/cmd/start.go b/cmd/start.go index 12adaf3..2dad03c 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -34,7 +34,8 @@ var startCmd = &cobra.Command{ // Database Setup dbDSN := viper.GetString("db-dsn") - database, err := db.NewDB(ctx, logger, dbDSN) + dbConfig := db.DefaultDBConfig(dbDSN) + database, err := db.NewDB(ctx, logger, dbConfig) if err != nil { logger.Error("failed to initialize database", slog.Any("error", err)) os.Exit(1) diff --git a/internal/db/database.go b/internal/db/database.go index aebc31c..b7e8dc1 100644 --- a/internal/db/database.go +++ b/internal/db/database.go @@ -8,6 +8,7 @@ import ( "log/slog" "os" "path/filepath" + "time" _ "github.com/mattn/go-sqlite3" // SQLite driver ) @@ -17,30 +18,56 @@ var ddl string // DBConfig holds database configuration. type DBConfig struct { - DSN string // Data Source Name for SQLite + DSN string // Data Source Name for SQLite + MaxOpenConns int // Maximum number of open connections + MaxIdleConns int // Maximum number of idle connections + ConnMaxLifetime time.Duration // Maximum lifetime of connections + ConnMaxIdleTime time.Duration // Maximum idle time for connections +} + +// DefaultDBConfig returns a DBConfig with sensible defaults for SQLite. +func DefaultDBConfig(dsn string) *DBConfig { + return &DBConfig{ + DSN: dsn, + MaxOpenConns: 25, // SQLite performs better with limited connections + MaxIdleConns: 10, // Keep some connections idle for reuse + ConnMaxLifetime: 30 * time.Minute, // Rotate connections every 30 minutes + ConnMaxIdleTime: 5 * time.Minute, // Close idle connections after 5 minutes + } } // NewDB initializes and returns a new database connection pool and runs migrations. -func NewDB(ctx context.Context, logger *slog.Logger, dsn string) (*sql.DB, error) { +func NewDB(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB, error) { // Ensure the directory for the SQLite file exists - dbDir := filepath.Dir(dsn) + dbDir := filepath.Dir(config.DSN) if err := os.MkdirAll(dbDir, 0755); err != nil { return nil, fmt.Errorf("failed to create database directory %s: %w", dbDir, err) } - db, err := sql.Open("sqlite3", dsn+"?_foreign_keys=on") // Enable foreign key constraints + db, err := sql.Open("sqlite3", config.DSN+"?_foreign_keys=on") // Enable foreign key constraints if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } + // Configure connection pool + db.SetMaxOpenConns(config.MaxOpenConns) + db.SetMaxIdleConns(config.MaxIdleConns) + db.SetConnMaxLifetime(config.ConnMaxLifetime) + db.SetConnMaxIdleTime(config.ConnMaxIdleTime) + if err = db.PingContext(ctx); err != nil { + db.Close() // Clean up connection on failure return nil, fmt.Errorf("failed to ping database: %w", err) } - logger.Info("database connection established", slog.String("dsn", dsn)) + logger.Info("database connection established", + slog.String("dsn", config.DSN), + slog.Int("max_open_conns", config.MaxOpenConns), + slog.Int("max_idle_conns", config.MaxIdleConns)) // Execute schema. if _, err := db.ExecContext(ctx, ddl); err != nil { + db.Close() // Clean up connection on failure return nil, fmt.Errorf("failed to execute DDL: %w", err) } logger.Info("database schema applied")