package database import ( "context" "database/sql" "fmt" "sync" _ "github.com/go-sql-driver/mysql" "zombiezen.com/go/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 database connections for both SQLite (zombiezen) and MySQL type Database struct { db *sql.DB // For MySQL pool *sqlitex.Pool // For SQLite (zombiezen) config Config mutex sync.RWMutex } // New creates a new database connection with the provided configuration func New(config Config) (*Database, error) { // Set default pool size if config.PoolSize == 0 { config.PoolSize = 25 } var db *sql.DB var pool *sqlitex.Pool switch config.Type { case SQLite: // Use zombiezen sqlite pool var err error pool, err = sqlitex.NewPool(config.DSN, sqlitex.PoolOptions{ PoolSize: config.PoolSize, }) if err != nil { return nil, fmt.Errorf("failed to create sqlite pool: %w", err) } case MySQL: // Use standard database/sql for MySQL var err error 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) default: return nil, fmt.Errorf("unsupported database type: %d", config.Type) } 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() } if d.db != nil { return d.db.Close() } return nil } // GetType returns the database type func (d *Database) GetType() DatabaseType { return d.config.Type } // GetPool returns the sqlitex pool func (d *Database) GetPool() *sqlitex.Pool { return d.pool } // Query executes a query that returns rows (database/sql compatibility) func (d *Database) Query(query string, args ...any) (*sql.Rows, error) { if d.config.Type == MySQL { return d.db.Query(query, args...) } return nil, fmt.Errorf("Query method only supported for MySQL; use ExecTransient for SQLite") } // QueryRow executes a query that returns a single row (database/sql compatibility) func (d *Database) QueryRow(query string, args ...any) *sql.Row { if d.config.Type == MySQL { return d.db.QueryRow(query, args...) } return nil // This will result in an error when scanned } // Exec executes a query that doesn't return rows (database/sql compatibility) func (d *Database) Exec(query string, args ...any) (sql.Result, error) { if d.config.Type == MySQL { return d.db.Exec(query, args...) } return nil, fmt.Errorf("Exec method only supported for MySQL; use Execute for SQLite") } // Begin starts a transaction (database/sql compatibility) func (d *Database) Begin() (*sql.Tx, error) { if d.config.Type == MySQL { return d.db.Begin() } return nil, fmt.Errorf("Begin method only supported for MySQL; use zombiezen transaction helpers for SQLite") } // Execute executes a query using the zombiezen sqlite approach (SQLite only) func (d *Database) Execute(query string, opts *sqlitex.ExecOptions) error { if d.config.Type != SQLite { return fmt.Errorf("Execute method only supported for SQLite") } conn, err := d.pool.Take(context.Background()) if err != nil { return err } defer d.pool.Put(conn) return sqlitex.Execute(conn, query, opts) } // ExecTransient executes a transient query and calls resultFn for each row (SQLite only) func (d *Database) ExecTransient(query string, resultFn func(stmt *sqlite.Stmt) error, args ...any) error { if d.config.Type != SQLite { return fmt.Errorf("ExecTransient method only supported for SQLite") } conn, err := d.pool.Take(context.Background()) if err != nil { return err } defer d.pool.Put(conn) return sqlitex.ExecTransient(conn, query, resultFn, args...) } // LoadRules loads all rules from the database func (d *Database) LoadRules() (map[string]map[string]string, error) { rules := make(map[string]map[string]string) if d.config.Type == SQLite { err := d.ExecTransient("SELECT category, name, value FROM rules", func(stmt *sqlite.Stmt) error { category := stmt.ColumnText(0) name := stmt.ColumnText(1) value := stmt.ColumnText(2) if rules[category] == nil { rules[category] = make(map[string]string) } rules[category][name] = value return nil }) return rules, err } else { // MySQL using database/sql 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 { if d.config.Type == SQLite { return d.Execute(` INSERT OR REPLACE INTO rules (category, name, value, description) VALUES (?, ?, ?, ?) `, &sqlitex.ExecOptions{ Args: []any{category, name, value, description}, }) } else { // MySQL using database/sql _, 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 } } // 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) { 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 ` if d.config.Type == SQLite { err := d.ExecTransient(query, func(stmt *sqlite.Stmt) error { zone := make(map[string]any) zone["id"] = stmt.ColumnInt(0) zone["name"] = stmt.ColumnText(1) zone["file"] = stmt.ColumnText(2) zone["description"] = stmt.ColumnText(3) zone["motd"] = stmt.ColumnText(4) zone["min_level"] = stmt.ColumnInt(5) zone["max_level"] = stmt.ColumnInt(6) zone["min_version"] = stmt.ColumnInt(7) zone["xp_modifier"] = stmt.ColumnFloat(8) zone["city_zone"] = stmt.ColumnBool(9) zone["weather_allowed"] = stmt.ColumnBool(10) zone["safe_x"] = stmt.ColumnFloat(11) zone["safe_y"] = stmt.ColumnFloat(12) zone["safe_z"] = stmt.ColumnFloat(13) zone["safe_heading"] = stmt.ColumnFloat(14) zones = append(zones, zone) return nil }) return zones, err } else { // MySQL using database/sql 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() } }