Files
tankstopp-app/internal/database/config.go
T
2025-07-07 01:44:12 +02:00

496 lines
15 KiB
Go

package database
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/spf13/viper"
"gorm.io/gorm/logger"
)
// Config holds database configuration settings
type Config struct {
// Database file path
DatabasePath string
// Connection pool settings
MaxIdleConns int
MaxOpenConns int
ConnMaxLifetime time.Duration
ConnMaxIdleTime time.Duration
// Logging settings
LogLevel logger.LogLevel
SlowQueryLog time.Duration
// Migration settings
AutoMigrate bool
DropTableFirst bool
CreateBatchSize int
// Performance settings
PrepareStmt bool
DisableForeignKeyCheck bool
IgnoreRelationshipsWhenMigrating bool
// Development settings
Debug bool
DryRun bool
QueryFields bool
CreateInBatches int
}
// DefaultConfig returns a configuration with sensible defaults
func DefaultConfig() *Config {
return &Config{
DatabasePath: "fuel_stops.db",
MaxIdleConns: 10,
MaxOpenConns: 100,
ConnMaxLifetime: time.Hour,
ConnMaxIdleTime: 30 * time.Minute,
LogLevel: logger.Silent,
SlowQueryLog: 200 * time.Millisecond,
AutoMigrate: true,
DropTableFirst: false,
CreateBatchSize: 1000,
PrepareStmt: true,
DisableForeignKeyCheck: false,
IgnoreRelationshipsWhenMigrating: false,
Debug: false,
DryRun: false,
QueryFields: false,
CreateInBatches: 100,
}
}
// LoadFromConfig loads configuration from config file using Viper
func LoadFromConfig(configPath string) *Config {
config := DefaultConfig()
// Initialize Viper
v := viper.New()
// Set config file path if provided
if configPath != "" {
v.SetConfigFile(configPath)
} else {
// Search for config file in multiple locations
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath(".")
v.AddConfigPath("./config")
v.AddConfigPath("$HOME/.tankstopp")
v.AddConfigPath("/etc/tankstopp")
}
// Try to read config file
if err := v.ReadInConfig(); err != nil {
// If config file not found, fall back to environment variables
return LoadFromEnv()
}
// Load database configuration from Viper
if v.IsSet("database.path") {
config.DatabasePath = v.GetString("database.path")
}
// Connection pool settings
if v.IsSet("database.connection_pool.max_idle_connections") {
config.MaxIdleConns = v.GetInt("database.connection_pool.max_idle_connections")
}
if v.IsSet("database.connection_pool.max_open_connections") {
config.MaxOpenConns = v.GetInt("database.connection_pool.max_open_connections")
}
if v.IsSet("database.connection_pool.connection_max_lifetime") {
config.ConnMaxLifetime = v.GetDuration("database.connection_pool.connection_max_lifetime")
}
if v.IsSet("database.connection_pool.connection_max_idle_time") {
config.ConnMaxIdleTime = v.GetDuration("database.connection_pool.connection_max_idle_time")
}
// Logging settings
if v.IsSet("database.logging.level") {
config.LogLevel = getLogLevelFromString(v.GetString("database.logging.level"))
}
if v.IsSet("database.logging.debug") {
config.Debug = v.GetBool("database.logging.debug")
}
if v.IsSet("database.logging.slow_query_threshold") {
config.SlowQueryLog = v.GetDuration("database.logging.slow_query_threshold")
}
// Migration settings
if v.IsSet("database.migration.auto_migrate") {
config.AutoMigrate = v.GetBool("database.migration.auto_migrate")
}
if v.IsSet("database.migration.drop_tables_first") {
config.DropTableFirst = v.GetBool("database.migration.drop_tables_first")
}
if v.IsSet("database.migration.create_batch_size") {
config.CreateBatchSize = v.GetInt("database.migration.create_batch_size")
}
// Performance settings
if v.IsSet("database.performance.prepare_statements") {
config.PrepareStmt = v.GetBool("database.performance.prepare_statements")
}
if v.IsSet("database.performance.disable_foreign_key_check") {
config.DisableForeignKeyCheck = v.GetBool("database.performance.disable_foreign_key_check")
}
if v.IsSet("database.performance.ignore_relationships_when_migrating") {
config.IgnoreRelationshipsWhenMigrating = v.GetBool("database.performance.ignore_relationships_when_migrating")
}
if v.IsSet("database.performance.query_fields") {
config.QueryFields = v.GetBool("database.performance.query_fields")
}
if v.IsSet("database.performance.dry_run") {
config.DryRun = v.GetBool("database.performance.dry_run")
}
if v.IsSet("database.performance.create_in_batches") {
config.CreateInBatches = v.GetInt("database.performance.create_in_batches")
}
// Environment variables still take precedence over config file
config = mergeWithEnvVars(config)
return config
}
// LoadFromEnv loads configuration from environment variables
func LoadFromEnv() *Config {
config := DefaultConfig()
// Database path
if dbPath := os.Getenv("DB_PATH"); dbPath != "" {
config.DatabasePath = dbPath
}
// Connection pool settings
if maxIdle := getEnvInt("DB_MAX_IDLE_CONNS", config.MaxIdleConns); maxIdle > 0 {
config.MaxIdleConns = maxIdle
}
if maxOpen := getEnvInt("DB_MAX_OPEN_CONNS", config.MaxOpenConns); maxOpen > 0 {
config.MaxOpenConns = maxOpen
}
if lifetime := getEnvDuration("DB_CONN_MAX_LIFETIME", config.ConnMaxLifetime); lifetime > 0 {
config.ConnMaxLifetime = lifetime
}
if idleTime := getEnvDuration("DB_CONN_MAX_IDLE_TIME", config.ConnMaxIdleTime); idleTime > 0 {
config.ConnMaxIdleTime = idleTime
}
// Logging settings
config.LogLevel = getLogLevel()
config.Debug = getEnvBool("DB_DEBUG", config.Debug)
if slowLog := getEnvDuration("DB_SLOW_QUERY_LOG", config.SlowQueryLog); slowLog > 0 {
config.SlowQueryLog = slowLog
}
// Migration settings
config.AutoMigrate = getEnvBool("DB_AUTO_MIGRATE", config.AutoMigrate)
config.DropTableFirst = getEnvBool("DB_DROP_TABLE_FIRST", config.DropTableFirst)
if batchSize := getEnvInt("DB_CREATE_BATCH_SIZE", config.CreateBatchSize); batchSize > 0 {
config.CreateBatchSize = batchSize
}
// Performance settings
config.PrepareStmt = getEnvBool("DB_PREPARE_STMT", config.PrepareStmt)
config.DisableForeignKeyCheck = getEnvBool("DB_DISABLE_FOREIGN_KEY_CHECK", config.DisableForeignKeyCheck)
config.IgnoreRelationshipsWhenMigrating = getEnvBool("DB_IGNORE_RELATIONSHIPS_WHEN_MIGRATING", config.IgnoreRelationshipsWhenMigrating)
// Development settings
config.DryRun = getEnvBool("DB_DRY_RUN", config.DryRun)
config.QueryFields = getEnvBool("DB_QUERY_FIELDS", config.QueryFields)
if inBatches := getEnvInt("DB_CREATE_IN_BATCHES", config.CreateInBatches); inBatches > 0 {
config.CreateInBatches = inBatches
}
return config
}
// mergeWithEnvVars merges environment variables into existing config
// Environment variables take precedence over config file values
func mergeWithEnvVars(config *Config) *Config {
// Database path
if dbPath := os.Getenv("DB_PATH"); dbPath != "" {
config.DatabasePath = dbPath
}
// Connection pool settings
if maxIdle := getEnvInt("DB_MAX_IDLE_CONNS", config.MaxIdleConns); maxIdle > 0 {
config.MaxIdleConns = maxIdle
}
if maxOpen := getEnvInt("DB_MAX_OPEN_CONNS", config.MaxOpenConns); maxOpen > 0 {
config.MaxOpenConns = maxOpen
}
if lifetime := getEnvDuration("DB_CONN_MAX_LIFETIME", config.ConnMaxLifetime); lifetime > 0 {
config.ConnMaxLifetime = lifetime
}
if idleTime := getEnvDuration("DB_CONN_MAX_IDLE_TIME", config.ConnMaxIdleTime); idleTime > 0 {
config.ConnMaxIdleTime = idleTime
}
// Logging settings
if envLogLevel := getLogLevel(); envLogLevel != logger.Silent {
config.LogLevel = envLogLevel
}
if envDebug := os.Getenv("DB_DEBUG"); envDebug != "" {
config.Debug = getEnvBool("DB_DEBUG", config.Debug)
}
if slowLog := getEnvDuration("DB_SLOW_QUERY_LOG", config.SlowQueryLog); slowLog > 0 {
config.SlowQueryLog = slowLog
}
// Migration settings
if envAutoMigrate := os.Getenv("DB_AUTO_MIGRATE"); envAutoMigrate != "" {
config.AutoMigrate = getEnvBool("DB_AUTO_MIGRATE", config.AutoMigrate)
}
if envDropFirst := os.Getenv("DB_DROP_TABLE_FIRST"); envDropFirst != "" {
config.DropTableFirst = getEnvBool("DB_DROP_TABLE_FIRST", config.DropTableFirst)
}
if batchSize := getEnvInt("DB_CREATE_BATCH_SIZE", config.CreateBatchSize); batchSize > 0 {
config.CreateBatchSize = batchSize
}
// Performance settings
if envPrepare := os.Getenv("DB_PREPARE_STMT"); envPrepare != "" {
config.PrepareStmt = getEnvBool("DB_PREPARE_STMT", config.PrepareStmt)
}
if envFKCheck := os.Getenv("DB_DISABLE_FOREIGN_KEY_CHECK"); envFKCheck != "" {
config.DisableForeignKeyCheck = getEnvBool("DB_DISABLE_FOREIGN_KEY_CHECK", config.DisableForeignKeyCheck)
}
if envIgnoreRel := os.Getenv("DB_IGNORE_RELATIONSHIPS_WHEN_MIGRATING"); envIgnoreRel != "" {
config.IgnoreRelationshipsWhenMigrating = getEnvBool("DB_IGNORE_RELATIONSHIPS_WHEN_MIGRATING", config.IgnoreRelationshipsWhenMigrating)
}
// Development settings
if envDryRun := os.Getenv("DB_DRY_RUN"); envDryRun != "" {
config.DryRun = getEnvBool("DB_DRY_RUN", config.DryRun)
}
if envQueryFields := os.Getenv("DB_QUERY_FIELDS"); envQueryFields != "" {
config.QueryFields = getEnvBool("DB_QUERY_FIELDS", config.QueryFields)
}
if inBatches := getEnvInt("DB_CREATE_IN_BATCHES", config.CreateInBatches); inBatches > 0 {
config.CreateInBatches = inBatches
}
return config
}
// Validate checks if the configuration is valid
func (c *Config) Validate() error {
if c.DatabasePath == "" {
return fmt.Errorf("database path cannot be empty")
}
if c.MaxIdleConns < 0 {
return fmt.Errorf("max idle connections cannot be negative")
}
if c.MaxOpenConns < 0 {
return fmt.Errorf("max open connections cannot be negative")
}
if c.MaxIdleConns > c.MaxOpenConns && c.MaxOpenConns > 0 {
return fmt.Errorf("max idle connections (%d) cannot be greater than max open connections (%d)",
c.MaxIdleConns, c.MaxOpenConns)
}
if c.ConnMaxLifetime < 0 {
return fmt.Errorf("connection max lifetime cannot be negative")
}
if c.ConnMaxIdleTime < 0 {
return fmt.Errorf("connection max idle time cannot be negative")
}
if c.SlowQueryLog < 0 {
return fmt.Errorf("slow query log threshold cannot be negative")
}
if c.CreateBatchSize <= 0 {
return fmt.Errorf("create batch size must be greater than 0")
}
if c.CreateInBatches <= 0 {
return fmt.Errorf("create in batches size must be greater than 0")
}
return nil
}
// String returns a string representation of the configuration
func (c *Config) String() string {
return fmt.Sprintf(`Database Configuration:
Database Path: %s
Max Idle Connections: %d
Max Open Connections: %d
Connection Max Lifetime: %v
Connection Max Idle Time: %v
Log Level: %v
Slow Query Log Threshold: %v
Auto Migrate: %t
Prepare Statements: %t
Debug Mode: %t
Dry Run: %t
Create Batch Size: %d
Create In Batches: %d`,
c.DatabasePath,
c.MaxIdleConns,
c.MaxOpenConns,
c.ConnMaxLifetime,
c.ConnMaxIdleTime,
c.LogLevel,
c.SlowQueryLog,
c.AutoMigrate,
c.PrepareStmt,
c.Debug,
c.DryRun,
c.CreateBatchSize,
c.CreateInBatches,
)
}
// IsProduction returns true if running in production environment
func (c *Config) IsProduction() bool {
env := os.Getenv("ENV")
return env == "production" || env == "prod"
}
// IsDevelopment returns true if running in development environment
func (c *Config) IsDevelopment() bool {
env := os.Getenv("ENV")
return env == "development" || env == "dev" || env == ""
}
// IsTest returns true if running in test environment
func (c *Config) IsTest() bool {
env := os.Getenv("ENV")
return env == "test" || env == "testing"
}
// Helper functions
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
func getEnvBool(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
if boolValue, err := strconv.ParseBool(value); err == nil {
return boolValue
}
}
return defaultValue
}
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return defaultValue
}
func getLogLevel() logger.LogLevel {
debug := getEnvBool("DB_DEBUG", false)
env := os.Getenv("ENV")
logLevel := os.Getenv("DB_LOG_LEVEL")
switch {
case debug:
return logger.Info
case env == "development" || env == "dev":
return logger.Warn
case env == "test" || env == "testing":
return logger.Silent
case logLevel == "silent":
return logger.Silent
case logLevel == "error":
return logger.Error
case logLevel == "warn":
return logger.Warn
case logLevel == "info":
return logger.Info
default:
return logger.Silent
}
}
// getLogLevelFromString converts string log level to GORM logger level
func getLogLevelFromString(level string) logger.LogLevel {
switch strings.ToLower(level) {
case "silent":
return logger.Silent
case "error":
return logger.Error
case "warn", "warning":
return logger.Warn
case "info":
return logger.Info
default:
return logger.Silent
}
}
// Environment variable documentation
/*
Available Environment Variables:
Database Settings:
DB_PATH - Database file path (default: "fuel_stops.db")
DB_AUTO_MIGRATE - Enable automatic migrations (default: true)
DB_DROP_TABLE_FIRST - Drop tables before migration (default: false)
Connection Pool Settings:
DB_MAX_IDLE_CONNS - Maximum idle connections (default: 10)
DB_MAX_OPEN_CONNS - Maximum open connections (default: 100)
DB_CONN_MAX_LIFETIME - Connection maximum lifetime (default: "1h")
DB_CONN_MAX_IDLE_TIME - Connection maximum idle time (default: "30m")
Logging Settings:
DB_DEBUG - Enable debug logging (default: false)
DB_LOG_LEVEL - Log level: silent, error, warn, info (default: silent)
DB_SLOW_QUERY_LOG - Slow query threshold (default: "200ms")
Performance Settings:
DB_PREPARE_STMT - Use prepared statements (default: true)
DB_CREATE_BATCH_SIZE - Batch size for migrations (default: 1000)
DB_CREATE_IN_BATCHES - Batch size for bulk operations (default: 100)
DB_QUERY_FIELDS - Select only required fields (default: false)
Development Settings:
ENV - Environment: development, production, test
DB_DRY_RUN - Enable dry run mode (default: false)
DB_DISABLE_FOREIGN_KEY_CHECK - Disable FK checks (default: false)
DB_IGNORE_RELATIONSHIPS_WHEN_MIGRATING - Ignore relationships in migration (default: false)
Examples:
export DB_DEBUG=true
export DB_MAX_OPEN_CONNS=200
export DB_CONN_MAX_LIFETIME=2h
export DB_LOG_LEVEL=info
export ENV=development
*/