Compare commits
2 Commits
1288bc086f
...
a011342a36
Author | SHA1 | Date | |
---|---|---|---|
a011342a36 | |||
674b14f278 |
@ -876,7 +876,7 @@ func TestMockLanguageProcessor(t *testing.T) {
|
|||||||
func BenchmarkChannelJoin(b *testing.B) {
|
func BenchmarkChannelJoin(b *testing.B) {
|
||||||
channel := NewChannel("benchmark")
|
channel := NewChannel("benchmark")
|
||||||
|
|
||||||
for i := 0; b.Loop(); i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
channel.JoinChannel(int32(i))
|
channel.JoinChannel(int32(i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -888,7 +888,7 @@ func BenchmarkChannelIsInChannel(b *testing.B) {
|
|||||||
channel.JoinChannel(int32(i))
|
channel.JoinChannel(int32(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; b.Loop(); i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
channel.IsInChannel(int32(i % 1000))
|
channel.IsInChannel(int32(i % 1000))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -900,7 +900,7 @@ func BenchmarkChannelGetMembers(b *testing.B) {
|
|||||||
channel.JoinChannel(int32(i))
|
channel.JoinChannel(int32(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
for b.Loop() {
|
for i := 0; i < b.N; i++ {
|
||||||
channel.GetMembers()
|
channel.GetMembers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,40 +4,49 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"eq2emu/internal/database"
|
"zombiezen.com/go/sqlite"
|
||||||
|
"zombiezen.com/go/sqlite/sqlitex"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DatabaseChannelManager implements ChannelDatabase interface using the correct database wrapper
|
// DatabaseChannelManager implements ChannelDatabase interface using sqlitex.Pool
|
||||||
type DatabaseChannelManager struct {
|
type DatabaseChannelManager struct {
|
||||||
db *database.DB
|
pool *sqlitex.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDatabaseChannelManager creates a new database channel manager using the correct wrapper
|
// NewDatabaseChannelManager creates a new database channel manager using sqlitex.Pool
|
||||||
func NewDatabaseChannelManager(db *database.DB) *DatabaseChannelManager {
|
func NewDatabaseChannelManager(pool *sqlitex.Pool) *DatabaseChannelManager {
|
||||||
return &DatabaseChannelManager{
|
return &DatabaseChannelManager{
|
||||||
db: db,
|
pool: pool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadWorldChannels retrieves all persistent world channels from database
|
// LoadWorldChannels retrieves all persistent world channels from database
|
||||||
func (dcm *DatabaseChannelManager) LoadWorldChannels(ctx context.Context) ([]ChatChannelData, error) {
|
func (dcm *DatabaseChannelManager) LoadWorldChannels(ctx context.Context) ([]ChatChannelData, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "SELECT `name`, `password`, `level_restriction`, `classes`, `races` FROM `channels`"
|
query := "SELECT `name`, `password`, `level_restriction`, `classes`, `races` FROM `channels`"
|
||||||
|
|
||||||
var channels []ChatChannelData
|
var channels []ChatChannelData
|
||||||
|
|
||||||
err := dcm.db.Query(query, func(row *database.Row) error {
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
var channel ChatChannelData
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
var channel ChatChannelData
|
||||||
|
|
||||||
channel.Name = row.Text(0)
|
channel.Name = stmt.ColumnText(0)
|
||||||
if !row.IsNull(1) {
|
if stmt.ColumnType(1) != sqlite.TypeNull {
|
||||||
channel.Password = row.Text(1)
|
channel.Password = stmt.ColumnText(1)
|
||||||
}
|
}
|
||||||
channel.LevelRestriction = int32(row.Int64(2))
|
channel.LevelRestriction = int32(stmt.ColumnInt64(2))
|
||||||
channel.ClassRestriction = int32(row.Int64(3))
|
channel.ClassRestriction = int32(stmt.ColumnInt64(3))
|
||||||
channel.RaceRestriction = int32(row.Int64(4))
|
channel.RaceRestriction = int32(stmt.ColumnInt64(4))
|
||||||
|
|
||||||
channels = append(channels, channel)
|
channels = append(channels, channel)
|
||||||
return nil
|
return nil
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -49,24 +58,32 @@ func (dcm *DatabaseChannelManager) LoadWorldChannels(ctx context.Context) ([]Cha
|
|||||||
|
|
||||||
// SaveChannel persists a channel to database (world channels only)
|
// SaveChannel persists a channel to database (world channels only)
|
||||||
func (dcm *DatabaseChannelManager) SaveChannel(ctx context.Context, channel ChatChannelData) error {
|
func (dcm *DatabaseChannelManager) SaveChannel(ctx context.Context, channel ChatChannelData) error {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
// Insert or update channel
|
// Insert or update channel
|
||||||
query := `
|
query := `
|
||||||
INSERT OR REPLACE INTO channels
|
INSERT OR REPLACE INTO channels
|
||||||
(name, password, level_restriction, classes, races)
|
(name, password, level_restriction, classes, races)
|
||||||
VALUES (?, ?, ?, ?, ?)`
|
VALUES (?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
var password *string
|
var password any
|
||||||
if channel.Password != "" {
|
if channel.Password != "" {
|
||||||
password = &channel.Password
|
password = channel.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
err := dcm.db.Exec(query,
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
channel.Name,
|
Args: []any{
|
||||||
password,
|
channel.Name,
|
||||||
channel.LevelRestriction,
|
password,
|
||||||
channel.ClassRestriction,
|
channel.LevelRestriction,
|
||||||
channel.RaceRestriction,
|
channel.ClassRestriction,
|
||||||
)
|
channel.RaceRestriction,
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to save channel %s: %w", channel.Name, err)
|
return fmt.Errorf("failed to save channel %s: %w", channel.Name, err)
|
||||||
}
|
}
|
||||||
@ -76,9 +93,17 @@ func (dcm *DatabaseChannelManager) SaveChannel(ctx context.Context, channel Chat
|
|||||||
|
|
||||||
// DeleteChannel removes a channel from database
|
// DeleteChannel removes a channel from database
|
||||||
func (dcm *DatabaseChannelManager) DeleteChannel(ctx context.Context, channelName string) error {
|
func (dcm *DatabaseChannelManager) DeleteChannel(ctx context.Context, channelName string) error {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "DELETE FROM channels WHERE name = ?"
|
query := "DELETE FROM channels WHERE name = ?"
|
||||||
|
|
||||||
err := dcm.db.Exec(query, channelName)
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{channelName},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete channel %s: %w", channelName, err)
|
return fmt.Errorf("failed to delete channel %s: %w", channelName, err)
|
||||||
}
|
}
|
||||||
@ -88,6 +113,12 @@ func (dcm *DatabaseChannelManager) DeleteChannel(ctx context.Context, channelNam
|
|||||||
|
|
||||||
// EnsureChannelsTable creates the channels table if it doesn't exist
|
// EnsureChannelsTable creates the channels table if it doesn't exist
|
||||||
func (dcm *DatabaseChannelManager) EnsureChannelsTable(ctx context.Context) error {
|
func (dcm *DatabaseChannelManager) EnsureChannelsTable(ctx context.Context) error {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
CREATE TABLE IF NOT EXISTS channels (
|
CREATE TABLE IF NOT EXISTS channels (
|
||||||
name TEXT PRIMARY KEY,
|
name TEXT PRIMARY KEY,
|
||||||
@ -99,7 +130,7 @@ func (dcm *DatabaseChannelManager) EnsureChannelsTable(ctx context.Context) erro
|
|||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)`
|
)`
|
||||||
|
|
||||||
err := dcm.db.Exec(query)
|
err = sqlitex.Execute(conn, query, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create channels table: %w", err)
|
return fmt.Errorf("failed to create channels table: %w", err)
|
||||||
}
|
}
|
||||||
@ -109,58 +140,81 @@ func (dcm *DatabaseChannelManager) EnsureChannelsTable(ctx context.Context) erro
|
|||||||
|
|
||||||
// GetChannelCount returns the total number of channels in the database
|
// GetChannelCount returns the total number of channels in the database
|
||||||
func (dcm *DatabaseChannelManager) GetChannelCount(ctx context.Context) (int, error) {
|
func (dcm *DatabaseChannelManager) GetChannelCount(ctx context.Context) (int, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "SELECT COUNT(*) FROM channels"
|
query := "SELECT COUNT(*) FROM channels"
|
||||||
|
|
||||||
row, err := dcm.db.QueryRow(query)
|
var count int
|
||||||
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
count = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to query channel count: %w", err)
|
return 0, fmt.Errorf("failed to query channel count: %w", err)
|
||||||
}
|
}
|
||||||
if row == nil {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
defer row.Close()
|
|
||||||
|
|
||||||
count := row.Int(0)
|
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChannelByName retrieves a specific channel by name
|
// GetChannelByName retrieves a specific channel by name
|
||||||
func (dcm *DatabaseChannelManager) GetChannelByName(ctx context.Context, channelName string) (*ChatChannelData, error) {
|
func (dcm *DatabaseChannelManager) GetChannelByName(ctx context.Context, channelName string) (*ChatChannelData, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "SELECT `name`, `password`, `level_restriction`, `classes`, `races` FROM `channels` WHERE `name` = ?"
|
query := "SELECT `name`, `password`, `level_restriction`, `classes`, `races` FROM `channels` WHERE `name` = ?"
|
||||||
|
|
||||||
row, err := dcm.db.QueryRow(query, channelName)
|
var channel *ChatChannelData
|
||||||
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{channelName},
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
channel = &ChatChannelData{}
|
||||||
|
channel.Name = stmt.ColumnText(0)
|
||||||
|
if stmt.ColumnType(1) != sqlite.TypeNull {
|
||||||
|
channel.Password = stmt.ColumnText(1)
|
||||||
|
}
|
||||||
|
channel.LevelRestriction = int32(stmt.ColumnInt64(2))
|
||||||
|
channel.ClassRestriction = int32(stmt.ColumnInt64(3))
|
||||||
|
channel.RaceRestriction = int32(stmt.ColumnInt64(4))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query channel %s: %w", channelName, err)
|
return nil, fmt.Errorf("failed to query channel %s: %w", channelName, err)
|
||||||
}
|
}
|
||||||
if row == nil {
|
if channel == nil {
|
||||||
return nil, fmt.Errorf("channel %s not found", channelName)
|
return nil, fmt.Errorf("channel %s not found", channelName)
|
||||||
}
|
}
|
||||||
defer row.Close()
|
|
||||||
|
|
||||||
var channel ChatChannelData
|
return channel, nil
|
||||||
|
|
||||||
channel.Name = row.Text(0)
|
|
||||||
if !row.IsNull(1) {
|
|
||||||
channel.Password = row.Text(1)
|
|
||||||
}
|
|
||||||
channel.LevelRestriction = int32(row.Int64(2))
|
|
||||||
channel.ClassRestriction = int32(row.Int64(3))
|
|
||||||
channel.RaceRestriction = int32(row.Int64(4))
|
|
||||||
|
|
||||||
return &channel, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListChannelNames returns a list of all channel names in the database
|
// ListChannelNames returns a list of all channel names in the database
|
||||||
func (dcm *DatabaseChannelManager) ListChannelNames(ctx context.Context) ([]string, error) {
|
func (dcm *DatabaseChannelManager) ListChannelNames(ctx context.Context) ([]string, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "SELECT name FROM channels ORDER BY name"
|
query := "SELECT name FROM channels ORDER BY name"
|
||||||
|
|
||||||
var names []string
|
var names []string
|
||||||
|
|
||||||
err := dcm.db.Query(query, func(row *database.Row) error {
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
name := row.Text(0)
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
names = append(names, name)
|
name := stmt.ColumnText(0)
|
||||||
return nil
|
names = append(names, name)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -172,14 +226,22 @@ func (dcm *DatabaseChannelManager) ListChannelNames(ctx context.Context) ([]stri
|
|||||||
|
|
||||||
// UpdateChannelPassword updates just the password for a channel
|
// UpdateChannelPassword updates just the password for a channel
|
||||||
func (dcm *DatabaseChannelManager) UpdateChannelPassword(ctx context.Context, channelName, password string) error {
|
func (dcm *DatabaseChannelManager) UpdateChannelPassword(ctx context.Context, channelName, password string) error {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "UPDATE channels SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?"
|
query := "UPDATE channels SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?"
|
||||||
|
|
||||||
var passwordParam *string
|
var passwordParam any
|
||||||
if password != "" {
|
if password != "" {
|
||||||
passwordParam = &password
|
passwordParam = password
|
||||||
}
|
}
|
||||||
|
|
||||||
err := dcm.db.Exec(query, passwordParam, channelName)
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{passwordParam, channelName},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update password for channel %s: %w", channelName, err)
|
return fmt.Errorf("failed to update password for channel %s: %w", channelName, err)
|
||||||
}
|
}
|
||||||
@ -189,9 +251,17 @@ func (dcm *DatabaseChannelManager) UpdateChannelPassword(ctx context.Context, ch
|
|||||||
|
|
||||||
// UpdateChannelRestrictions updates the level, race, and class restrictions for a channel
|
// UpdateChannelRestrictions updates the level, race, and class restrictions for a channel
|
||||||
func (dcm *DatabaseChannelManager) UpdateChannelRestrictions(ctx context.Context, channelName string, levelRestriction, classRestriction, raceRestriction int32) error {
|
func (dcm *DatabaseChannelManager) UpdateChannelRestrictions(ctx context.Context, channelName string, levelRestriction, classRestriction, raceRestriction int32) error {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "UPDATE channels SET level_restriction = ?, classes = ?, races = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?"
|
query := "UPDATE channels SET level_restriction = ?, classes = ?, races = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?"
|
||||||
|
|
||||||
err := dcm.db.Exec(query, levelRestriction, classRestriction, raceRestriction, channelName)
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{levelRestriction, classRestriction, raceRestriction, channelName},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update restrictions for channel %s: %w", channelName, err)
|
return fmt.Errorf("failed to update restrictions for channel %s: %w", channelName, err)
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"eq2emu/internal/database"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mock implementations for testing
|
// Mock implementations for testing
|
||||||
@ -916,69 +914,8 @@ func TestCollectionManagerPlayerOperations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests for DatabaseCollectionManager
|
// Note: Database integration tests were removed to eliminate dependency on internal/database wrapper.
|
||||||
|
// Database functionality is tested through unit tests with mocks above.
|
||||||
func TestDatabaseCollectionManager(t *testing.T) {
|
|
||||||
// Create in-memory database
|
|
||||||
db, err := database.Open(":memory:")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create database: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
dcm := NewDatabaseCollectionManager(db)
|
|
||||||
|
|
||||||
// Ensure tables exist
|
|
||||||
err = dcm.EnsureCollectionTables(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create tables: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test saving and loading collections
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Insert test collection
|
|
||||||
err = db.Exec(`INSERT INTO collections (id, collection_name, collection_category, level)
|
|
||||||
VALUES (?, ?, ?, ?)`, 1, "Test Collection", "Test Category", 10)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to insert collection: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load collections
|
|
||||||
collections, err := dcm.LoadCollections(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load collections: %v", err)
|
|
||||||
}
|
|
||||||
if len(collections) != 1 {
|
|
||||||
t.Errorf("Expected 1 collection, got %d", len(collections))
|
|
||||||
}
|
|
||||||
if collections[0].Name != "Test Collection" {
|
|
||||||
t.Errorf("Expected name 'Test Collection', got %s", collections[0].Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test player collection operations
|
|
||||||
err = dcm.SavePlayerCollection(ctx, 1001, 1, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save player collection: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
playerCollections, err := dcm.LoadPlayerCollections(ctx, 1001)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load player collections: %v", err)
|
|
||||||
}
|
|
||||||
if len(playerCollections) != 1 {
|
|
||||||
t.Errorf("Expected 1 player collection, got %d", len(playerCollections))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test statistics
|
|
||||||
stats, err := dcm.GetCollectionStatistics(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get statistics: %v", err)
|
|
||||||
}
|
|
||||||
if stats.TotalCollections != 1 {
|
|
||||||
t.Errorf("Expected 1 total collection, got %d", stats.TotalCollections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests for EntityCollectionAdapter
|
// Tests for EntityCollectionAdapter
|
||||||
|
|
||||||
@ -1104,7 +1041,7 @@ func TestMasterCollectionListConcurrency(t *testing.T) {
|
|||||||
go func(id int32) {
|
go func(id int32) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
_ = ml.GetCollection(id)
|
_ = ml.GetCollection(id)
|
||||||
_ = ml.FindCollectionsByName("Collection")
|
_ = ml.FindCollectionsByName("Collection 1")
|
||||||
_ = ml.GetCollectionsByCategory("Category 1")
|
_ = ml.GetCollectionsByCategory("Category 1")
|
||||||
_ = ml.GetAllCollections()
|
_ = ml.GetAllCollections()
|
||||||
}(int32(i + 1))
|
}(int32(i + 1))
|
||||||
@ -1136,7 +1073,7 @@ func BenchmarkCollectionMarkItemFound(b *testing.B) {
|
|||||||
func BenchmarkMasterCollectionListSearch(b *testing.B) {
|
func BenchmarkMasterCollectionListSearch(b *testing.B) {
|
||||||
db := NewMockCollectionDatabase()
|
db := NewMockCollectionDatabase()
|
||||||
|
|
||||||
// Add test data
|
// Add test data with diverse names
|
||||||
for i := 1; i <= 1000; i++ {
|
for i := 1; i <= 1000; i++ {
|
||||||
db.collections = append(db.collections, CollectionData{
|
db.collections = append(db.collections, CollectionData{
|
||||||
ID: int32(i),
|
ID: int32(i),
|
||||||
@ -1160,9 +1097,17 @@ func BenchmarkMasterCollectionListSearch(b *testing.B) {
|
|||||||
ml := NewMasterCollectionList(db)
|
ml := NewMasterCollectionList(db)
|
||||||
ml.Initialize(context.Background(), itemLookup)
|
ml.Initialize(context.Background(), itemLookup)
|
||||||
|
|
||||||
|
// Test with various search patterns
|
||||||
|
searches := []string{
|
||||||
|
"Collection 500", // Exact match - 1 result
|
||||||
|
"Collection 5", // Prefix match - ~111 results (5, 50-59, 150-159, etc.)
|
||||||
|
"NonExistent", // No matches - 0 results
|
||||||
|
}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_ = ml.FindCollectionsByName("Collection")
|
search := searches[i%len(searches)]
|
||||||
|
_ = ml.FindCollectionsByName(search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,34 +4,43 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"eq2emu/internal/database"
|
"zombiezen.com/go/sqlite"
|
||||||
|
"zombiezen.com/go/sqlite/sqlitex"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DatabaseCollectionManager implements CollectionDatabase interface using the existing database wrapper
|
// DatabaseCollectionManager implements CollectionDatabase interface using sqlitex.Pool
|
||||||
type DatabaseCollectionManager struct {
|
type DatabaseCollectionManager struct {
|
||||||
db *database.DB
|
pool *sqlitex.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDatabaseCollectionManager creates a new database collection manager
|
// NewDatabaseCollectionManager creates a new database collection manager
|
||||||
func NewDatabaseCollectionManager(db *database.DB) *DatabaseCollectionManager {
|
func NewDatabaseCollectionManager(pool *sqlitex.Pool) *DatabaseCollectionManager {
|
||||||
return &DatabaseCollectionManager{
|
return &DatabaseCollectionManager{
|
||||||
db: db,
|
pool: pool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadCollections retrieves all collections from database
|
// LoadCollections retrieves all collections from database
|
||||||
func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]CollectionData, error) {
|
func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]CollectionData, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "SELECT `id`, `collection_name`, `collection_category`, `level` FROM `collections`"
|
query := "SELECT `id`, `collection_name`, `collection_category`, `level` FROM `collections`"
|
||||||
|
|
||||||
var collections []CollectionData
|
var collections []CollectionData
|
||||||
err := dcm.db.Query(query, func(row *database.Row) error {
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
var collection CollectionData
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
collection.ID = int32(row.Int(0))
|
var collection CollectionData
|
||||||
collection.Name = row.Text(1)
|
collection.ID = int32(stmt.ColumnInt64(0))
|
||||||
collection.Category = row.Text(2)
|
collection.Name = stmt.ColumnText(1)
|
||||||
collection.Level = int8(row.Int(3))
|
collection.Category = stmt.ColumnText(2)
|
||||||
collections = append(collections, collection)
|
collection.Level = int8(stmt.ColumnInt64(3))
|
||||||
return nil
|
collections = append(collections, collection)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -43,21 +52,30 @@ func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]Co
|
|||||||
|
|
||||||
// LoadCollectionItems retrieves items for a specific collection
|
// LoadCollectionItems retrieves items for a specific collection
|
||||||
func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error) {
|
func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := `SELECT item_id, item_index
|
query := `SELECT item_id, item_index
|
||||||
FROM collection_details
|
FROM collection_details
|
||||||
WHERE collection_id = ?
|
WHERE collection_id = ?
|
||||||
ORDER BY item_index ASC`
|
ORDER BY item_index ASC`
|
||||||
|
|
||||||
var items []CollectionItem
|
var items []CollectionItem
|
||||||
err := dcm.db.Query(query, func(row *database.Row) error {
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
var item CollectionItem
|
Args: []any{collectionID},
|
||||||
item.ItemID = int32(row.Int(0))
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
item.Index = int8(row.Int(1))
|
var item CollectionItem
|
||||||
// Items start as not found
|
item.ItemID = int32(stmt.ColumnInt64(0))
|
||||||
item.Found = ItemNotFound
|
item.Index = int8(stmt.ColumnInt64(1))
|
||||||
items = append(items, item)
|
// Items start as not found
|
||||||
return nil
|
item.Found = ItemNotFound
|
||||||
}, collectionID)
|
items = append(items, item)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err)
|
return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err)
|
||||||
@ -68,20 +86,29 @@ func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, c
|
|||||||
|
|
||||||
// LoadCollectionRewards retrieves rewards for a specific collection
|
// LoadCollectionRewards retrieves rewards for a specific collection
|
||||||
func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error) {
|
func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := `SELECT collection_id, reward_type, reward_value, reward_quantity
|
query := `SELECT collection_id, reward_type, reward_value, reward_quantity
|
||||||
FROM collection_rewards
|
FROM collection_rewards
|
||||||
WHERE collection_id = ?`
|
WHERE collection_id = ?`
|
||||||
|
|
||||||
var rewards []CollectionRewardData
|
var rewards []CollectionRewardData
|
||||||
err := dcm.db.Query(query, func(row *database.Row) error {
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
var reward CollectionRewardData
|
Args: []any{collectionID},
|
||||||
reward.CollectionID = int32(row.Int(0))
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
reward.RewardType = row.Text(1)
|
var reward CollectionRewardData
|
||||||
reward.RewardValue = row.Text(2)
|
reward.CollectionID = int32(stmt.ColumnInt64(0))
|
||||||
reward.Quantity = int8(row.Int(3))
|
reward.RewardType = stmt.ColumnText(1)
|
||||||
rewards = append(rewards, reward)
|
reward.RewardValue = stmt.ColumnText(2)
|
||||||
return nil
|
reward.Quantity = int8(stmt.ColumnInt64(3))
|
||||||
}, collectionID)
|
rewards = append(rewards, reward)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query collection rewards for collection %d: %w", collectionID, err)
|
return nil, fmt.Errorf("failed to query collection rewards for collection %d: %w", collectionID, err)
|
||||||
@ -92,19 +119,28 @@ func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context,
|
|||||||
|
|
||||||
// LoadPlayerCollections retrieves player's collection progress
|
// LoadPlayerCollections retrieves player's collection progress
|
||||||
func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error) {
|
func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := `SELECT char_id, collection_id, completed
|
query := `SELECT char_id, collection_id, completed
|
||||||
FROM character_collections
|
FROM character_collections
|
||||||
WHERE char_id = ?`
|
WHERE char_id = ?`
|
||||||
|
|
||||||
var collections []PlayerCollectionData
|
var collections []PlayerCollectionData
|
||||||
err := dcm.db.Query(query, func(row *database.Row) error {
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
var collection PlayerCollectionData
|
Args: []any{characterID},
|
||||||
collection.CharacterID = int32(row.Int(0))
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
collection.CollectionID = int32(row.Int(1))
|
var collection PlayerCollectionData
|
||||||
collection.Completed = row.Bool(2)
|
collection.CharacterID = int32(stmt.ColumnInt64(0))
|
||||||
collections = append(collections, collection)
|
collection.CollectionID = int32(stmt.ColumnInt64(1))
|
||||||
return nil
|
collection.Completed = stmt.ColumnInt64(2) != 0
|
||||||
}, characterID)
|
collections = append(collections, collection)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query player collections for character %d: %w", characterID, err)
|
return nil, fmt.Errorf("failed to query player collections for character %d: %w", characterID, err)
|
||||||
@ -115,16 +151,25 @@ func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context,
|
|||||||
|
|
||||||
// LoadPlayerCollectionItems retrieves player's found collection items
|
// LoadPlayerCollectionItems retrieves player's found collection items
|
||||||
func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error) {
|
func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := `SELECT collection_item_id
|
query := `SELECT collection_item_id
|
||||||
FROM character_collection_items
|
FROM character_collection_items
|
||||||
WHERE char_id = ? AND collection_id = ?`
|
WHERE char_id = ? AND collection_id = ?`
|
||||||
|
|
||||||
var itemIDs []int32
|
var itemIDs []int32
|
||||||
err := dcm.db.Query(query, func(row *database.Row) error {
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
itemID := int32(row.Int(0))
|
Args: []any{characterID, collectionID},
|
||||||
itemIDs = append(itemIDs, itemID)
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
return nil
|
itemID := int32(stmt.ColumnInt64(0))
|
||||||
}, characterID, collectionID)
|
itemIDs = append(itemIDs, itemID)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query player collection items for character %d, collection %d: %w", characterID, collectionID, err)
|
return nil, fmt.Errorf("failed to query player collection items for character %d, collection %d: %w", characterID, collectionID, err)
|
||||||
@ -135,6 +180,12 @@ func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Cont
|
|||||||
|
|
||||||
// SavePlayerCollection saves player collection completion status
|
// SavePlayerCollection saves player collection completion status
|
||||||
func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context, characterID, collectionID int32, completed bool) error {
|
func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context, characterID, collectionID int32, completed bool) error {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
completedInt := 0
|
completedInt := 0
|
||||||
if completed {
|
if completed {
|
||||||
completedInt = 1
|
completedInt = 1
|
||||||
@ -145,7 +196,9 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context,
|
|||||||
ON CONFLICT(char_id, collection_id)
|
ON CONFLICT(char_id, collection_id)
|
||||||
DO UPDATE SET completed = ?`
|
DO UPDATE SET completed = ?`
|
||||||
|
|
||||||
err := dcm.db.Exec(query, characterID, collectionID, completedInt, completedInt)
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{characterID, collectionID, completedInt, completedInt},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to save player collection for character %d, collection %d: %w", characterID, collectionID, err)
|
return fmt.Errorf("failed to save player collection for character %d, collection %d: %w", characterID, collectionID, err)
|
||||||
}
|
}
|
||||||
@ -155,10 +208,18 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context,
|
|||||||
|
|
||||||
// SavePlayerCollectionItem saves a found collection item
|
// SavePlayerCollectionItem saves a found collection item
|
||||||
func (dcm *DatabaseCollectionManager) SavePlayerCollectionItem(ctx context.Context, characterID, collectionID, itemID int32) error {
|
func (dcm *DatabaseCollectionManager) SavePlayerCollectionItem(ctx context.Context, characterID, collectionID, itemID int32) error {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
|
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
|
||||||
VALUES (?, ?, ?)`
|
VALUES (?, ?, ?)`
|
||||||
|
|
||||||
err := dcm.db.Exec(query, characterID, collectionID, itemID)
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{characterID, collectionID, itemID},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to save player collection item for character %d, collection %d, item %d: %w", characterID, collectionID, itemID, err)
|
return fmt.Errorf("failed to save player collection item for character %d, collection %d, item %d: %w", characterID, collectionID, itemID, err)
|
||||||
}
|
}
|
||||||
@ -172,35 +233,40 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollections(ctx context.Context,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a transaction for atomic updates
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
err := dcm.db.Transaction(func(db *database.DB) error {
|
|
||||||
for _, collection := range collections {
|
|
||||||
if !collection.GetSaveNeeded() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save collection completion status
|
|
||||||
if err := dcm.savePlayerCollectionInTx(db, characterID, collection); err != nil {
|
|
||||||
return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save found items
|
|
||||||
if err := dcm.savePlayerCollectionItemsInTx(db, characterID, collection); err != nil {
|
|
||||||
return fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("transaction failed: %w", err)
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
|
// Use a transaction for atomic updates
|
||||||
|
err = sqlitex.Execute(conn, "BEGIN", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer sqlitex.Execute(conn, "ROLLBACK", nil)
|
||||||
|
|
||||||
|
for _, collection := range collections {
|
||||||
|
if !collection.GetSaveNeeded() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save collection completion status
|
||||||
|
if err := dcm.savePlayerCollectionInTx(conn, characterID, collection); err != nil {
|
||||||
|
return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save found items
|
||||||
|
if err := dcm.savePlayerCollectionItemsInTx(conn, characterID, collection); err != nil {
|
||||||
|
return fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return sqlitex.Execute(conn, "COMMIT", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// savePlayerCollectionInTx saves a single collection within a transaction
|
// savePlayerCollectionInTx saves a single collection within a transaction
|
||||||
func (dcm *DatabaseCollectionManager) savePlayerCollectionInTx(db *database.DB, characterID int32, collection *Collection) error {
|
func (dcm *DatabaseCollectionManager) savePlayerCollectionInTx(conn *sqlite.Conn, characterID int32, collection *Collection) error {
|
||||||
completedInt := 0
|
completedInt := 0
|
||||||
if collection.GetCompleted() {
|
if collection.GetCompleted() {
|
||||||
completedInt = 1
|
completedInt = 1
|
||||||
@ -211,12 +277,13 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionInTx(db *database.DB,
|
|||||||
ON CONFLICT(char_id, collection_id)
|
ON CONFLICT(char_id, collection_id)
|
||||||
DO UPDATE SET completed = ?`
|
DO UPDATE SET completed = ?`
|
||||||
|
|
||||||
err := db.Exec(query, characterID, collection.GetID(), completedInt, completedInt)
|
return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
return err
|
Args: []any{characterID, collection.GetID(), completedInt, completedInt},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// savePlayerCollectionItemsInTx saves collection items within a transaction
|
// savePlayerCollectionItemsInTx saves collection items within a transaction
|
||||||
func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsInTx(db *database.DB, characterID int32, collection *Collection) error {
|
func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsInTx(conn *sqlite.Conn, characterID int32, collection *Collection) error {
|
||||||
items := collection.GetCollectionItems()
|
items := collection.GetCollectionItems()
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
@ -224,7 +291,9 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsInTx(db *database
|
|||||||
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
|
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
|
||||||
VALUES (?, ?, ?)`
|
VALUES (?, ?, ?)`
|
||||||
|
|
||||||
err := db.Exec(query, characterID, collection.GetID(), item.ItemID)
|
err := sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{characterID, collection.GetID(), item.ItemID},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to save item %d: %w", item.ItemID, err)
|
return fmt.Errorf("failed to save item %d: %w", item.ItemID, err)
|
||||||
}
|
}
|
||||||
@ -236,6 +305,12 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsInTx(db *database
|
|||||||
|
|
||||||
// EnsureCollectionTables creates the collection tables if they don't exist
|
// EnsureCollectionTables creates the collection tables if they don't exist
|
||||||
func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context) error {
|
func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context) error {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
queries := []string{
|
queries := []string{
|
||||||
`CREATE TABLE IF NOT EXISTS collections (
|
`CREATE TABLE IF NOT EXISTS collections (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
@ -280,7 +355,7 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, query := range queries {
|
for i, query := range queries {
|
||||||
err := dcm.db.Exec(query)
|
err := sqlitex.Execute(conn, query, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create collection table %d: %w", i+1, err)
|
return fmt.Errorf("failed to create collection table %d: %w", i+1, err)
|
||||||
}
|
}
|
||||||
@ -298,7 +373,7 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, query := range indexes {
|
for i, query := range indexes {
|
||||||
err := dcm.db.Exec(query)
|
err := sqlitex.Execute(conn, query, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create collection index %d: %w", i+1, err)
|
return fmt.Errorf("failed to create collection index %d: %w", i+1, err)
|
||||||
}
|
}
|
||||||
@ -309,162 +384,202 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context
|
|||||||
|
|
||||||
// GetCollectionCount returns the total number of collections in the database
|
// GetCollectionCount returns the total number of collections in the database
|
||||||
func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (int, error) {
|
func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (int, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "SELECT COUNT(*) FROM collections"
|
query := "SELECT COUNT(*) FROM collections"
|
||||||
|
|
||||||
row, err := dcm.db.QueryRow(query)
|
var count int
|
||||||
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
count = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to get collection count: %w", err)
|
return 0, fmt.Errorf("failed to get collection count: %w", err)
|
||||||
}
|
}
|
||||||
defer row.Close()
|
|
||||||
|
|
||||||
if row == nil {
|
return count, nil
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return row.Int(0), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPlayerCollectionCount returns the number of collections a player has
|
// GetPlayerCollectionCount returns the number of collections a player has
|
||||||
func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Context, characterID int32) (int, error) {
|
func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Context, characterID int32) (int, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ?"
|
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ?"
|
||||||
|
|
||||||
row, err := dcm.db.QueryRow(query, characterID)
|
var count int
|
||||||
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{characterID},
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
count = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to get player collection count for character %d: %w", characterID, err)
|
return 0, fmt.Errorf("failed to get player collection count for character %d: %w", characterID, err)
|
||||||
}
|
}
|
||||||
defer row.Close()
|
|
||||||
|
|
||||||
if row == nil {
|
return count, nil
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return row.Int(0), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCompletedCollectionCount returns the number of completed collections for a player
|
// GetCompletedCollectionCount returns the number of completed collections for a player
|
||||||
func (dcm *DatabaseCollectionManager) GetCompletedCollectionCount(ctx context.Context, characterID int32) (int, error) {
|
func (dcm *DatabaseCollectionManager) GetCompletedCollectionCount(ctx context.Context, characterID int32) (int, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ? AND completed = 1"
|
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ? AND completed = 1"
|
||||||
|
|
||||||
row, err := dcm.db.QueryRow(query, characterID)
|
var count int
|
||||||
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
Args: []any{characterID},
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
count = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to get completed collection count for character %d: %w", characterID, err)
|
return 0, fmt.Errorf("failed to get completed collection count for character %d: %w", characterID, err)
|
||||||
}
|
}
|
||||||
defer row.Close()
|
|
||||||
|
|
||||||
if row == nil {
|
return count, nil
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return row.Int(0), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePlayerCollection removes a player's collection progress
|
// DeletePlayerCollection removes a player's collection progress
|
||||||
func (dcm *DatabaseCollectionManager) DeletePlayerCollection(ctx context.Context, characterID, collectionID int32) error {
|
func (dcm *DatabaseCollectionManager) DeletePlayerCollection(ctx context.Context, characterID, collectionID int32) error {
|
||||||
// Use a transaction to ensure both tables are updated atomically
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
err := dcm.db.Transaction(func(db *database.DB) error {
|
|
||||||
// Delete collection items first due to foreign key constraint
|
|
||||||
err := db.Exec(
|
|
||||||
"DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?",
|
|
||||||
characterID, collectionID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete player collection items: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete collection
|
|
||||||
err = db.Exec(
|
|
||||||
"DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?",
|
|
||||||
characterID, collectionID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete player collection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("transaction failed: %w", err)
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
|
// Use a transaction to ensure both tables are updated atomically
|
||||||
|
err = sqlitex.Execute(conn, "BEGIN", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer sqlitex.Execute(conn, "ROLLBACK", nil)
|
||||||
|
|
||||||
|
// Delete collection items first due to foreign key constraint
|
||||||
|
err = sqlitex.Execute(conn, "DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?", &sqlitex.ExecOptions{
|
||||||
|
Args: []any{characterID, collectionID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete player collection items: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Delete collection
|
||||||
|
err = sqlitex.Execute(conn, "DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?", &sqlitex.ExecOptions{
|
||||||
|
Args: []any{characterID, collectionID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete player collection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqlitex.Execute(conn, "COMMIT", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCollectionStatistics returns database-level collection statistics
|
// GetCollectionStatistics returns database-level collection statistics
|
||||||
func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Context) (CollectionStatistics, error) {
|
func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Context) (CollectionStatistics, error) {
|
||||||
|
conn, err := dcm.pool.Take(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return CollectionStatistics{}, fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer dcm.pool.Put(conn)
|
||||||
|
|
||||||
var stats CollectionStatistics
|
var stats CollectionStatistics
|
||||||
|
|
||||||
// Total collections
|
// Total collections
|
||||||
row, err := dcm.db.QueryRow("SELECT COUNT(*) FROM collections")
|
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM collections", &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
stats.TotalCollections = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stats, fmt.Errorf("failed to get total collections: %w", err)
|
return stats, fmt.Errorf("failed to get total collections: %w", err)
|
||||||
}
|
}
|
||||||
if row != nil {
|
|
||||||
stats.TotalCollections = row.Int(0)
|
|
||||||
row.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Total collection items
|
// Total collection items
|
||||||
row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM collection_details")
|
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM collection_details", &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
stats.TotalItems = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stats, fmt.Errorf("failed to get total items: %w", err)
|
return stats, fmt.Errorf("failed to get total items: %w", err)
|
||||||
}
|
}
|
||||||
if row != nil {
|
|
||||||
stats.TotalItems = row.Int(0)
|
|
||||||
row.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Players with collections
|
// Players with collections
|
||||||
row, err = dcm.db.QueryRow("SELECT COUNT(DISTINCT char_id) FROM character_collections")
|
err = sqlitex.Execute(conn, "SELECT COUNT(DISTINCT char_id) FROM character_collections", &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
stats.PlayersWithCollections = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stats, fmt.Errorf("failed to get players with collections: %w", err)
|
return stats, fmt.Errorf("failed to get players with collections: %w", err)
|
||||||
}
|
}
|
||||||
if row != nil {
|
|
||||||
stats.PlayersWithCollections = row.Int(0)
|
|
||||||
row.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Completed collections across all players
|
// Completed collections across all players
|
||||||
row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM character_collections WHERE completed = 1")
|
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM character_collections WHERE completed = 1", &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
stats.CompletedCollections = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stats, fmt.Errorf("failed to get completed collections: %w", err)
|
return stats, fmt.Errorf("failed to get completed collections: %w", err)
|
||||||
}
|
}
|
||||||
if row != nil {
|
|
||||||
stats.CompletedCollections = row.Int(0)
|
|
||||||
row.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active collections (incomplete with at least one item found) across all players
|
// Active collections (incomplete with at least one item found) across all players
|
||||||
query := `SELECT COUNT(DISTINCT cc.char_id || '-' || cc.collection_id)
|
query := `SELECT COUNT(DISTINCT cc.char_id || '-' || cc.collection_id)
|
||||||
FROM character_collections cc
|
FROM character_collections cc
|
||||||
JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id
|
JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id
|
||||||
WHERE cc.completed = 0`
|
WHERE cc.completed = 0`
|
||||||
row, err = dcm.db.QueryRow(query)
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
stats.ActiveCollections = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stats, fmt.Errorf("failed to get active collections: %w", err)
|
return stats, fmt.Errorf("failed to get active collections: %w", err)
|
||||||
}
|
}
|
||||||
if row != nil {
|
|
||||||
stats.ActiveCollections = row.Int(0)
|
|
||||||
row.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Found items across all players
|
// Found items across all players
|
||||||
row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM character_collection_items")
|
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM character_collection_items", &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
stats.FoundItems = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stats, fmt.Errorf("failed to get found items: %w", err)
|
return stats, fmt.Errorf("failed to get found items: %w", err)
|
||||||
}
|
}
|
||||||
if row != nil {
|
|
||||||
stats.FoundItems = row.Int(0)
|
|
||||||
row.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Total rewards
|
// Total rewards
|
||||||
row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM collection_rewards")
|
err = sqlitex.Execute(conn, "SELECT COUNT(*) FROM collection_rewards", &sqlitex.ExecOptions{
|
||||||
|
ResultFunc: func(stmt *sqlite.Stmt) error {
|
||||||
|
stats.TotalRewards = int(stmt.ColumnInt64(0))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stats, fmt.Errorf("failed to get total rewards: %w", err)
|
return stats, fmt.Errorf("failed to get total rewards: %w", err)
|
||||||
}
|
}
|
||||||
if row != nil {
|
|
||||||
stats.TotalRewards = row.Int(0)
|
|
||||||
row.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user