Implement collections tests and fix database usage

This commit is contained in:
Sky Johnson 2025-08-01 18:56:47 -05:00
parent 9fd69fba38
commit f9fdef9466
2 changed files with 1395 additions and 174 deletions

File diff suppressed because it is too large Load Diff

View File

@ -23,31 +23,20 @@ func NewDatabaseCollectionManager(db *database.DB) *DatabaseCollectionManager {
func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]CollectionData, error) {
query := "SELECT `id`, `collection_name`, `collection_category`, `level` FROM `collections`"
rows, err := dcm.db.QueryContext(ctx, query)
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
})
if err != nil {
return nil, fmt.Errorf("failed to query collections: %w", err)
}
defer rows.Close()
var collections []CollectionData
for rows.Next() {
var collection CollectionData
err := rows.Scan(
&collection.ID,
&collection.Name,
&collection.Category,
&collection.Level,
)
if err != nil {
return nil, fmt.Errorf("failed to scan collection row: %w", err)
}
collections = append(collections, collection)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating collection rows: %w", err)
}
return collections, nil
}
@ -59,30 +48,19 @@ func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, c
WHERE collection_id = ?
ORDER BY item_index ASC`
rows, err := dcm.db.QueryContext(ctx, query, collectionID)
if err != nil {
return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err)
}
defer rows.Close()
var items []CollectionItem
for rows.Next() {
err := dcm.db.Query(query, func(row *database.Row) error {
var item CollectionItem
err := rows.Scan(
&item.ItemID,
&item.Index,
)
if err != nil {
return nil, fmt.Errorf("failed to scan collection item row: %w", err)
}
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)
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating collection item rows: %w", err)
if err != nil {
return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err)
}
return items, nil
@ -94,31 +72,20 @@ func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context,
FROM collection_rewards
WHERE collection_id = ?`
rows, err := dcm.db.QueryContext(ctx, query, collectionID)
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)
if err != nil {
return nil, fmt.Errorf("failed to query collection rewards for collection %d: %w", collectionID, err)
}
defer rows.Close()
var rewards []CollectionRewardData
for rows.Next() {
var reward CollectionRewardData
err := rows.Scan(
&reward.CollectionID,
&reward.RewardType,
&reward.RewardValue,
&reward.Quantity,
)
if err != nil {
return nil, fmt.Errorf("failed to scan collection reward row: %w", err)
}
rewards = append(rewards, reward)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating collection reward rows: %w", err)
}
return rewards, nil
}
@ -129,32 +96,19 @@ func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context,
FROM character_collections
WHERE char_id = ?`
rows, err := dcm.db.QueryContext(ctx, query, characterID)
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)
if err != nil {
return nil, fmt.Errorf("failed to query player collections for character %d: %w", characterID, err)
}
defer rows.Close()
var collections []PlayerCollectionData
for rows.Next() {
var collection PlayerCollectionData
var completed int
err := rows.Scan(
&collection.CharacterID,
&collection.CollectionID,
&completed,
)
if err != nil {
return nil, fmt.Errorf("failed to scan player collection row: %w", err)
}
collection.Completed = completed == 1
collections = append(collections, collection)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating player collection rows: %w", err)
}
return collections, nil
}
@ -165,26 +119,16 @@ func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Cont
FROM character_collection_items
WHERE char_id = ? AND collection_id = ?`
rows, err := dcm.db.QueryContext(ctx, query, characterID, collectionID)
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)
if err != nil {
return nil, fmt.Errorf("failed to query player collection items for character %d, collection %d: %w", characterID, collectionID, err)
}
defer rows.Close()
var itemIDs []int32
for rows.Next() {
var itemID int32
err := rows.Scan(&itemID)
if err != nil {
return nil, fmt.Errorf("failed to scan player collection item row: %w", err)
}
itemIDs = append(itemIDs, itemID)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating player collection item rows: %w", err)
}
return itemIDs, nil
}
@ -201,7 +145,7 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context,
ON CONFLICT(char_id, collection_id)
DO UPDATE SET completed = ?`
_, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, completedInt, completedInt)
err := dcm.db.Exec(query, characterID, collectionID, completedInt, completedInt)
if err != nil {
return fmt.Errorf("failed to save player collection for character %d, collection %d: %w", characterID, collectionID, err)
}
@ -214,7 +158,7 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollectionItem(ctx context.Conte
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
VALUES (?, ?, ?)`
_, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, itemID)
err := dcm.db.Exec(query, 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)
}
@ -229,37 +173,34 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollections(ctx context.Context,
}
// Use a transaction for atomic updates
tx, err := dcm.db.BeginTx(ctx, nil)
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 {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
for _, collection := range collections {
if !collection.GetSaveNeeded() {
continue
}
// Save collection completion status
if err := dcm.savePlayerCollectionTx(ctx, tx, characterID, collection); err != nil {
return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err)
}
// Save found items
if err := dcm.savePlayerCollectionItemsTx(ctx, tx, characterID, collection); err != nil {
return fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
return fmt.Errorf("transaction failed: %w", err)
}
return nil
}
// savePlayerCollectionTx saves a single collection within a transaction
func (dcm *DatabaseCollectionManager) savePlayerCollectionTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error {
// savePlayerCollectionInTx saves a single collection within a transaction
func (dcm *DatabaseCollectionManager) savePlayerCollectionInTx(db *database.DB, characterID int32, collection *Collection) error {
completedInt := 0
if collection.GetCompleted() {
completedInt = 1
@ -270,12 +211,12 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionTx(ctx context.Context
ON CONFLICT(char_id, collection_id)
DO UPDATE SET completed = ?`
_, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), completedInt, completedInt)
err := db.Exec(query, characterID, collection.GetID(), completedInt, completedInt)
return err
}
// savePlayerCollectionItemsTx saves collection items within a transaction
func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error {
// savePlayerCollectionItemsInTx saves collection items within a transaction
func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsInTx(db *database.DB, characterID int32, collection *Collection) error {
items := collection.GetCollectionItems()
for _, item := range items {
@ -283,7 +224,7 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsTx(ctx context.Co
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
VALUES (?, ?, ?)`
_, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), item.ItemID)
err := db.Exec(query, characterID, collection.GetID(), item.ItemID)
if err != nil {
return fmt.Errorf("failed to save item %d: %w", item.ItemID, err)
}
@ -339,7 +280,7 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context
}
for i, query := range queries {
_, err := dcm.db.ExecContext(ctx, query)
err := dcm.db.Exec(query)
if err != nil {
return fmt.Errorf("failed to create collection table %d: %w", i+1, err)
}
@ -357,7 +298,7 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context
}
for i, query := range indexes {
_, err := dcm.db.ExecContext(ctx, query)
err := dcm.db.Exec(query)
if err != nil {
return fmt.Errorf("failed to create collection index %d: %w", i+1, err)
}
@ -370,68 +311,78 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context
func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (int, error) {
query := "SELECT COUNT(*) FROM collections"
var count int
err := dcm.db.QueryRowContext(ctx, query).Scan(&count)
row, err := dcm.db.QueryRow(query)
if err != nil {
return 0, fmt.Errorf("failed to get collection count: %w", err)
}
defer row.Close()
return count, nil
if row == nil {
return 0, nil
}
return row.Int(0), nil
}
// GetPlayerCollectionCount returns the number of collections a player has
func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Context, characterID int32) (int, error) {
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ?"
var count int
err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count)
row, err := dcm.db.QueryRow(query, characterID)
if err != nil {
return 0, fmt.Errorf("failed to get player collection count for character %d: %w", characterID, err)
}
defer row.Close()
return count, nil
if row == nil {
return 0, nil
}
return row.Int(0), nil
}
// GetCompletedCollectionCount returns the number of completed collections for a player
func (dcm *DatabaseCollectionManager) GetCompletedCollectionCount(ctx context.Context, characterID int32) (int, error) {
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ? AND completed = 1"
var count int
err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count)
row, err := dcm.db.QueryRow(query, characterID)
if err != nil {
return 0, fmt.Errorf("failed to get completed collection count for character %d: %w", characterID, err)
}
defer row.Close()
return count, nil
if row == nil {
return 0, nil
}
return row.Int(0), 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
tx, err := dcm.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
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 items first due to foreign key constraint
_, err = tx.ExecContext(ctx,
"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)
}
// Delete collection
_, err = tx.ExecContext(ctx,
"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 := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
if err != nil {
return fmt.Errorf("transaction failed: %w", err)
}
return nil
@ -442,50 +393,78 @@ func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Contex
var stats CollectionStatistics
// Total collections
err := dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collections").Scan(&stats.TotalCollections)
row, err := dcm.db.QueryRow("SELECT COUNT(*) FROM collections")
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
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collection_details").Scan(&stats.TotalItems)
row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM collection_details")
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
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(DISTINCT char_id) FROM character_collections").Scan(&stats.PlayersWithCollections)
row, err = dcm.db.QueryRow("SELECT COUNT(DISTINCT char_id) FROM character_collections")
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
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM character_collections WHERE completed = 1").Scan(&stats.CompletedCollections)
row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM character_collections WHERE completed = 1")
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)
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`
err = dcm.db.QueryRowContext(ctx, query).Scan(&stats.ActiveCollections)
row, err = dcm.db.QueryRow(query)
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
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM character_collection_items").Scan(&stats.FoundItems)
row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM character_collection_items")
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
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collection_rewards").Scan(&stats.TotalRewards)
row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM collection_rewards")
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
}