Fix collections database file and improve master search lookup

This commit is contained in:
Sky Johnson 2025-08-02 10:12:42 -05:00
parent 674b14f278
commit a011342a36
2 changed files with 282 additions and 222 deletions

View File

@ -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)
} }
} }

View File

@ -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{
ResultFunc: func(stmt *sqlite.Stmt) error {
var collection CollectionData var collection CollectionData
collection.ID = int32(row.Int(0)) collection.ID = int32(stmt.ColumnInt64(0))
collection.Name = row.Text(1) collection.Name = stmt.ColumnText(1)
collection.Category = row.Text(2) collection.Category = stmt.ColumnText(2)
collection.Level = int8(row.Int(3)) collection.Level = int8(stmt.ColumnInt64(3))
collections = append(collections, collection) collections = append(collections, collection)
return nil 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{
Args: []any{collectionID},
ResultFunc: func(stmt *sqlite.Stmt) error {
var item CollectionItem var item CollectionItem
item.ItemID = int32(row.Int(0)) item.ItemID = int32(stmt.ColumnInt64(0))
item.Index = int8(row.Int(1)) item.Index = int8(stmt.ColumnInt64(1))
// Items start as not found // Items start as not found
item.Found = ItemNotFound item.Found = ItemNotFound
items = append(items, item) items = append(items, item)
return nil return nil
}, collectionID) },
})
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{
Args: []any{collectionID},
ResultFunc: func(stmt *sqlite.Stmt) error {
var reward CollectionRewardData var reward CollectionRewardData
reward.CollectionID = int32(row.Int(0)) reward.CollectionID = int32(stmt.ColumnInt64(0))
reward.RewardType = row.Text(1) reward.RewardType = stmt.ColumnText(1)
reward.RewardValue = row.Text(2) reward.RewardValue = stmt.ColumnText(2)
reward.Quantity = int8(row.Int(3)) reward.Quantity = int8(stmt.ColumnInt64(3))
rewards = append(rewards, reward) rewards = append(rewards, reward)
return nil return nil
}, collectionID) },
})
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{
Args: []any{characterID},
ResultFunc: func(stmt *sqlite.Stmt) error {
var collection PlayerCollectionData var collection PlayerCollectionData
collection.CharacterID = int32(row.Int(0)) collection.CharacterID = int32(stmt.ColumnInt64(0))
collection.CollectionID = int32(row.Int(1)) collection.CollectionID = int32(stmt.ColumnInt64(1))
collection.Completed = row.Bool(2) collection.Completed = stmt.ColumnInt64(2) != 0
collections = append(collections, collection) collections = append(collections, collection)
return nil return nil
}, characterID) },
})
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},
ResultFunc: func(stmt *sqlite.Stmt) error {
itemID := int32(stmt.ColumnInt64(0))
itemIDs = append(itemIDs, itemID) itemIDs = append(itemIDs, itemID)
return nil return nil
}, characterID, collectionID) },
})
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
} }
conn, err := dcm.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer dcm.pool.Put(conn)
// Use a transaction for atomic updates // Use a transaction for atomic updates
err := dcm.db.Transaction(func(db *database.DB) error { 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 { for _, collection := range collections {
if !collection.GetSaveNeeded() { if !collection.GetSaveNeeded() {
continue continue
} }
// Save collection completion status // Save collection completion status
if err := dcm.savePlayerCollectionInTx(db, characterID, collection); err != nil { if err := dcm.savePlayerCollectionInTx(conn, characterID, collection); err != nil {
return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err) return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err)
} }
// Save found items // Save found items
if err := dcm.savePlayerCollectionItemsInTx(db, characterID, collection); err != nil { 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 fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err)
} }
} }
return nil
})
if err != nil { return sqlitex.Execute(conn, "COMMIT", nil)
return fmt.Errorf("transaction failed: %w", err)
}
return 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 {
conn, err := dcm.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer dcm.pool.Put(conn)
// Use a transaction to ensure both tables are updated atomically // Use a transaction to ensure both tables are updated atomically
err := dcm.db.Transaction(func(db *database.DB) error { 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 // Delete collection items first due to foreign key constraint
err := db.Exec( err = sqlitex.Execute(conn, "DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?", &sqlitex.ExecOptions{
"DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?", Args: []any{characterID, collectionID},
characterID, collectionID) })
if err != nil { if err != nil {
return fmt.Errorf("failed to delete player collection items: %w", err) return fmt.Errorf("failed to delete player collection items: %w", err)
} }
// Delete collection // Delete collection
err = db.Exec( err = sqlitex.Execute(conn, "DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?", &sqlitex.ExecOptions{
"DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?", Args: []any{characterID, collectionID},
characterID, collectionID) })
if err != nil { if err != nil {
return fmt.Errorf("failed to delete player collection: %w", err) return fmt.Errorf("failed to delete player collection: %w", err)
} }
return nil return sqlitex.Execute(conn, "COMMIT", nil)
})
if err != nil {
return fmt.Errorf("transaction failed: %w", err)
}
return 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
} }