eq2go/internal/database/database.go

227 lines
5.3 KiB
Go

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 ...interface{}) (*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 ...interface{}) *sql.Row {
return d.db.QueryRow(query, args...)
}
// Exec executes a query that doesn't return rows
func (d *Database) Exec(query string, args ...interface{}) (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]interface{}, 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]interface{}
for rows.Next() {
zone := make(map[string]interface{})
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()
}