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 */