package database import ( "database/sql" "fmt" "sync" _ "github.com/go-sql-driver/mysql" ) // DatabaseType represents the type of database backend type DatabaseType int const ( MySQL DatabaseType = iota ) // Config holds database configuration type Config struct { DSN string // Data Source Name (connection string) PoolSize int // Connection pool size } // Database wraps MySQL database connections type Database struct { db *sql.DB config Config mutex sync.RWMutex } // New creates a new MySQL database connection with the provided configuration func New(config Config) (*Database, error) { // Set default pool size if config.PoolSize == 0 { config.PoolSize = 25 } // Use standard database/sql for MySQL db, err := sql.Open("mysql", config.DSN) if err != nil { return nil, fmt.Errorf("failed to open mysql database: %w", err) } // Test connection if err := db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping mysql database: %w", err) } // Set connection pool settings db.SetMaxOpenConns(config.PoolSize) db.SetMaxIdleConns(config.PoolSize / 5) d := &Database{ db: db, config: config, } return d, nil } // Close closes the database connection func (d *Database) Close() error { if d.db != nil { return d.db.Close() } return nil } // GetType returns the database type func (d *Database) GetType() DatabaseType { return MySQL } // 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) { rules := make(map[string]map[string]string) rows, err := d.Query("SELECT category, name, value FROM rules") if err != nil { return nil, err } defer rows.Close() 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 INTO rules (category, name, value, description) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), description = VALUES(description) `, category, name, value, description) return err } // 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{ DSN: dsn, }) } // QuerySingle executes a query that returns a single row // Returns true if a row was found, false otherwise func (d *Database) QuerySingle(query string, args ...any) (*sql.Row, bool) { row := d.db.QueryRow(query, args...) // We can't determine if a row exists without scanning, so we assume it exists // The caller should handle sql.ErrNoRows appropriately return row, true } // Exists checks if a query returns any rows func (d *Database) Exists(query string, args ...any) (bool, error) { rows, err := d.Query(query, args...) if err != nil { return false, err } defer rows.Close() return rows.Next(), rows.Err() } // InsertReturningID executes an INSERT and returns the last insert ID func (d *Database) InsertReturningID(query string, args ...any) (int64, error) { result, err := d.Exec(query, args...) if err != nil { return 0, err } return result.LastInsertId() } // UpdateOrInsert performs an UPSERT operation using MySQL ON DUPLICATE KEY UPDATE func (d *Database) UpdateOrInsert(table string, data map[string]any, conflictColumns []string) error { columns := make([]string, 0, len(data)) placeholders := make([]string, 0, len(data)) updates := make([]string, 0, len(data)) args := make([]any, 0, len(data)) for col, val := range data { columns = append(columns, fmt.Sprintf("`%s`", col)) placeholders = append(placeholders, "?") updates = append(updates, fmt.Sprintf("`%s` = VALUES(`%s`)", col, col)) args = append(args, val) } columnStr := "" for i, col := range columns { if i > 0 { columnStr += ", " } columnStr += col } placeholderStr := "" for i := range placeholders { if i > 0 { placeholderStr += ", " } placeholderStr += "?" } updateStr := "" for i, upd := range updates { if i > 0 { updateStr += ", " } updateStr += upd } query := fmt.Sprintf("INSERT INTO `%s` (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s", table, columnStr, placeholderStr, updateStr) _, err := d.Exec(query, args...) return err } // GetZones retrieves all zones from the database func (d *Database) GetZones() ([]map[string]any, error) { var zones []map[string]any 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 ` rows, err := d.Query(query) if err != nil { return nil, err } defer rows.Close() 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() }