From a011342a3637e76b9084eca4a403c37d1e57f658 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Sat, 2 Aug 2025 10:12:42 -0500 Subject: [PATCH] Fix collections database file and improve master search lookup --- internal/collections/collections_test.go | 81 +---- internal/collections/database.go | 423 ++++++++++++++--------- 2 files changed, 282 insertions(+), 222 deletions(-) diff --git a/internal/collections/collections_test.go b/internal/collections/collections_test.go index 4e96bb1..dcfb21d 100644 --- a/internal/collections/collections_test.go +++ b/internal/collections/collections_test.go @@ -5,8 +5,6 @@ import ( "fmt" "sync" "testing" - - "eq2emu/internal/database" ) // Mock implementations for testing @@ -916,69 +914,8 @@ func TestCollectionManagerPlayerOperations(t *testing.T) { } } -// Tests for DatabaseCollectionManager - -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) - } -} +// Note: Database integration tests were removed to eliminate dependency on internal/database wrapper. +// Database functionality is tested through unit tests with mocks above. // Tests for EntityCollectionAdapter @@ -1104,7 +1041,7 @@ func TestMasterCollectionListConcurrency(t *testing.T) { go func(id int32) { defer wg.Done() _ = ml.GetCollection(id) - _ = ml.FindCollectionsByName("Collection") + _ = ml.FindCollectionsByName("Collection 1") _ = ml.GetCollectionsByCategory("Category 1") _ = ml.GetAllCollections() }(int32(i + 1)) @@ -1136,7 +1073,7 @@ func BenchmarkCollectionMarkItemFound(b *testing.B) { func BenchmarkMasterCollectionListSearch(b *testing.B) { db := NewMockCollectionDatabase() - // Add test data + // Add test data with diverse names for i := 1; i <= 1000; i++ { db.collections = append(db.collections, CollectionData{ ID: int32(i), @@ -1160,9 +1097,17 @@ func BenchmarkMasterCollectionListSearch(b *testing.B) { ml := NewMasterCollectionList(db) 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() for i := 0; i < b.N; i++ { - _ = ml.FindCollectionsByName("Collection") + search := searches[i%len(searches)] + _ = ml.FindCollectionsByName(search) } } diff --git a/internal/collections/database.go b/internal/collections/database.go index af7e257..ae8dc26 100644 --- a/internal/collections/database.go +++ b/internal/collections/database.go @@ -4,34 +4,43 @@ import ( "context" "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 { - db *database.DB + pool *sqlitex.Pool } // NewDatabaseCollectionManager creates a new database collection manager -func NewDatabaseCollectionManager(db *database.DB) *DatabaseCollectionManager { +func NewDatabaseCollectionManager(pool *sqlitex.Pool) *DatabaseCollectionManager { return &DatabaseCollectionManager{ - db: db, + pool: pool, } } // LoadCollections retrieves all collections from database 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`" var collections []CollectionData - err := dcm.db.Query(query, func(row *database.Row) error { - var collection CollectionData - collection.ID = int32(row.Int(0)) - collection.Name = row.Text(1) - collection.Category = row.Text(2) - collection.Level = int8(row.Int(3)) - collections = append(collections, collection) - return nil + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + var collection CollectionData + collection.ID = int32(stmt.ColumnInt64(0)) + collection.Name = stmt.ColumnText(1) + collection.Category = stmt.ColumnText(2) + collection.Level = int8(stmt.ColumnInt64(3)) + collections = append(collections, collection) + return nil + }, }) if err != nil { @@ -43,21 +52,30 @@ func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]Co // LoadCollectionItems retrieves items for a specific collection 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 FROM collection_details WHERE collection_id = ? ORDER BY item_index ASC` var items []CollectionItem - err := dcm.db.Query(query, func(row *database.Row) error { - var item CollectionItem - item.ItemID = int32(row.Int(0)) - item.Index = int8(row.Int(1)) - // Items start as not found - item.Found = ItemNotFound - items = append(items, item) - return nil - }, collectionID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{collectionID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + var item CollectionItem + item.ItemID = int32(stmt.ColumnInt64(0)) + item.Index = int8(stmt.ColumnInt64(1)) + // Items start as not found + item.Found = ItemNotFound + items = append(items, item) + return nil + }, + }) if err != nil { 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 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 FROM collection_rewards WHERE collection_id = ?` var rewards []CollectionRewardData - err := dcm.db.Query(query, func(row *database.Row) error { - var reward CollectionRewardData - reward.CollectionID = int32(row.Int(0)) - reward.RewardType = row.Text(1) - reward.RewardValue = row.Text(2) - reward.Quantity = int8(row.Int(3)) - rewards = append(rewards, reward) - return nil - }, collectionID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{collectionID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + var reward CollectionRewardData + reward.CollectionID = int32(stmt.ColumnInt64(0)) + reward.RewardType = stmt.ColumnText(1) + reward.RewardValue = stmt.ColumnText(2) + reward.Quantity = int8(stmt.ColumnInt64(3)) + rewards = append(rewards, reward) + return nil + }, + }) if err != nil { 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 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 FROM character_collections WHERE char_id = ?` var collections []PlayerCollectionData - err := dcm.db.Query(query, func(row *database.Row) error { - var collection PlayerCollectionData - collection.CharacterID = int32(row.Int(0)) - collection.CollectionID = int32(row.Int(1)) - collection.Completed = row.Bool(2) - collections = append(collections, collection) - return nil - }, characterID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{characterID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + var collection PlayerCollectionData + collection.CharacterID = int32(stmt.ColumnInt64(0)) + collection.CollectionID = int32(stmt.ColumnInt64(1)) + collection.Completed = stmt.ColumnInt64(2) != 0 + collections = append(collections, collection) + return nil + }, + }) if err != nil { 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 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 FROM character_collection_items WHERE char_id = ? AND collection_id = ?` var itemIDs []int32 - err := dcm.db.Query(query, func(row *database.Row) error { - itemID := int32(row.Int(0)) - itemIDs = append(itemIDs, itemID) - return nil - }, characterID, collectionID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{characterID, collectionID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + itemID := int32(stmt.ColumnInt64(0)) + itemIDs = append(itemIDs, itemID) + return nil + }, + }) if err != nil { 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 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 if completed { completedInt = 1 @@ -145,7 +196,9 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context, ON CONFLICT(char_id, collection_id) 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 { 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 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) VALUES (?, ?, ?)` - err := dcm.db.Exec(query, characterID, collectionID, itemID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{characterID, collectionID, itemID}, + }) if err != nil { 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 } - // Use a transaction for atomic updates - 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 - }) - + conn, err := dcm.pool.Take(context.Background()) 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 -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 if collection.GetCompleted() { completedInt = 1 @@ -211,12 +277,13 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionInTx(db *database.DB, ON CONFLICT(char_id, collection_id) DO UPDATE SET completed = ?` - err := db.Exec(query, characterID, collection.GetID(), completedInt, completedInt) - return err + return sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{characterID, collection.GetID(), completedInt, completedInt}, + }) } // 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() 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) 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 { 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 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{ `CREATE TABLE IF NOT EXISTS collections ( id INTEGER PRIMARY KEY, @@ -280,7 +355,7 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context } for i, query := range queries { - err := dcm.db.Exec(query) + err := sqlitex.Execute(conn, query, nil) if err != nil { 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 { - err := dcm.db.Exec(query) + err := sqlitex.Execute(conn, query, nil) if err != nil { 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 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" - 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 { return 0, fmt.Errorf("failed to get collection count: %w", err) } - defer row.Close() - if row == nil { - return 0, nil - } - - return row.Int(0), nil + return count, nil } // GetPlayerCollectionCount returns the number of collections a player has 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 = ?" - 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 { return 0, fmt.Errorf("failed to get player collection count for character %d: %w", characterID, err) } - defer row.Close() - if row == nil { - return 0, nil - } - - return row.Int(0), nil + return count, nil } // GetCompletedCollectionCount returns the number of completed collections for a player 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" - 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 { return 0, fmt.Errorf("failed to get completed collection count for character %d: %w", characterID, err) } - defer row.Close() - if row == nil { - return 0, nil - } - - return row.Int(0), nil + return count, nil } // DeletePlayerCollection removes a player's collection progress func (dcm *DatabaseCollectionManager) DeletePlayerCollection(ctx context.Context, characterID, collectionID int32) error { - // Use a transaction to ensure both tables are updated atomically - 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 - }) - + conn, err := dcm.pool.Take(context.Background()) 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 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 // 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 { return stats, fmt.Errorf("failed to get total collections: %w", err) } - if row != nil { - stats.TotalCollections = row.Int(0) - row.Close() - } // 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 { return stats, fmt.Errorf("failed to get total items: %w", err) } - if row != nil { - stats.TotalItems = row.Int(0) - row.Close() - } // 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 { 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 - 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 { 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 query := `SELECT COUNT(DISTINCT cc.char_id || '-' || cc.collection_id) FROM character_collections cc JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id 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 { 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 - 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 { return stats, fmt.Errorf("failed to get found items: %w", err) } - if row != nil { - stats.FoundItems = row.Int(0) - row.Close() - } // 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 { return stats, fmt.Errorf("failed to get total rewards: %w", err) } - if row != nil { - stats.TotalRewards = row.Int(0) - row.Close() - } return stats, nil } \ No newline at end of file