allow mysql alongside sqlite
This commit is contained in:
parent
31dbfa0fc3
commit
41f80008c9
3
go.mod
3
go.mod
@ -7,8 +7,11 @@ require (
|
|||||||
zombiezen.com/go/sqlite v1.4.2
|
zombiezen.com/go/sqlite v1.4.2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -1,5 +1,9 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
@ -5,21 +5,62 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
"zombiezen.com/go/sqlite/sqlitex"
|
"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
|
// Database wraps the SQL database connection
|
||||||
type Database struct {
|
type Database struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
pool *sqlitex.Pool // For achievements system compatibility
|
pool *sqlitex.Pool // For achievements system compatibility (SQLite only)
|
||||||
dbPath string // Store path for pool creation
|
config Config
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new database connection
|
// New creates a new database connection with the provided configuration
|
||||||
func New(path string) (*Database, error) {
|
func New(config Config) (*Database, error) {
|
||||||
db, err := sql.Open("sqlite", path)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
@ -30,26 +71,13 @@ func New(path string) (*Database, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set connection pool settings
|
// Set connection pool settings
|
||||||
db.SetMaxOpenConns(25)
|
db.SetMaxOpenConns(config.PoolSize)
|
||||||
db.SetMaxIdleConns(5)
|
db.SetMaxIdleConns(config.PoolSize / 5)
|
||||||
|
|
||||||
// Create sqlitex pool for achievements system
|
|
||||||
pool, err := sqlitex.NewPool(path, sqlitex.PoolOptions{
|
|
||||||
PoolSize: 5,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create sqlite pool: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d := &Database{
|
d := &Database{
|
||||||
db: db,
|
db: db,
|
||||||
pool: pool,
|
pool: pool,
|
||||||
dbPath: path,
|
config: config,
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize schema
|
|
||||||
if err := d.initSchema(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to initialize schema: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
@ -63,257 +91,16 @@ func (d *Database) Close() error {
|
|||||||
return d.db.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
|
// GetPool returns the sqlitex pool for achievements system compatibility
|
||||||
func (d *Database) GetPool() *sqlitex.Pool {
|
func (d *Database) GetPool() *sqlitex.Pool {
|
||||||
return d.pool
|
return d.pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// initSchema creates the database schema if it doesn't exist
|
|
||||||
func (d *Database) initSchema() error {
|
|
||||||
schemas := []string{
|
|
||||||
// Rules table
|
|
||||||
`CREATE TABLE IF NOT EXISTS rules (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
category TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
UNIQUE(category, name)
|
|
||||||
)`,
|
|
||||||
|
|
||||||
// Accounts table
|
|
||||||
`CREATE TABLE IF NOT EXISTS accounts (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT UNIQUE NOT NULL,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
email TEXT,
|
|
||||||
admin_level INTEGER DEFAULT 0,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_login DATETIME
|
|
||||||
)`,
|
|
||||||
|
|
||||||
// Characters table
|
|
||||||
`CREATE TABLE IF NOT EXISTS characters (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
account_id INTEGER NOT NULL,
|
|
||||||
name TEXT UNIQUE NOT NULL,
|
|
||||||
race_id INTEGER NOT NULL,
|
|
||||||
class_id INTEGER NOT NULL,
|
|
||||||
level INTEGER DEFAULT 1,
|
|
||||||
x REAL DEFAULT 0,
|
|
||||||
y REAL DEFAULT 0,
|
|
||||||
z REAL DEFAULT 0,
|
|
||||||
heading REAL DEFAULT 0,
|
|
||||||
zone_id INTEGER DEFAULT 0,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_played DATETIME,
|
|
||||||
FOREIGN KEY(account_id) REFERENCES accounts(id)
|
|
||||||
)`,
|
|
||||||
|
|
||||||
// Zones table
|
|
||||||
`CREATE TABLE IF NOT EXISTS zones (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
file TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
motd TEXT,
|
|
||||||
min_level INTEGER DEFAULT 0,
|
|
||||||
max_level INTEGER DEFAULT 100,
|
|
||||||
min_version INTEGER DEFAULT 0,
|
|
||||||
xp_modifier REAL DEFAULT 1.0,
|
|
||||||
city_zone INTEGER DEFAULT 0,
|
|
||||||
weather_allowed INTEGER DEFAULT 1,
|
|
||||||
safe_x REAL DEFAULT 0,
|
|
||||||
safe_y REAL DEFAULT 0,
|
|
||||||
safe_z REAL DEFAULT 0,
|
|
||||||
safe_heading REAL DEFAULT 0
|
|
||||||
)`,
|
|
||||||
|
|
||||||
// Server statistics table
|
|
||||||
`CREATE TABLE IF NOT EXISTS server_stats (
|
|
||||||
stat_id INTEGER PRIMARY KEY,
|
|
||||||
stat_value INTEGER,
|
|
||||||
stat_date INTEGER,
|
|
||||||
save_needed INTEGER DEFAULT 0
|
|
||||||
)`,
|
|
||||||
|
|
||||||
// Merchant tables
|
|
||||||
`CREATE TABLE IF NOT EXISTS merchants (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
merchant_type INTEGER DEFAULT 0
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS merchant_items (
|
|
||||||
merchant_id INTEGER NOT NULL,
|
|
||||||
item_id INTEGER NOT NULL,
|
|
||||||
quantity INTEGER DEFAULT -1,
|
|
||||||
price_coins INTEGER DEFAULT 0,
|
|
||||||
price_status INTEGER DEFAULT 0,
|
|
||||||
PRIMARY KEY(merchant_id, item_id),
|
|
||||||
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
|
|
||||||
)`,
|
|
||||||
|
|
||||||
// Achievement tables
|
|
||||||
`CREATE TABLE IF NOT EXISTS achievements (
|
|
||||||
achievement_id INTEGER PRIMARY KEY,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
uncompleted_text TEXT,
|
|
||||||
completed_text TEXT,
|
|
||||||
category TEXT,
|
|
||||||
expansion TEXT,
|
|
||||||
icon INTEGER DEFAULT 0,
|
|
||||||
point_value INTEGER DEFAULT 0,
|
|
||||||
qty_req INTEGER DEFAULT 1,
|
|
||||||
hide_achievement INTEGER DEFAULT 0,
|
|
||||||
unknown3a INTEGER DEFAULT 0,
|
|
||||||
unknown3b INTEGER DEFAULT 0
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS achievements_requirements (
|
|
||||||
achievement_id INTEGER NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
qty_req INTEGER DEFAULT 1,
|
|
||||||
PRIMARY KEY(achievement_id, name),
|
|
||||||
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS achievements_rewards (
|
|
||||||
achievement_id INTEGER NOT NULL,
|
|
||||||
reward TEXT NOT NULL,
|
|
||||||
PRIMARY KEY(achievement_id, reward),
|
|
||||||
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS character_achievements (
|
|
||||||
char_id INTEGER NOT NULL,
|
|
||||||
achievement_id INTEGER NOT NULL,
|
|
||||||
completed_date INTEGER,
|
|
||||||
PRIMARY KEY(char_id, achievement_id),
|
|
||||||
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS character_achievements_items (
|
|
||||||
char_id INTEGER NOT NULL,
|
|
||||||
achievement_id INTEGER NOT NULL,
|
|
||||||
items INTEGER DEFAULT 0,
|
|
||||||
PRIMARY KEY(char_id, achievement_id),
|
|
||||||
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
|
|
||||||
// Title tables
|
|
||||||
`CREATE TABLE IF NOT EXISTS titles (
|
|
||||||
title_id INTEGER PRIMARY KEY,
|
|
||||||
text TEXT NOT NULL,
|
|
||||||
category INTEGER DEFAULT 0,
|
|
||||||
rarity INTEGER DEFAULT 0,
|
|
||||||
position INTEGER DEFAULT 0,
|
|
||||||
description TEXT,
|
|
||||||
is_unique INTEGER DEFAULT 0,
|
|
||||||
is_hidden INTEGER DEFAULT 0,
|
|
||||||
color_code TEXT,
|
|
||||||
requirements TEXT,
|
|
||||||
source_type INTEGER DEFAULT 0,
|
|
||||||
source_id INTEGER DEFAULT 0,
|
|
||||||
created_date INTEGER,
|
|
||||||
expire_date INTEGER
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS character_titles (
|
|
||||||
char_id INTEGER NOT NULL,
|
|
||||||
title_id INTEGER NOT NULL,
|
|
||||||
source_achievement_id INTEGER DEFAULT 0,
|
|
||||||
source_quest_id INTEGER DEFAULT 0,
|
|
||||||
granted_date INTEGER,
|
|
||||||
expire_date INTEGER,
|
|
||||||
PRIMARY KEY(char_id, title_id),
|
|
||||||
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY(title_id) REFERENCES titles(title_id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS character_active_titles (
|
|
||||||
char_id INTEGER PRIMARY KEY,
|
|
||||||
prefix_title_id INTEGER DEFAULT 0,
|
|
||||||
suffix_title_id INTEGER DEFAULT 0,
|
|
||||||
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY(prefix_title_id) REFERENCES titles(title_id) ON DELETE SET NULL,
|
|
||||||
FOREIGN KEY(suffix_title_id) REFERENCES titles(title_id) ON DELETE SET NULL
|
|
||||||
)`,
|
|
||||||
|
|
||||||
// NPC tables
|
|
||||||
`CREATE TABLE IF NOT EXISTS npcs (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
level INTEGER DEFAULT 1,
|
|
||||||
max_level INTEGER DEFAULT 1,
|
|
||||||
race INTEGER DEFAULT 0,
|
|
||||||
model_type INTEGER DEFAULT 0,
|
|
||||||
size INTEGER DEFAULT 32,
|
|
||||||
hp INTEGER DEFAULT 100,
|
|
||||||
power INTEGER DEFAULT 100,
|
|
||||||
x REAL DEFAULT 0,
|
|
||||||
y REAL DEFAULT 0,
|
|
||||||
z REAL DEFAULT 0,
|
|
||||||
heading REAL DEFAULT 0,
|
|
||||||
respawn_time INTEGER DEFAULT 300,
|
|
||||||
zone_id INTEGER DEFAULT 0,
|
|
||||||
aggro_radius REAL DEFAULT 10,
|
|
||||||
ai_strategy INTEGER DEFAULT 0,
|
|
||||||
loot_table_id INTEGER DEFAULT 0,
|
|
||||||
merchant_type INTEGER DEFAULT 0,
|
|
||||||
randomize_appearance INTEGER DEFAULT 0,
|
|
||||||
show_name INTEGER DEFAULT 1,
|
|
||||||
show_level INTEGER DEFAULT 1,
|
|
||||||
targetable INTEGER DEFAULT 1,
|
|
||||||
show_command_icon INTEGER DEFAULT 1,
|
|
||||||
display_hand_icon INTEGER DEFAULT 0,
|
|
||||||
faction_id INTEGER DEFAULT 0,
|
|
||||||
created_date INTEGER,
|
|
||||||
last_modified INTEGER
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS npc_spells (
|
|
||||||
npc_id INTEGER NOT NULL,
|
|
||||||
spell_id INTEGER NOT NULL,
|
|
||||||
tier INTEGER DEFAULT 1,
|
|
||||||
hp_percentage INTEGER DEFAULT 100,
|
|
||||||
priority INTEGER DEFAULT 1,
|
|
||||||
cast_type INTEGER DEFAULT 0,
|
|
||||||
recast_delay INTEGER DEFAULT 5,
|
|
||||||
PRIMARY KEY(npc_id, spell_id),
|
|
||||||
FOREIGN KEY(npc_id) REFERENCES npcs(id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS npc_skills (
|
|
||||||
npc_id INTEGER NOT NULL,
|
|
||||||
skill_name TEXT NOT NULL,
|
|
||||||
skill_value INTEGER DEFAULT 0,
|
|
||||||
max_value INTEGER DEFAULT 0,
|
|
||||||
PRIMARY KEY(npc_id, skill_name),
|
|
||||||
FOREIGN KEY(npc_id) REFERENCES npcs(id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
|
|
||||||
`CREATE TABLE IF NOT EXISTS npc_loot (
|
|
||||||
npc_id INTEGER NOT NULL,
|
|
||||||
item_id INTEGER NOT NULL,
|
|
||||||
probability REAL DEFAULT 100.0,
|
|
||||||
min_level INTEGER DEFAULT 0,
|
|
||||||
max_level INTEGER DEFAULT 100,
|
|
||||||
PRIMARY KEY(npc_id, item_id),
|
|
||||||
FOREIGN KEY(npc_id) REFERENCES npcs(id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, schema := range schemas {
|
|
||||||
if _, err := d.db.Exec(schema); err != nil {
|
|
||||||
return fmt.Errorf("failed to create schema: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query executes a query that returns rows
|
// Query executes a query that returns rows
|
||||||
func (d *Database) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
func (d *Database) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||||
@ -369,6 +156,23 @@ func (d *Database) SaveRule(category, name, value, description string) error {
|
|||||||
return err
|
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
|
// GetZones retrieves all zones from the database
|
||||||
func (d *Database) GetZones() ([]map[string]interface{}, error) {
|
func (d *Database) GetZones() ([]map[string]interface{}, error) {
|
||||||
rows, err := d.Query(`
|
rows, err := d.Query(`
|
||||||
|
108
internal/database/database_test.go
Normal file
108
internal/database/database_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSQLite(t *testing.T) {
|
||||||
|
// Test SQLite connection
|
||||||
|
db, err := NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create SQLite database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test database type
|
||||||
|
if db.GetType() != SQLite {
|
||||||
|
t.Errorf("Expected SQLite database type, got %v", db.GetType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test basic query
|
||||||
|
result, err := db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get rows affected: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected != 0 {
|
||||||
|
t.Errorf("Expected 0 rows affected for CREATE TABLE, got %d", affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test insert
|
||||||
|
_, err = db.Exec("INSERT INTO test (name) VALUES (?)", "test_value")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to insert test data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test query
|
||||||
|
var name string
|
||||||
|
err = db.QueryRow("SELECT name FROM test WHERE id = 1").Scan(&name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to query test data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name != "test_value" {
|
||||||
|
t.Errorf("Expected 'test_value', got '%s'", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid_sqlite_config",
|
||||||
|
config: Config{
|
||||||
|
Type: SQLite,
|
||||||
|
DSN: "file::memory:?mode=memory&cache=shared",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid_database_type",
|
||||||
|
config: Config{
|
||||||
|
Type: DatabaseType(99),
|
||||||
|
DSN: "test",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
db, err := New(tt.config)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if db != nil {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDatabaseTypeMethods(t *testing.T) {
|
||||||
|
// Test SQLite
|
||||||
|
db, err := NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create SQLite database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if db.GetType() != SQLite {
|
||||||
|
t.Errorf("Expected SQLite type, got %v", db.GetType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify GetPool works for SQLite
|
||||||
|
pool := db.GetPool()
|
||||||
|
if pool == nil {
|
||||||
|
t.Error("Expected non-nil pool for SQLite database")
|
||||||
|
}
|
||||||
|
}
|
@ -108,13 +108,39 @@ JSON-based configuration with CLI overrides:
|
|||||||
"listen_port": 9000,
|
"listen_port": 9000,
|
||||||
"max_clients": 1000,
|
"max_clients": 1000,
|
||||||
"web_port": 8080,
|
"web_port": 8080,
|
||||||
"database_path": "world.db",
|
"database_type": "sqlite",
|
||||||
|
"database_path": "eq2.db",
|
||||||
|
"database_host": "localhost",
|
||||||
|
"database_port": 3306,
|
||||||
|
"database_name": "eq2emu",
|
||||||
|
"database_user": "eq2",
|
||||||
|
"database_pass": "password",
|
||||||
"server_name": "EQ2Go World Server",
|
"server_name": "EQ2Go World Server",
|
||||||
"xp_rate": 1.0,
|
"xp_rate": 1.0,
|
||||||
"ts_xp_rate": 1.0
|
"ts_xp_rate": 1.0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For MySQL/MariaDB configuration:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"database_type": "mysql",
|
||||||
|
"database_host": "localhost",
|
||||||
|
"database_port": 3306,
|
||||||
|
"database_name": "eq2emu",
|
||||||
|
"database_user": "eq2",
|
||||||
|
"database_pass": "password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For SQLite configuration (default):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"database_type": "sqlite",
|
||||||
|
"database_path": "eq2.db"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Basic Startup
|
### Basic Startup
|
||||||
|
@ -3,6 +3,7 @@ package world
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -78,7 +79,13 @@ type WorldConfig struct {
|
|||||||
WebKeyPassword string `json:"web_key_password"`
|
WebKeyPassword string `json:"web_key_password"`
|
||||||
|
|
||||||
// Database settings
|
// Database settings
|
||||||
DatabasePath string `json:"database_path"`
|
DatabaseType string `json:"database_type"` // "sqlite" or "mysql"
|
||||||
|
DatabasePath string `json:"database_path"` // For SQLite: file path
|
||||||
|
DatabaseHost string `json:"database_host"` // For MySQL: hostname
|
||||||
|
DatabasePort int `json:"database_port"` // For MySQL: port
|
||||||
|
DatabaseName string `json:"database_name"` // For MySQL: database name
|
||||||
|
DatabaseUser string `json:"database_user"` // For MySQL: username
|
||||||
|
DatabasePass string `json:"database_pass"` // For MySQL: password
|
||||||
|
|
||||||
// Server settings
|
// Server settings
|
||||||
ServerName string `json:"server_name"`
|
ServerName string `json:"server_name"`
|
||||||
@ -138,7 +145,25 @@ type ServerStatistics struct {
|
|||||||
// NewWorld creates a new world server instance
|
// NewWorld creates a new world server instance
|
||||||
func NewWorld(config *WorldConfig) (*World, error) {
|
func NewWorld(config *WorldConfig) (*World, error) {
|
||||||
// Initialize database
|
// Initialize database
|
||||||
db, err := database.New(config.DatabasePath)
|
var db *database.Database
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch strings.ToLower(config.DatabaseType) {
|
||||||
|
case "mysql", "mariadb":
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci",
|
||||||
|
config.DatabaseUser, config.DatabasePass, config.DatabaseHost, config.DatabasePort, config.DatabaseName)
|
||||||
|
db, err = database.NewMySQL(dsn)
|
||||||
|
case "sqlite", "":
|
||||||
|
// Default to SQLite if not specified
|
||||||
|
dbPath := config.DatabasePath
|
||||||
|
if dbPath == "" {
|
||||||
|
dbPath = "eq2.db"
|
||||||
|
}
|
||||||
|
db, err = database.NewSQLite(dbPath)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported database type: %s", config.DatabaseType)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize database: %w", err)
|
return nil, fmt.Errorf("failed to initialize database: %w", err)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user