package database import ( "database/sql" "fmt" "sync" _ "github.com/go-sql-driver/mysql" _ "modernc.org/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // DatabaseType represents the type of database backend type DatabaseType int const ( SQLite DatabaseType = iota MySQL ) // Config holds database configuration type Config struct { Type DatabaseType DSN string // Data Source Name (connection string) PoolSize int // Connection pool size } // Database wraps the SQL database connection type Database struct { db *sql.DB pool *sqlitex.Pool // For achievements system compatibility (SQLite only) config Config mutex sync.RWMutex } // New creates a new database connection with the provided configuration func New(config Config) (*Database, error) { var driverName string var pool *sqlitex.Pool // Set default pool size if config.PoolSize == 0 { config.PoolSize = 25 } switch config.Type { case SQLite: driverName = "sqlite" // Create sqlitex pool for achievements system compatibility var err error pool, err = sqlitex.NewPool(config.DSN, sqlitex.PoolOptions{ PoolSize: 5, }) if err != nil { return nil, fmt.Errorf("failed to create sqlite pool: %w", err) } case MySQL: driverName = "mysql" default: return nil, fmt.Errorf("unsupported database type: %d", config.Type) } db, err := sql.Open(driverName, config.DSN) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } // Test connection if err := db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } // Set connection pool settings db.SetMaxOpenConns(config.PoolSize) db.SetMaxIdleConns(config.PoolSize / 5) d := &Database{ db: db, pool: pool, config: config, } return d, nil } // Close closes the database connection func (d *Database) Close() error { if d.pool != nil { d.pool.Close() } return d.db.Close() } // GetType returns the database type func (d *Database) GetType() DatabaseType { return d.config.Type } // GetPool returns the sqlitex pool for achievements system compatibility func (d *Database) GetPool() *sqlitex.Pool { return d.pool } // Query executes a query that returns rows func (d *Database) Query(query string, args ...any) (*sql.Rows, error) { return d.db.Query(query, args...) } // QueryRow executes a query that returns a single row func (d *Database) QueryRow(query string, args ...any) *sql.Row { return d.db.QueryRow(query, args...) } // Exec executes a query that doesn't return rows func (d *Database) Exec(query string, args ...any) (sql.Result, error) { return d.db.Exec(query, args...) } // Begin starts a transaction func (d *Database) Begin() (*sql.Tx, error) { return d.db.Begin() } // LoadRules loads all rules from the database func (d *Database) LoadRules() (map[string]map[string]string, error) { rows, err := d.Query("SELECT category, name, value FROM rules") if err != nil { return nil, err } defer rows.Close() rules := make(map[string]map[string]string) for rows.Next() { var category, name, value string if err := rows.Scan(&category, &name, &value); err != nil { return nil, err } if rules[category] == nil { rules[category] = make(map[string]string) } rules[category][name] = value } return rules, rows.Err() } // SaveRule saves a rule to the database func (d *Database) SaveRule(category, name, value, description string) error { _, err := d.Exec(` INSERT OR REPLACE INTO rules (category, name, value, description) VALUES (?, ?, ?, ?) `, category, name, value, description) return err } // NewSQLite creates a new SQLite database connection func NewSQLite(path string) (*Database, error) { return New(Config{ Type: SQLite, DSN: path, }) } // NewMySQL creates a new MySQL/MariaDB database connection // DSN format: user:password@tcp(host:port)/database func NewMySQL(dsn string) (*Database, error) { return New(Config{ Type: MySQL, DSN: dsn, }) } // GetZones retrieves all zones from the database func (d *Database) GetZones() ([]map[string]any, error) { rows, err := d.Query(` SELECT id, name, file, description, motd, min_level, max_level, min_version, xp_modifier, city_zone, weather_allowed, safe_x, safe_y, safe_z, safe_heading FROM zones ORDER BY name `) if err != nil { return nil, err } defer rows.Close() var zones []map[string]any for rows.Next() { zone := make(map[string]any) var id, minLevel, maxLevel, minVersion int var name, file, description, motd string var xpModifier, safeX, safeY, safeZ, safeHeading float64 var cityZone, weatherAllowed bool err := rows.Scan(&id, &name, &file, &description, &motd, &minLevel, &maxLevel, &minVersion, &xpModifier, &cityZone, &weatherAllowed, &safeX, &safeY, &safeZ, &safeHeading) if err != nil { return nil, err } zone["id"] = id zone["name"] = name zone["file"] = file zone["description"] = description zone["motd"] = motd zone["min_level"] = minLevel zone["max_level"] = maxLevel zone["min_version"] = minVersion zone["xp_modifier"] = xpModifier zone["city_zone"] = cityZone zone["weather_allowed"] = weatherAllowed zone["safe_x"] = safeX zone["safe_y"] = safeY zone["safe_z"] = safeZ zone["safe_heading"] = safeHeading zones = append(zones, zone) } return zones, rows.Err() }