267 lines
6.4 KiB
Go
267 lines
6.4 KiB
Go
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()
|
|
}
|