496 lines
15 KiB
Go
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
|
|
*/
|