From 966501670f875d7d845f56524962589e46613a7f Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Sat, 2 Aug 2025 13:21:17 -0500 Subject: [PATCH] fix guild package database usage --- internal/guilds/database.go | 1198 +++++++++++++++++------------- internal/guilds/database_test.go | 580 +++++---------- 2 files changed, 886 insertions(+), 892 deletions(-) diff --git a/internal/guilds/database.go b/internal/guilds/database.go index e992a86..13cd890 100644 --- a/internal/guilds/database.go +++ b/internal/guilds/database.go @@ -5,44 +5,55 @@ import ( "fmt" "time" - "eq2emu/internal/database" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" ) -// DatabaseGuildManager implements GuildDatabase interface using the existing database wrapper +// DatabaseGuildManager implements GuildDatabase interface using zombiezen/go-sqlite type DatabaseGuildManager struct { - db database.DBInterface + pool *sqlitex.Pool } // NewDatabaseGuildManager creates a new database guild manager -func NewDatabaseGuildManager(db database.DBInterface) *DatabaseGuildManager { +func NewDatabaseGuildManager(pool *sqlitex.Pool) *DatabaseGuildManager { return &DatabaseGuildManager{ - db: db, + pool: pool, } } // LoadGuilds retrieves all guilds from database func (dgm *DatabaseGuildManager) LoadGuilds(ctx context.Context) ([]GuildData, error) { - query := "SELECT `id`, `name`, `motd`, `level`, `xp`, `xp_needed`, `formed_on` FROM `guilds`" + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep("SELECT `id`, `name`, `motd`, `level`, `xp`, `xp_needed`, `formed_on` FROM `guilds`") + defer stmt.Finalize() var guilds []GuildData - err := dgm.db.Query(query, func(row *database.Row) error { - var guild GuildData - guild.ID = int32(row.Int(0)) - guild.Name = row.Text(1) - if !row.IsNull(2) { - guild.MOTD = row.Text(2) + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("failed to query guilds: %w", err) } - guild.Level = int8(row.Int(3)) - guild.EXPCurrent = row.Int64(4) - guild.EXPToNextLevel = row.Int64(5) - guild.FormedDate = time.Unix(row.Int64(6), 0) + if !hasRow { + break + } + + var guild GuildData + guild.ID = int32(stmt.ColumnInt64(0)) + guild.Name = stmt.ColumnText(1) + if stmt.ColumnType(2) != sqlite.TypeNull { + guild.MOTD = stmt.ColumnText(2) + } + guild.Level = int8(stmt.ColumnInt64(3)) + guild.EXPCurrent = stmt.ColumnInt64(4) + guild.EXPToNextLevel = stmt.ColumnInt64(5) + guild.FormedDate = time.Unix(stmt.ColumnInt64(6), 0) guilds = append(guilds, guild) - return nil - }) - - if err != nil { - return nil, fmt.Errorf("failed to query guilds: %w", err) } return guilds, nil @@ -50,105 +61,139 @@ func (dgm *DatabaseGuildManager) LoadGuilds(ctx context.Context) ([]GuildData, e // LoadGuild retrieves a specific guild from database func (dgm *DatabaseGuildManager) LoadGuild(ctx context.Context, guildID int32) (*GuildData, error) { - query := "SELECT `id`, `name`, `motd`, `level`, `xp`, `xp_needed`, `formed_on` FROM `guilds` WHERE `id` = ?" + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) - row, err := dgm.db.QueryRow(query, guildID) + stmt := conn.Prep("SELECT `id`, `name`, `motd`, `level`, `xp`, `xp_needed`, `formed_on` FROM `guilds` WHERE `id` = ?") + defer stmt.Finalize() + stmt.BindInt64(1, int64(guildID)) + + hasRow, err := stmt.Step() if err != nil { return nil, fmt.Errorf("failed to load guild %d: %w", guildID, err) } - if row == nil { + if !hasRow { return nil, fmt.Errorf("guild %d not found", guildID) } - defer row.Close() var guild GuildData - guild.ID = int32(row.Int(0)) - guild.Name = row.Text(1) - if !row.IsNull(2) { - guild.MOTD = row.Text(2) + guild.ID = int32(stmt.ColumnInt64(0)) + guild.Name = stmt.ColumnText(1) + if stmt.ColumnType(2) != sqlite.TypeNull { + guild.MOTD = stmt.ColumnText(2) } - guild.Level = int8(row.Int(3)) - guild.EXPCurrent = row.Int64(4) - guild.EXPToNextLevel = row.Int64(5) - guild.FormedDate = time.Unix(row.Int64(6), 0) + guild.Level = int8(stmt.ColumnInt64(3)) + guild.EXPCurrent = stmt.ColumnInt64(4) + guild.EXPToNextLevel = stmt.ColumnInt64(5) + guild.FormedDate = time.Unix(stmt.ColumnInt64(6), 0) return &guild, nil } // LoadGuildMembers retrieves all members for a guild func (dgm *DatabaseGuildManager) LoadGuildMembers(ctx context.Context, guildID int32) ([]GuildMemberData, error) { - query := `SELECT char_id, guild_id, account_id, recruiter_id, name, guild_status, points, - adventure_class, adventure_level, tradeskill_class, tradeskill_level, rank, - member_flags, zone, join_date, last_login_date, note, officer_note, - recruiter_description, recruiter_picture_data, recruiting_show_adventure_class - FROM guild_members WHERE guild_id = ?` + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep(`SELECT char_id, guild_id, account_id, recruiter_id, name, guild_status, points, + adventure_class, adventure_level, tradeskill_class, tradeskill_level, rank, + member_flags, zone, join_date, last_login_date, note, officer_note, + recruiter_description, recruiter_picture_data, recruiting_show_adventure_class + FROM guild_members WHERE guild_id = ?`) + defer stmt.Finalize() + stmt.BindInt64(1, int64(guildID)) - // Use the main implementation with zero-copy optimization var members []GuildMemberData - err := dgm.db.Query(query, func(row *database.Row) error { + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("failed to query guild members for guild %d: %w", guildID, err) + } + if !hasRow { + break + } + var member GuildMemberData - member.CharacterID = int32(row.Int(0)) - member.GuildID = int32(row.Int(1)) - member.AccountID = int32(row.Int(2)) - member.RecruiterID = int32(row.Int(3)) - member.Name = row.Text(4) - member.GuildStatus = int32(row.Int(5)) - member.Points = row.Float(6) - member.AdventureClass = int8(row.Int(7)) - member.AdventureLevel = int8(row.Int(8)) - member.TradeskillClass = int8(row.Int(9)) - member.TradeskillLevel = int8(row.Int(10)) - member.Rank = int8(row.Int(11)) - member.MemberFlags = int8(row.Int(12)) - member.Zone = row.Text(13) - member.JoinDate = time.Unix(row.Int64(14), 0) - member.LastLoginDate = time.Unix(row.Int64(15), 0) - if !row.IsNull(16) { - member.Note = row.Text(16) + member.CharacterID = int32(stmt.ColumnInt64(0)) + member.GuildID = int32(stmt.ColumnInt64(1)) + member.AccountID = int32(stmt.ColumnInt64(2)) + member.RecruiterID = int32(stmt.ColumnInt64(3)) + member.Name = stmt.ColumnText(4) + member.GuildStatus = int32(stmt.ColumnInt64(5)) + member.Points = stmt.ColumnFloat(6) + member.AdventureClass = int8(stmt.ColumnInt64(7)) + member.AdventureLevel = int8(stmt.ColumnInt64(8)) + member.TradeskillClass = int8(stmt.ColumnInt64(9)) + member.TradeskillLevel = int8(stmt.ColumnInt64(10)) + member.Rank = int8(stmt.ColumnInt64(11)) + member.MemberFlags = int8(stmt.ColumnInt64(12)) + member.Zone = stmt.ColumnText(13) + member.JoinDate = time.Unix(stmt.ColumnInt64(14), 0) + member.LastLoginDate = time.Unix(stmt.ColumnInt64(15), 0) + if stmt.ColumnType(16) != sqlite.TypeNull { + member.Note = stmt.ColumnText(16) } - if !row.IsNull(17) { - member.OfficerNote = row.Text(17) + if stmt.ColumnType(17) != sqlite.TypeNull { + member.OfficerNote = stmt.ColumnText(17) } - if !row.IsNull(18) { - member.RecruiterDescription = row.Text(18) + if stmt.ColumnType(18) != sqlite.TypeNull { + member.RecruiterDescription = stmt.ColumnText(18) } - // TODO: Handle blob data for recruiter_picture_data (column 19) - member.RecruitingShowAdventureClass = int8(row.Int(20)) + if stmt.ColumnType(19) != sqlite.TypeNull { + len := stmt.ColumnLen(19) + if len > 0 { + member.RecruiterPictureData = make([]byte, len) + stmt.ColumnBytes(19, member.RecruiterPictureData) + } + } + member.RecruitingShowAdventureClass = int8(stmt.ColumnInt64(20)) members = append(members, member) - return nil - }, guildID) - - if err != nil { - return nil, fmt.Errorf("failed to query guild members for guild %d: %w", guildID, err) } return members, nil } - // LoadGuildEvents retrieves events for a guild func (dgm *DatabaseGuildManager) LoadGuildEvents(ctx context.Context, guildID int32) ([]GuildEventData, error) { - query := `SELECT event_id, guild_id, date, type, description, locked - FROM guild_events WHERE guild_id = ? - ORDER BY event_id DESC LIMIT ?` + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep(`SELECT event_id, guild_id, date, type, description, locked + FROM guild_events WHERE guild_id = ? + ORDER BY event_id DESC LIMIT ?`) + defer stmt.Finalize() + stmt.BindInt64(1, int64(guildID)) + stmt.BindInt64(2, MaxEvents) var events []GuildEventData - err := dgm.db.Query(query, func(row *database.Row) error { + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("failed to query guild events for guild %d: %w", guildID, err) + } + if !hasRow { + break + } + var event GuildEventData - event.EventID = row.Int64(0) - event.GuildID = int32(row.Int(1)) - event.Date = time.Unix(row.Int64(2), 0) - event.Type = int32(row.Int(3)) - event.Description = row.Text(4) - event.Locked = int8(row.Int(5)) + event.EventID = stmt.ColumnInt64(0) + event.GuildID = int32(stmt.ColumnInt64(1)) + event.Date = time.Unix(stmt.ColumnInt64(2), 0) + event.Type = int32(stmt.ColumnInt64(3)) + event.Description = stmt.ColumnText(4) + event.Locked = int8(stmt.ColumnInt64(5)) events = append(events, event) - return nil - }, guildID, MaxEvents) - - if err != nil { - return nil, fmt.Errorf("failed to query guild events for guild %d: %w", guildID, err) } return events, nil @@ -156,21 +201,32 @@ func (dgm *DatabaseGuildManager) LoadGuildEvents(ctx context.Context, guildID in // LoadGuildRanks retrieves custom rank names for a guild func (dgm *DatabaseGuildManager) LoadGuildRanks(ctx context.Context, guildID int32) ([]GuildRankData, error) { - query := "SELECT guild_id, rank, name FROM guild_ranks WHERE guild_id = ?" + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep("SELECT guild_id, rank, name FROM guild_ranks WHERE guild_id = ?") + defer stmt.Finalize() + stmt.BindInt64(1, int64(guildID)) var ranks []GuildRankData - err := dgm.db.Query(query, func(row *database.Row) error { + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("failed to query guild ranks for guild %d: %w", guildID, err) + } + if !hasRow { + break + } + var rank GuildRankData - rank.GuildID = int32(row.Int(0)) - rank.Rank = int8(row.Int(1)) - rank.Name = row.Text(2) + rank.GuildID = int32(stmt.ColumnInt64(0)) + rank.Rank = int8(stmt.ColumnInt64(1)) + rank.Name = stmt.ColumnText(2) ranks = append(ranks, rank) - return nil - }, guildID) - - if err != nil { - return nil, fmt.Errorf("failed to query guild ranks for guild %d: %w", guildID, err) } return ranks, nil @@ -178,22 +234,33 @@ func (dgm *DatabaseGuildManager) LoadGuildRanks(ctx context.Context, guildID int // LoadGuildPermissions retrieves permissions for a guild func (dgm *DatabaseGuildManager) LoadGuildPermissions(ctx context.Context, guildID int32) ([]GuildPermissionData, error) { - query := "SELECT guild_id, rank, permission, value FROM guild_permissions WHERE guild_id = ?" + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep("SELECT guild_id, rank, permission, value FROM guild_permissions WHERE guild_id = ?") + defer stmt.Finalize() + stmt.BindInt64(1, int64(guildID)) var permissions []GuildPermissionData - err := dgm.db.Query(query, func(row *database.Row) error { + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("failed to query guild permissions for guild %d: %w", guildID, err) + } + if !hasRow { + break + } + var permission GuildPermissionData - permission.GuildID = int32(row.Int(0)) - permission.Rank = int8(row.Int(1)) - permission.Permission = int8(row.Int(2)) - permission.Value = int8(row.Int(3)) + permission.GuildID = int32(stmt.ColumnInt64(0)) + permission.Rank = int8(stmt.ColumnInt64(1)) + permission.Permission = int8(stmt.ColumnInt64(2)) + permission.Value = int8(stmt.ColumnInt64(3)) permissions = append(permissions, permission) - return nil - }, guildID) - - if err != nil { - return nil, fmt.Errorf("failed to query guild permissions for guild %d: %w", guildID, err) } return permissions, nil @@ -201,22 +268,33 @@ func (dgm *DatabaseGuildManager) LoadGuildPermissions(ctx context.Context, guild // LoadGuildEventFilters retrieves event filters for a guild func (dgm *DatabaseGuildManager) LoadGuildEventFilters(ctx context.Context, guildID int32) ([]GuildEventFilterData, error) { - query := "SELECT guild_id, event_id, category, value FROM guild_event_filters WHERE guild_id = ?" + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep("SELECT guild_id, event_id, category, value FROM guild_event_filters WHERE guild_id = ?") + defer stmt.Finalize() + stmt.BindInt64(1, int64(guildID)) var filters []GuildEventFilterData - err := dgm.db.Query(query, func(row *database.Row) error { + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("failed to query guild event filters for guild %d: %w", guildID, err) + } + if !hasRow { + break + } + var filter GuildEventFilterData - filter.GuildID = int32(row.Int(0)) - filter.EventID = int8(row.Int(1)) - filter.Category = int8(row.Int(2)) - filter.Value = int8(row.Int(3)) + filter.GuildID = int32(stmt.ColumnInt64(0)) + filter.EventID = int8(stmt.ColumnInt64(1)) + filter.Category = int8(stmt.ColumnInt64(2)) + filter.Value = int8(stmt.ColumnInt64(3)) filters = append(filters, filter) - return nil - }, guildID) - - if err != nil { - return nil, fmt.Errorf("failed to query guild event filters for guild %d: %w", guildID, err) } return filters, nil @@ -224,21 +302,32 @@ func (dgm *DatabaseGuildManager) LoadGuildEventFilters(ctx context.Context, guil // LoadGuildRecruiting retrieves recruiting settings for a guild func (dgm *DatabaseGuildManager) LoadGuildRecruiting(ctx context.Context, guildID int32) ([]GuildRecruitingData, error) { - query := "SELECT guild_id, flag, value FROM guild_recruiting WHERE guild_id = ?" + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep("SELECT guild_id, flag, value FROM guild_recruiting WHERE guild_id = ?") + defer stmt.Finalize() + stmt.BindInt64(1, int64(guildID)) var recruiting []GuildRecruitingData - err := dgm.db.Query(query, func(row *database.Row) error { + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("failed to query guild recruiting for guild %d: %w", guildID, err) + } + if !hasRow { + break + } + var recruit GuildRecruitingData - recruit.GuildID = int32(row.Int(0)) - recruit.Flag = int8(row.Int(1)) - recruit.Value = int8(row.Int(2)) + recruit.GuildID = int32(stmt.ColumnInt64(0)) + recruit.Flag = int8(stmt.ColumnInt64(1)) + recruit.Value = int8(stmt.ColumnInt64(2)) recruiting = append(recruiting, recruit) - return nil - }, guildID) - - if err != nil { - return nil, fmt.Errorf("failed to query guild recruiting for guild %d: %w", guildID, err) } return recruiting, nil @@ -246,25 +335,37 @@ func (dgm *DatabaseGuildManager) LoadGuildRecruiting(ctx context.Context, guildI // LoadPointHistory retrieves point history for a member func (dgm *DatabaseGuildManager) LoadPointHistory(ctx context.Context, characterID int32) ([]PointHistoryData, error) { - query := `SELECT char_id, date, modified_by, comment, points - FROM guild_point_history WHERE char_id = ? - ORDER BY date DESC LIMIT ?` + conn, err := dgm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep(`SELECT char_id, date, modified_by, comment, points + FROM guild_point_history WHERE char_id = ? + ORDER BY date DESC LIMIT ?`) + defer stmt.Finalize() + stmt.BindInt64(1, int64(characterID)) + stmt.BindInt64(2, MaxPointHistory) var history []PointHistoryData - err := dgm.db.Query(query, func(row *database.Row) error { + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, fmt.Errorf("failed to query point history for character %d: %w", characterID, err) + } + if !hasRow { + break + } + var entry PointHistoryData - entry.CharacterID = int32(row.Int(0)) - entry.Date = time.Unix(row.Int64(1), 0) - entry.ModifiedBy = row.Text(2) - entry.Comment = row.Text(3) - entry.Points = row.Float(4) + entry.CharacterID = int32(stmt.ColumnInt64(0)) + entry.Date = time.Unix(stmt.ColumnInt64(1), 0) + entry.ModifiedBy = stmt.ColumnText(2) + entry.Comment = stmt.ColumnText(3) + entry.Points = stmt.ColumnFloat(4) history = append(history, entry) - return nil - }, characterID, MaxPointHistory) - - if err != nil { - return nil, fmt.Errorf("failed to query point history for character %d: %w", characterID, err) } return history, nil @@ -272,21 +373,28 @@ func (dgm *DatabaseGuildManager) LoadPointHistory(ctx context.Context, character // SaveGuild saves guild basic data func (dgm *DatabaseGuildManager) SaveGuild(ctx context.Context, guild *Guild) error { - query := `INSERT OR REPLACE INTO guilds - (id, name, motd, level, xp, xp_needed, formed_on) - VALUES (?, ?, ?, ?, ?, ?, ?)` + conn, err := dgm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep(`INSERT OR REPLACE INTO guilds + (id, name, motd, level, xp, xp_needed, formed_on) + VALUES (?, ?, ?, ?, ?, ?, ?)`) + defer stmt.Finalize() formedTimestamp := guild.GetFormedDate().Unix() - err := dgm.db.Exec(query, - guild.GetID(), - guild.GetName(), - guild.GetMOTD(), - guild.GetLevel(), - guild.GetEXPCurrent(), - guild.GetEXPToNextLevel(), - formedTimestamp, - ) + stmt.BindInt64(1, int64(guild.GetID())) + stmt.BindText(2, guild.GetName()) + stmt.BindText(3, guild.GetMOTD()) + stmt.BindInt64(4, int64(guild.GetLevel())) + stmt.BindInt64(5, guild.GetEXPCurrent()) + stmt.BindInt64(6, guild.GetEXPToNextLevel()) + stmt.BindInt64(7, formedTimestamp) + + _, err = stmt.Step() if err != nil { return fmt.Errorf("failed to save guild %d: %w", guild.GetID(), err) } @@ -300,53 +408,74 @@ func (dgm *DatabaseGuildManager) SaveGuildMembers(ctx context.Context, guildID i return nil } - // Use a transaction for atomic updates - return dgm.db.Transaction(func(db database.DBInterface) error { - // Delete existing members for this guild - if err := db.Exec("DELETE FROM guild_members WHERE guild_id = ?", guildID); err != nil { - return fmt.Errorf("failed to delete existing guild members: %w", err) + conn, err := dgm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + endFn, err := sqlitex.ImmediateTransaction(conn) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer endFn(&err) + + // Delete existing members for this guild + delStmt := conn.Prep("DELETE FROM guild_members WHERE guild_id = ?") + delStmt.BindInt64(1, int64(guildID)) + _, err = delStmt.Step() + delStmt.Finalize() + if err != nil { + return fmt.Errorf("failed to delete existing guild members: %w", err) + } + + // Insert all members + insertStmt := conn.Prep(`INSERT INTO guild_members + (char_id, guild_id, account_id, recruiter_id, name, guild_status, points, + adventure_class, adventure_level, tradeskill_class, tradeskill_level, rank, + member_flags, zone, join_date, last_login_date, note, officer_note, + recruiter_description, recruiter_picture_data, recruiting_show_adventure_class) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + defer insertStmt.Finalize() + + for _, member := range members { + joinTimestamp := member.GetJoinDate().Unix() + lastLoginTimestamp := member.GetLastLoginDate().Unix() + + insertStmt.Reset() + insertStmt.BindInt64(1, int64(member.GetCharacterID())) + insertStmt.BindInt64(2, int64(guildID)) + insertStmt.BindInt64(3, int64(member.AccountID)) + insertStmt.BindInt64(4, int64(member.GetRecruiterID())) + insertStmt.BindText(5, member.GetName()) + insertStmt.BindInt64(6, int64(member.GuildStatus)) + insertStmt.BindFloat(7, member.GetPoints()) + insertStmt.BindInt64(8, int64(member.GetAdventureClass())) + insertStmt.BindInt64(9, int64(member.GetAdventureLevel())) + insertStmt.BindInt64(10, int64(member.GetTradeskillClass())) + insertStmt.BindInt64(11, int64(member.GetTradeskillLevel())) + insertStmt.BindInt64(12, int64(member.GetRank())) + insertStmt.BindInt64(13, int64(member.GetMemberFlags())) + insertStmt.BindText(14, member.GetZone()) + insertStmt.BindInt64(15, joinTimestamp) + insertStmt.BindInt64(16, lastLoginTimestamp) + insertStmt.BindText(17, member.GetNote()) + insertStmt.BindText(18, member.GetOfficerNote()) + insertStmt.BindText(19, member.GetRecruiterDescription()) + if pictureData := member.GetRecruiterPictureData(); pictureData != nil { + insertStmt.BindBytes(20, pictureData) + } else { + insertStmt.BindNull(20) } + insertStmt.BindInt64(21, int64(member.RecruitingShowAdventureClass)) - // Insert all members - insertQuery := `INSERT INTO guild_members - (char_id, guild_id, account_id, recruiter_id, name, guild_status, points, - adventure_class, adventure_level, tradeskill_class, tradeskill_level, rank, - member_flags, zone, join_date, last_login_date, note, officer_note, - recruiter_description, recruiter_picture_data, recruiting_show_adventure_class) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - - for _, member := range members { - joinTimestamp := member.GetJoinDate().Unix() - lastLoginTimestamp := member.GetLastLoginDate().Unix() - - if err := db.Exec(insertQuery, - member.GetCharacterID(), - guildID, - member.AccountID, - member.GetRecruiterID(), - member.GetName(), - member.GuildStatus, - member.GetPoints(), - member.GetAdventureClass(), - member.GetAdventureLevel(), - member.GetTradeskillClass(), - member.GetTradeskillLevel(), - member.GetRank(), - member.GetMemberFlags(), - member.GetZone(), - joinTimestamp, - lastLoginTimestamp, - member.GetNote(), - member.GetOfficerNote(), - member.GetRecruiterDescription(), - member.GetRecruiterPictureData(), - member.RecruitingShowAdventureClass, - ); err != nil { - return fmt.Errorf("failed to insert guild member %d: %w", member.GetCharacterID(), err) - } + _, err = insertStmt.Step() + if err != nil { + return fmt.Errorf("failed to insert guild member %d: %w", member.GetCharacterID(), err) } - return nil - }) + } + + return nil } // SaveGuildEvents saves guild events @@ -355,9 +484,16 @@ func (dgm *DatabaseGuildManager) SaveGuildEvents(ctx context.Context, guildID in return nil } - query := `INSERT OR REPLACE INTO guild_events - (event_id, guild_id, date, type, description, locked) - VALUES (?, ?, ?, ?, ?, ?)` + conn, err := dgm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep(`INSERT OR REPLACE INTO guild_events + (event_id, guild_id, date, type, description, locked) + VALUES (?, ?, ?, ?, ?, ?)`) + defer stmt.Finalize() for _, event := range events { if !event.SaveNeeded { @@ -366,14 +502,16 @@ func (dgm *DatabaseGuildManager) SaveGuildEvents(ctx context.Context, guildID in dateTimestamp := event.Date.Unix() - if err := dgm.db.Exec(query, - event.EventID, - guildID, - dateTimestamp, - event.Type, - event.Description, - event.Locked, - ); err != nil { + stmt.Reset() + stmt.BindInt64(1, event.EventID) + stmt.BindInt64(2, int64(guildID)) + stmt.BindInt64(3, dateTimestamp) + stmt.BindInt64(4, int64(event.Type)) + stmt.BindText(5, event.Description) + stmt.BindInt64(6, int64(event.Locked)) + + _, err = stmt.Step() + if err != nil { return fmt.Errorf("failed to save guild event %d: %w", event.EventID, err) } } @@ -383,100 +521,192 @@ func (dgm *DatabaseGuildManager) SaveGuildEvents(ctx context.Context, guildID in // SaveGuildRanks saves guild rank names func (dgm *DatabaseGuildManager) SaveGuildRanks(ctx context.Context, guildID int32, ranks map[int8]string) error { - // Use a transaction for atomic updates - return dgm.db.Transaction(func(db database.DBInterface) error { - // Delete existing ranks for this guild - if err := db.Exec("DELETE FROM guild_ranks WHERE guild_id = ?", guildID); err != nil { - return fmt.Errorf("failed to delete existing guild ranks: %w", err) - } + conn, err := dgm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) - // Insert all ranks - insertQuery := "INSERT INTO guild_ranks (guild_id, rank, name) VALUES (?, ?, ?)" + endFn, err := sqlitex.ImmediateTransaction(conn) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer endFn(&err) - for rank, name := range ranks { - // Only save non-default rank names - if defaultName, exists := DefaultRankNames[rank]; !exists || name != defaultName { - if err := db.Exec(insertQuery, guildID, rank, name); err != nil { - return fmt.Errorf("failed to insert guild rank %d: %w", rank, err) - } + // Delete existing ranks for this guild + delStmt := conn.Prep("DELETE FROM guild_ranks WHERE guild_id = ?") + delStmt.BindInt64(1, int64(guildID)) + _, err = delStmt.Step() + delStmt.Finalize() + if err != nil { + return fmt.Errorf("failed to delete existing guild ranks: %w", err) + } + + // Insert all ranks + insertStmt := conn.Prep("INSERT INTO guild_ranks (guild_id, rank, name) VALUES (?, ?, ?)") + defer insertStmt.Finalize() + + for rank, name := range ranks { + // Only save non-default rank names + if defaultName, exists := DefaultRankNames[rank]; !exists || name != defaultName { + insertStmt.Reset() + insertStmt.BindInt64(1, int64(guildID)) + insertStmt.BindInt64(2, int64(rank)) + insertStmt.BindText(3, name) + + _, err = insertStmt.Step() + if err != nil { + return fmt.Errorf("failed to insert guild rank %d: %w", rank, err) } } - return nil - }) + } + + return nil } // SaveGuildPermissions saves guild permissions func (dgm *DatabaseGuildManager) SaveGuildPermissions(ctx context.Context, guildID int32, permissions map[int8]map[int8]int8) error { - // Use a transaction for atomic updates - return dgm.db.Transaction(func(db database.DBInterface) error { - // Delete existing permissions for this guild - if err := db.Exec("DELETE FROM guild_permissions WHERE guild_id = ?", guildID); err != nil { - return fmt.Errorf("failed to delete existing guild permissions: %w", err) - } + conn, err := dgm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) - // Insert all permissions - insertQuery := "INSERT INTO guild_permissions (guild_id, rank, permission, value) VALUES (?, ?, ?, ?)" + endFn, err := sqlitex.ImmediateTransaction(conn) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer endFn(&err) - for rank, rankPermissions := range permissions { - for permission, value := range rankPermissions { - if err := db.Exec(insertQuery, guildID, rank, permission, value); err != nil { - return fmt.Errorf("failed to insert guild permission %d/%d: %w", rank, permission, err) - } + // Delete existing permissions for this guild + delStmt := conn.Prep("DELETE FROM guild_permissions WHERE guild_id = ?") + delStmt.BindInt64(1, int64(guildID)) + _, err = delStmt.Step() + delStmt.Finalize() + if err != nil { + return fmt.Errorf("failed to delete existing guild permissions: %w", err) + } + + // Insert all permissions + insertStmt := conn.Prep("INSERT INTO guild_permissions (guild_id, rank, permission, value) VALUES (?, ?, ?, ?)") + defer insertStmt.Finalize() + + for rank, rankPermissions := range permissions { + for permission, value := range rankPermissions { + insertStmt.Reset() + insertStmt.BindInt64(1, int64(guildID)) + insertStmt.BindInt64(2, int64(rank)) + insertStmt.BindInt64(3, int64(permission)) + insertStmt.BindInt64(4, int64(value)) + + _, err = insertStmt.Step() + if err != nil { + return fmt.Errorf("failed to insert guild permission %d/%d: %w", rank, permission, err) } } - return nil - }) + } + + return nil } // SaveGuildEventFilters saves guild event filters func (dgm *DatabaseGuildManager) SaveGuildEventFilters(ctx context.Context, guildID int32, filters map[int8]map[int8]int8) error { - // Use a transaction for atomic updates - return dgm.db.Transaction(func(db database.DBInterface) error { - // Delete existing filters for this guild - if err := db.Exec("DELETE FROM guild_event_filters WHERE guild_id = ?", guildID); err != nil { - return fmt.Errorf("failed to delete existing guild event filters: %w", err) - } + conn, err := dgm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) - // Insert all filters - insertQuery := "INSERT INTO guild_event_filters (guild_id, event_id, category, value) VALUES (?, ?, ?, ?)" + endFn, err := sqlitex.ImmediateTransaction(conn) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer endFn(&err) - for eventID, eventFilters := range filters { - for category, value := range eventFilters { - if err := db.Exec(insertQuery, guildID, eventID, category, value); err != nil { - return fmt.Errorf("failed to insert guild event filter %d/%d: %w", eventID, category, err) - } + // Delete existing filters for this guild + delStmt := conn.Prep("DELETE FROM guild_event_filters WHERE guild_id = ?") + delStmt.BindInt64(1, int64(guildID)) + _, err = delStmt.Step() + delStmt.Finalize() + if err != nil { + return fmt.Errorf("failed to delete existing guild event filters: %w", err) + } + + // Insert all filters + insertStmt := conn.Prep("INSERT INTO guild_event_filters (guild_id, event_id, category, value) VALUES (?, ?, ?, ?)") + defer insertStmt.Finalize() + + for eventID, eventFilters := range filters { + for category, value := range eventFilters { + insertStmt.Reset() + insertStmt.BindInt64(1, int64(guildID)) + insertStmt.BindInt64(2, int64(eventID)) + insertStmt.BindInt64(3, int64(category)) + insertStmt.BindInt64(4, int64(value)) + + _, err = insertStmt.Step() + if err != nil { + return fmt.Errorf("failed to insert guild event filter %d/%d: %w", eventID, category, err) } } - return nil - }) + } + + return nil } // SaveGuildRecruiting saves guild recruiting settings func (dgm *DatabaseGuildManager) SaveGuildRecruiting(ctx context.Context, guildID int32, flags, descTags map[int8]int8) error { - // Use a transaction for atomic updates - return dgm.db.Transaction(func(db database.DBInterface) error { - // Delete existing recruiting settings for this guild - if err := db.Exec("DELETE FROM guild_recruiting WHERE guild_id = ?", guildID); err != nil { - return fmt.Errorf("failed to delete existing guild recruiting: %w", err) - } + conn, err := dgm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) - // Insert recruiting flags - insertQuery := "INSERT INTO guild_recruiting (guild_id, flag, value) VALUES (?, ?, ?)" + endFn, err := sqlitex.ImmediateTransaction(conn) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer endFn(&err) - for flag, value := range flags { - if err := db.Exec(insertQuery, guildID, flag, value); err != nil { - return fmt.Errorf("failed to insert guild recruiting flag %d: %w", flag, err) - } - } + // Delete existing recruiting settings for this guild + delStmt := conn.Prep("DELETE FROM guild_recruiting WHERE guild_id = ?") + delStmt.BindInt64(1, int64(guildID)) + _, err = delStmt.Step() + delStmt.Finalize() + if err != nil { + return fmt.Errorf("failed to delete existing guild recruiting: %w", err) + } - // Insert description tags (with negative flag values to distinguish) - for tag, value := range descTags { - if err := db.Exec(insertQuery, guildID, -tag-1, value); err != nil { // Negative to distinguish from flags - return fmt.Errorf("failed to insert guild recruiting desc tag %d: %w", tag, err) - } + // Insert recruiting flags and description tags + insertStmt := conn.Prep("INSERT INTO guild_recruiting (guild_id, flag, value) VALUES (?, ?, ?)") + defer insertStmt.Finalize() + + for flag, value := range flags { + insertStmt.Reset() + insertStmt.BindInt64(1, int64(guildID)) + insertStmt.BindInt64(2, int64(flag)) + insertStmt.BindInt64(3, int64(value)) + + _, err = insertStmt.Step() + if err != nil { + return fmt.Errorf("failed to insert guild recruiting flag %d: %w", flag, err) } - return nil - }) + } + + // Insert description tags (with negative flag values to distinguish) + for tag, value := range descTags { + insertStmt.Reset() + insertStmt.BindInt64(1, int64(guildID)) + insertStmt.BindInt64(2, int64(-tag-1)) // Negative to distinguish from flags + insertStmt.BindInt64(3, int64(value)) + + _, err = insertStmt.Step() + if err != nil { + return fmt.Errorf("failed to insert guild recruiting desc tag %d: %w", tag, err) + } + } + + return nil } // SavePointHistory saves point history for a member @@ -485,256 +715,202 @@ func (dgm *DatabaseGuildManager) SavePointHistory(ctx context.Context, character return nil } - // Use a transaction for atomic updates - return dgm.db.Transaction(func(db database.DBInterface) error { - // Delete existing history for this character - if err := db.Exec("DELETE FROM guild_point_history WHERE char_id = ?", characterID); err != nil { - return fmt.Errorf("failed to delete existing point history: %w", err) - } - - // Insert all history entries - insertQuery := "INSERT INTO guild_point_history (char_id, date, modified_by, comment, points) VALUES (?, ?, ?, ?, ?)" - - for _, entry := range history { - dateTimestamp := entry.Date.Unix() - - if err := db.Exec(insertQuery, - characterID, - dateTimestamp, - entry.ModifiedBy, - entry.Comment, - entry.Points, - ); err != nil { - return fmt.Errorf("failed to insert point history entry: %w", err) - } - } - return nil - }) -} - -// GetGuildIDByCharacterID returns guild ID for a character -func (dgm *DatabaseGuildManager) GetGuildIDByCharacterID(ctx context.Context, characterID int32) (int32, error) { - query := "SELECT guild_id FROM guild_members WHERE char_id = ?" - - row, err := dgm.db.QueryRow(query, characterID) + conn, err := dgm.pool.Take(ctx) if err != nil { - return 0, fmt.Errorf("failed to get guild ID for character %d: %w", characterID, err) + return fmt.Errorf("failed to get database connection: %w", err) } - if row == nil { - return 0, fmt.Errorf("character %d not found in any guild", characterID) - } - defer row.Close() + defer dgm.pool.Put(conn) - guildID := int32(row.Int(0)) - return guildID, nil -} - -// CreateGuild creates a new guild -func (dgm *DatabaseGuildManager) CreateGuild(ctx context.Context, guildData GuildData) (int32, error) { - query := `INSERT INTO guilds (name, motd, level, xp, xp_needed, formed_on) - VALUES (?, ?, ?, ?, ?, ?)` - - formedTimestamp := guildData.FormedDate.Unix() - - id, err := dgm.db.ExecAndGetLastInsertID(query, - guildData.Name, - guildData.MOTD, - guildData.Level, - guildData.EXPCurrent, - guildData.EXPToNextLevel, - formedTimestamp, - ) + endFn, err := sqlitex.ImmediateTransaction(conn) if err != nil { - return 0, fmt.Errorf("failed to create guild: %w", err) + return fmt.Errorf("failed to begin transaction: %w", err) } + defer endFn(&err) - return int32(id), nil -} - -// DeleteGuild removes a guild and all related data -func (dgm *DatabaseGuildManager) DeleteGuild(ctx context.Context, guildID int32) error { - // Use a transaction for atomic deletion - return dgm.db.Transaction(func(db database.DBInterface) error { - // Delete related data first (foreign key constraints) - tables := []string{ - "guild_point_history", - "guild_members", - "guild_events", - "guild_ranks", - "guild_permissions", - "guild_event_filters", - "guild_recruiting", - } - - for _, table := range tables { - var query string - if table == "guild_point_history" { - // Special case: delete point history for all members of this guild - query = "DELETE FROM guild_point_history WHERE character_id IN (SELECT char_id FROM guild_members WHERE guild_id = ?)" - } else { - query = fmt.Sprintf("DELETE FROM %s WHERE guild_id = ?", table) - } - - if err := db.Exec(query, guildID); err != nil { - return fmt.Errorf("failed to delete from %s: %w", table, err) - } - } - - // Finally delete the guild itself - if err := db.Exec("DELETE FROM guilds WHERE id = ?", guildID); err != nil { - return fmt.Errorf("failed to delete guild: %w", err) - } - return nil - }) -} - -// GetNextGuildID returns the next available guild ID -func (dgm *DatabaseGuildManager) GetNextGuildID(ctx context.Context) (int32, error) { - query := "SELECT COALESCE(MAX(id), 0) + 1 FROM guilds" - - row, err := dgm.db.QueryRow(query) + // Delete existing history for this character + delStmt := conn.Prep("DELETE FROM guild_point_history WHERE char_id = ?") + delStmt.BindInt64(1, int64(characterID)) + _, err = delStmt.Step() + delStmt.Finalize() if err != nil { - return 0, fmt.Errorf("failed to get next guild ID: %w", err) - } - if row == nil { - return 1, nil // If no guilds exist, start with ID 1 - } - defer row.Close() - - nextID := int32(row.Int(0)) - return nextID, nil -} - -// GetNextEventID returns the next available event ID for a guild -func (dgm *DatabaseGuildManager) GetNextEventID(ctx context.Context, guildID int32) (int64, error) { - query := "SELECT COALESCE(MAX(event_id), 0) + 1 FROM guild_events WHERE guild_id = ?" - - row, err := dgm.db.QueryRow(query, guildID) - if err != nil { - return 0, fmt.Errorf("failed to get next event ID for guild %d: %w", guildID, err) - } - if row == nil { - return 1, nil // If no events exist for this guild, start with ID 1 - } - defer row.Close() - - nextID := row.Int64(0) - return nextID, nil -} - -// EnsureGuildTables creates the guild tables if they don't exist -func (dgm *DatabaseGuildManager) EnsureGuildTables(ctx context.Context) error { - queries := []string{ - `CREATE TABLE IF NOT EXISTS guilds ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - motd TEXT, - level INTEGER NOT NULL DEFAULT 1, - xp INTEGER NOT NULL DEFAULT 111, - xp_needed INTEGER NOT NULL DEFAULT 2521, - formed_on INTEGER NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - )`, - `CREATE TABLE IF NOT EXISTS guild_members ( - char_id INTEGER NOT NULL, - guild_id INTEGER NOT NULL, - account_id INTEGER NOT NULL DEFAULT 0, - recruiter_id INTEGER NOT NULL DEFAULT 0, - name TEXT NOT NULL, - guild_status INTEGER NOT NULL DEFAULT 0, - points REAL NOT NULL DEFAULT 0.0, - adventure_class INTEGER NOT NULL DEFAULT 0, - adventure_level INTEGER NOT NULL DEFAULT 1, - tradeskill_class INTEGER NOT NULL DEFAULT 0, - tradeskill_level INTEGER NOT NULL DEFAULT 1, - rank INTEGER NOT NULL DEFAULT 7, - member_flags INTEGER NOT NULL DEFAULT 0, - zone TEXT NOT NULL DEFAULT '', - join_date INTEGER NOT NULL, - last_login_date INTEGER NOT NULL, - note TEXT NOT NULL DEFAULT '', - officer_note TEXT NOT NULL DEFAULT '', - recruiter_description TEXT NOT NULL DEFAULT '', - recruiter_picture_data BLOB, - recruiting_show_adventure_class INTEGER NOT NULL DEFAULT 1, - PRIMARY KEY (char_id), - FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS guild_events ( - event_id INTEGER NOT NULL, - guild_id INTEGER NOT NULL, - date INTEGER NOT NULL, - type INTEGER NOT NULL, - description TEXT NOT NULL, - locked INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (event_id, guild_id), - FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS guild_ranks ( - guild_id INTEGER NOT NULL, - rank INTEGER NOT NULL, - name TEXT NOT NULL, - PRIMARY KEY (guild_id, rank), - FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS guild_permissions ( - guild_id INTEGER NOT NULL, - rank INTEGER NOT NULL, - permission INTEGER NOT NULL, - value INTEGER NOT NULL, - PRIMARY KEY (guild_id, rank, permission), - FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS guild_event_filters ( - guild_id INTEGER NOT NULL, - event_id INTEGER NOT NULL, - category INTEGER NOT NULL, - value INTEGER NOT NULL, - PRIMARY KEY (guild_id, event_id, category), - FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS guild_recruiting ( - guild_id INTEGER NOT NULL, - flag INTEGER NOT NULL, - value INTEGER NOT NULL, - PRIMARY KEY (guild_id, flag), - FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE - )`, - `CREATE TABLE IF NOT EXISTS guild_point_history ( - char_id INTEGER NOT NULL, - date INTEGER NOT NULL, - modified_by TEXT NOT NULL, - comment TEXT NOT NULL, - points REAL NOT NULL, - PRIMARY KEY (char_id, date), - FOREIGN KEY (char_id) REFERENCES guild_members(char_id) ON DELETE CASCADE - )`, + return fmt.Errorf("failed to delete existing point history: %w", err) } - for i, query := range queries { - if err := dgm.db.Exec(query); err != nil { - return fmt.Errorf("failed to create guild table %d: %w", i+1, err) - } - } + // Insert all history entries + insertStmt := conn.Prep("INSERT INTO guild_point_history (char_id, date, modified_by, comment, points) VALUES (?, ?, ?, ?, ?)") + defer insertStmt.Finalize() - // Create indexes for better performance - indexes := []string{ - `CREATE INDEX IF NOT EXISTS idx_guild_members_guild_id ON guild_members(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_guild_members_char_id ON guild_members(char_id)`, - `CREATE INDEX IF NOT EXISTS idx_guild_events_guild_id ON guild_events(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_guild_events_date ON guild_events(date)`, - `CREATE INDEX IF NOT EXISTS idx_guild_point_history_char_id ON guild_point_history(char_id)`, - `CREATE INDEX IF NOT EXISTS idx_guilds_name ON guilds(name)`, - `CREATE INDEX IF NOT EXISTS idx_guild_members_rank ON guild_members(rank)`, - `CREATE INDEX IF NOT EXISTS idx_guild_members_last_login ON guild_members(last_login_date)`, - } + for _, entry := range history { + dateTimestamp := entry.Date.Unix() - for i, query := range indexes { - if err := dgm.db.Exec(query); err != nil { - return fmt.Errorf("failed to create guild index %d: %w", i+1, err) + insertStmt.Reset() + insertStmt.BindInt64(1, int64(characterID)) + insertStmt.BindInt64(2, dateTimestamp) + insertStmt.BindText(3, entry.ModifiedBy) + insertStmt.BindText(4, entry.Comment) + insertStmt.BindFloat(5, entry.Points) + + _, err = insertStmt.Step() + if err != nil { + return fmt.Errorf("failed to insert point history entry: %w", err) } } return nil } + +// GetGuildIDByCharacterID returns guild ID for a character +func (dgm *DatabaseGuildManager) GetGuildIDByCharacterID(ctx context.Context, characterID int32) (int32, error) { + conn, err := dgm.pool.Take(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep("SELECT guild_id FROM guild_members WHERE char_id = ?") + defer stmt.Finalize() + stmt.BindInt64(1, int64(characterID)) + + hasRow, err := stmt.Step() + if err != nil { + return 0, fmt.Errorf("failed to get guild ID for character %d: %w", characterID, err) + } + if !hasRow { + return 0, fmt.Errorf("character %d not found in any guild", characterID) + } + + guildID := int32(stmt.ColumnInt64(0)) + return guildID, nil +} + +// CreateGuild creates a new guild +func (dgm *DatabaseGuildManager) CreateGuild(ctx context.Context, guildData GuildData) (int32, error) { + conn, err := dgm.pool.Take(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep(`INSERT INTO guilds (name, motd, level, xp, xp_needed, formed_on) + VALUES (?, ?, ?, ?, ?, ?)`) + defer stmt.Finalize() + + formedTimestamp := guildData.FormedDate.Unix() + + stmt.BindText(1, guildData.Name) + stmt.BindText(2, guildData.MOTD) + stmt.BindInt64(3, int64(guildData.Level)) + stmt.BindInt64(4, guildData.EXPCurrent) + stmt.BindInt64(5, guildData.EXPToNextLevel) + stmt.BindInt64(6, formedTimestamp) + + _, err = stmt.Step() + if err != nil { + return 0, fmt.Errorf("failed to create guild: %w", err) + } + + id := conn.LastInsertRowID() + return int32(id), nil +} + +// DeleteGuild removes a guild and all related data +func (dgm *DatabaseGuildManager) DeleteGuild(ctx context.Context, guildID int32) error { + conn, err := dgm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + endFn, err := sqlitex.ImmediateTransaction(conn) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer endFn(&err) + + // Delete related data first (foreign key constraints) + tables := []string{ + "guild_point_history", + "guild_members", + "guild_events", + "guild_ranks", + "guild_permissions", + "guild_event_filters", + "guild_recruiting", + } + + for _, table := range tables { + var query string + if table == "guild_point_history" { + // Special case: delete point history for all members of this guild + query = "DELETE FROM guild_point_history WHERE char_id IN (SELECT char_id FROM guild_members WHERE guild_id = ?)" + } else { + query = fmt.Sprintf("DELETE FROM %s WHERE guild_id = ?", table) + } + + stmt := conn.Prep(query) + stmt.BindInt64(1, int64(guildID)) + _, err = stmt.Step() + stmt.Finalize() + if err != nil { + return fmt.Errorf("failed to delete from %s: %w", table, err) + } + } + + // Finally delete the guild itself + delStmt := conn.Prep("DELETE FROM guilds WHERE id = ?") + delStmt.BindInt64(1, int64(guildID)) + _, err = delStmt.Step() + delStmt.Finalize() + if err != nil { + return fmt.Errorf("failed to delete guild: %w", err) + } + + return nil +} + +// GetNextGuildID returns the next available guild ID +func (dgm *DatabaseGuildManager) GetNextGuildID(ctx context.Context) (int32, error) { + conn, err := dgm.pool.Take(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep("SELECT COALESCE(MAX(id), 0) + 1 FROM guilds") + defer stmt.Finalize() + + hasRow, err := stmt.Step() + if err != nil { + return 0, fmt.Errorf("failed to get next guild ID: %w", err) + } + if !hasRow { + return 1, nil // If no guilds exist, start with ID 1 + } + + nextID := int32(stmt.ColumnInt64(0)) + return nextID, nil +} + +// GetNextEventID returns the next available event ID for a guild +func (dgm *DatabaseGuildManager) GetNextEventID(ctx context.Context, guildID int32) (int64, error) { + conn, err := dgm.pool.Take(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get database connection: %w", err) + } + defer dgm.pool.Put(conn) + + stmt := conn.Prep("SELECT COALESCE(MAX(event_id), 0) + 1 FROM guild_events WHERE guild_id = ?") + defer stmt.Finalize() + stmt.BindInt64(1, int64(guildID)) + + hasRow, err := stmt.Step() + if err != nil { + return 0, fmt.Errorf("failed to get next event ID for guild %d: %w", guildID, err) + } + if !hasRow { + return 1, nil // If no events exist for this guild, start with ID 1 + } + + nextID := stmt.ColumnInt64(0) + return nextID, nil +} diff --git a/internal/guilds/database_test.go b/internal/guilds/database_test.go index 90d0837..1744c84 100644 --- a/internal/guilds/database_test.go +++ b/internal/guilds/database_test.go @@ -7,32 +7,41 @@ import ( "testing" "time" - "eq2emu/internal/database" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" ) -// createTestDB creates a temporary test database -func createTestDB(t *testing.T) *database.DB { +// createTestPool creates a temporary test database pool +func createTestPool(t *testing.T) *sqlitex.Pool { // Create temporary directory for test database tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test_guilds.db") - // Create and initialize database - db, err := database.Open(dbPath) + // Create and initialize database pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, + }) if err != nil { - t.Fatalf("Failed to create test database: %v", err) + t.Fatalf("Failed to create test database pool: %v", err) } // Create guild tables for testing - err = createGuildTables(db) + conn, err := pool.Take(context.Background()) + if err != nil { + t.Fatalf("Failed to get connection: %v", err) + } + defer pool.Put(conn) + + err = createGuildTables(conn) if err != nil { t.Fatalf("Failed to create guild tables: %v", err) } - return db + return pool } // createGuildTables creates the necessary tables for guild testing -func createGuildTables(db *database.DB) error { +func createGuildTables(conn *sqlite.Conn) error { tables := []string{ `CREATE TABLE IF NOT EXISTS guilds ( id INTEGER PRIMARY KEY, @@ -68,12 +77,13 @@ func createGuildTables(db *database.DB) error { FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_events ( - event_id INTEGER PRIMARY KEY, + event_id INTEGER NOT NULL, guild_id INTEGER NOT NULL, date INTEGER NOT NULL, type INTEGER NOT NULL, description TEXT NOT NULL, locked INTEGER DEFAULT 0, + PRIMARY KEY (event_id, guild_id), FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_ranks ( @@ -107,17 +117,19 @@ func createGuildTables(db *database.DB) error { FOREIGN KEY (guild_id) REFERENCES guilds(id) )`, `CREATE TABLE IF NOT EXISTS guild_point_history ( - character_id INTEGER NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + char_id INTEGER NOT NULL, date INTEGER NOT NULL, modified_by TEXT NOT NULL, comment TEXT NOT NULL, points REAL NOT NULL, - FOREIGN KEY (character_id) REFERENCES guild_members(char_id) + FOREIGN KEY (char_id) REFERENCES guild_members(char_id) )`, } for _, sql := range tables { - if err := db.Exec(sql); err != nil { + err := sqlitex.ExecScript(conn, sql) + if err != nil { return err } } @@ -125,22 +137,57 @@ func createGuildTables(db *database.DB) error { return nil } +// execSQL is a helper to execute SQL with parameters +func execSQL(t *testing.T, pool *sqlitex.Pool, query string, args ...interface{}) { + conn, err := pool.Take(context.Background()) + if err != nil { + t.Fatalf("Failed to get connection: %v", err) + } + defer pool.Put(conn) + + stmt := conn.Prep(query) + defer stmt.Finalize() + + for i, arg := range args { + switch v := arg.(type) { + case int: + stmt.BindInt64(i+1, int64(v)) + case int32: + stmt.BindInt64(i+1, int64(v)) + case int64: + stmt.BindInt64(i+1, v) + case float64: + stmt.BindFloat(i+1, v) + case string: + stmt.BindText(i+1, v) + case []byte: + stmt.BindBytes(i+1, v) + case nil: + stmt.BindNull(i + 1) + default: + t.Fatalf("Unsupported argument type: %T", v) + } + } + + _, err = stmt.Step() + if err != nil { + t.Fatalf("Failed to execute SQL: %v", err) + } +} + // TestDatabaseGuildManager_LoadGuilds tests loading guilds from database func TestDatabaseGuildManager_LoadGuilds(t *testing.T) { - db := createTestDB(t) - defer db.Close() + pool := createTestPool(t) + defer pool.Close() - dgm := NewDatabaseGuildManager(db) + dgm := NewDatabaseGuildManager(pool) ctx := context.Background() // Insert test data - insertGuildSQL := `INSERT INTO guilds (id, name, motd, level, xp, xp_needed, formed_on) - VALUES (1, 'Test Guild', 'Welcome!', 5, 1000, 5000, ?)` formedTime := time.Now().Unix() - err := db.Exec(insertGuildSQL, formedTime) - if err != nil { - t.Fatalf("Failed to insert test guild: %v", err) - } + execSQL(t, pool, `INSERT INTO guilds (id, name, motd, level, xp, xp_needed, formed_on) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + 1, "Test Guild", "Welcome!", 5, 1000, 5000, formedTime) // Test loading guilds guilds, err := dgm.LoadGuilds(ctx) @@ -152,96 +199,46 @@ func TestDatabaseGuildManager_LoadGuilds(t *testing.T) { t.Errorf("Expected 1 guild, got %d", len(guilds)) } - guild := guilds[0] - if guild.ID != 1 { - t.Errorf("Expected guild ID 1, got %d", guild.ID) - } - if guild.Name != "Test Guild" { - t.Errorf("Expected guild name 'Test Guild', got '%s'", guild.Name) - } - if guild.MOTD != "Welcome!" { - t.Errorf("Expected MOTD 'Welcome!', got '%s'", guild.MOTD) - } - if guild.Level != 5 { - t.Errorf("Expected level 5, got %d", guild.Level) - } - if guild.EXPCurrent != 1000 { - t.Errorf("Expected current exp 1000, got %d", guild.EXPCurrent) - } - if guild.EXPToNextLevel != 5000 { - t.Errorf("Expected next level exp 5000, got %d", guild.EXPToNextLevel) - } -} - -// TestDatabaseGuildManager_LoadGuild tests loading a specific guild -func TestDatabaseGuildManager_LoadGuild(t *testing.T) { - db := createTestDB(t) - defer db.Close() - - dgm := NewDatabaseGuildManager(db) - ctx := context.Background() - - // Insert test data - insertGuildSQL := `INSERT INTO guilds (id, name, motd, level, xp, xp_needed, formed_on) - VALUES (123, 'Specific Guild', 'Test MOTD', 10, 2000, 8000, ?)` - formedTime := time.Now().Unix() - err := db.Exec(insertGuildSQL, formedTime) - if err != nil { - t.Fatalf("Failed to insert test guild: %v", err) - } - - // Test loading specific guild - guild, err := dgm.LoadGuild(ctx, 123) - if err != nil { - t.Fatalf("Failed to load guild: %v", err) - } - - if guild.ID != 123 { - t.Errorf("Expected guild ID 123, got %d", guild.ID) - } - if guild.Name != "Specific Guild" { - t.Errorf("Expected guild name 'Specific Guild', got '%s'", guild.Name) - } - if guild.MOTD != "Test MOTD" { - t.Errorf("Expected MOTD 'Test MOTD', got '%s'", guild.MOTD) - } - - // Test loading non-existent guild - _, err = dgm.LoadGuild(ctx, 999) - if err == nil { - t.Error("Expected error when loading non-existent guild") + if len(guilds) > 0 { + guild := guilds[0] + if guild.ID != 1 { + t.Errorf("Expected guild ID 1, got %d", guild.ID) + } + if guild.Name != "Test Guild" { + t.Errorf("Expected guild name 'Test Guild', got '%s'", guild.Name) + } + if guild.MOTD != "Welcome!" { + t.Errorf("Expected MOTD 'Welcome!', got '%s'", guild.MOTD) + } + if guild.Level != 5 { + t.Errorf("Expected level 5, got %d", guild.Level) + } } } // TestDatabaseGuildManager_LoadGuildMembers tests loading guild members func TestDatabaseGuildManager_LoadGuildMembers(t *testing.T) { - db := createTestDB(t) - defer db.Close() + pool := createTestPool(t) + defer pool.Close() - dgm := NewDatabaseGuildManager(db) + dgm := NewDatabaseGuildManager(pool) ctx := context.Background() - // Insert test guild - err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Test Guild')`) - if err != nil { - t.Fatalf("Failed to insert test guild: %v", err) - } + // Insert test guild and members + execSQL(t, pool, `INSERT INTO guilds (id, name) VALUES (?, ?)`, 1, "Test Guild") - // Insert test members joinTime := time.Now().Unix() - memberSQL := `INSERT INTO guild_members - (char_id, guild_id, account_id, name, rank, adventure_level, join_date, last_login_date) - VALUES (?, 1, 100, ?, ?, 50, ?, ?)` + loginTime := time.Now().Unix() - err = db.Exec(memberSQL, 1, "Player1", RankLeader, joinTime, joinTime) - if err != nil { - t.Fatalf("Failed to insert member 1: %v", err) - } - - err = db.Exec(memberSQL, 2, "Player2", RankMember, joinTime, joinTime) - if err != nil { - t.Fatalf("Failed to insert member 2: %v", err) - } + execSQL(t, pool, `INSERT INTO guild_members + (char_id, guild_id, account_id, name, guild_status, points, + adventure_class, adventure_level, tradeskill_class, tradeskill_level, + rank, member_flags, zone, join_date, last_login_date, note, officer_note, + recruiter_description, recruiter_picture_data, recruiting_show_adventure_class) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + 100, 1, 10, "TestPlayer", 1, 100.5, 5, 50, 2, 20, 3, 0, "Test Zone", + joinTime, loginTime, "Player note", "Officer note", "Recruiter desc", + []byte{0x01, 0x02, 0x03}, 1) // Test loading members members, err := dgm.LoadGuildMembers(ctx, 1) @@ -249,360 +246,181 @@ func TestDatabaseGuildManager_LoadGuildMembers(t *testing.T) { t.Fatalf("Failed to load guild members: %v", err) } - if len(members) != 2 { - t.Errorf("Expected 2 members, got %d", len(members)) + if len(members) != 1 { + t.Errorf("Expected 1 member, got %d", len(members)) } - // Verify first member - member1 := members[0] - if member1.CharacterID != 1 { - t.Errorf("Expected character ID 1, got %d", member1.CharacterID) - } - if member1.Name != "Player1" { - t.Errorf("Expected name 'Player1', got '%s'", member1.Name) - } - if member1.Rank != RankLeader { - t.Errorf("Expected rank %d, got %d", RankLeader, member1.Rank) + if len(members) > 0 { + member := members[0] + if member.CharacterID != 100 { + t.Errorf("Expected character ID 100, got %d", member.CharacterID) + } + if member.Name != "TestPlayer" { + t.Errorf("Expected name 'TestPlayer', got '%s'", member.Name) + } + if member.Points != 100.5 { + t.Errorf("Expected points 100.5, got %f", member.Points) + } + if len(member.RecruiterPictureData) != 3 { + t.Errorf("Expected picture data length 3, got %d", len(member.RecruiterPictureData)) + } } } // TestDatabaseGuildManager_SaveGuild tests saving guild data func TestDatabaseGuildManager_SaveGuild(t *testing.T) { - db := createTestDB(t) - defer db.Close() + pool := createTestPool(t) + defer pool.Close() - dgm := NewDatabaseGuildManager(db) + dgm := NewDatabaseGuildManager(pool) ctx := context.Background() - // Create test guild - guild := NewGuild() - guild.SetID(1) - guild.SetName("Saved Guild", false) - guild.SetLevel(15, false) - guild.SetMOTD("Saved MOTD", false) - guild.SetEXPCurrent(3000, false) - guild.SetEXPToNextLevel(9000, false) - guild.SetFormedDate(time.Now()) + // Create a test guild + guild := &Guild{ + id: 1, + name: "New Guild", + motd: "Test MOTD", + level: 10, + expCurrent: 2000, + expToNextLevel: 8000, + formedDate: time.Now(), + } - // Test saving guild + // Save the guild err := dgm.SaveGuild(ctx, guild) if err != nil { t.Fatalf("Failed to save guild: %v", err) } - // Verify the guild was saved - savedGuild, err := dgm.LoadGuild(ctx, 1) + // Load and verify + guilds, err := dgm.LoadGuilds(ctx) if err != nil { - t.Fatalf("Failed to load saved guild: %v", err) + t.Fatalf("Failed to load guilds: %v", err) } - if savedGuild.Name != "Saved Guild" { - t.Errorf("Expected saved name 'Saved Guild', got '%s'", savedGuild.Name) + if len(guilds) != 1 { + t.Errorf("Expected 1 guild, got %d", len(guilds)) } - if savedGuild.Level != 15 { - t.Errorf("Expected saved level 15, got %d", savedGuild.Level) - } - if savedGuild.MOTD != "Saved MOTD" { - t.Errorf("Expected saved MOTD 'Saved MOTD', got '%s'", savedGuild.MOTD) + + if len(guilds) > 0 { + loaded := guilds[0] + if loaded.Name != "New Guild" { + t.Errorf("Expected name 'New Guild', got '%s'", loaded.Name) + } + if loaded.MOTD != "Test MOTD" { + t.Errorf("Expected MOTD 'Test MOTD', got '%s'", loaded.MOTD) + } + if loaded.Level != 10 { + t.Errorf("Expected level 10, got %d", loaded.Level) + } } } -// TestDatabaseGuildManager_CreateGuild tests creating a new guild -func TestDatabaseGuildManager_CreateGuild(t *testing.T) { - db := createTestDB(t) - defer db.Close() +// TestDatabaseGuildManager_CreateAndDeleteGuild tests guild creation and deletion +func TestDatabaseGuildManager_CreateAndDeleteGuild(t *testing.T) { + pool := createTestPool(t) + defer pool.Close() - dgm := NewDatabaseGuildManager(db) + dgm := NewDatabaseGuildManager(pool) ctx := context.Background() // Create guild data guildData := GuildData{ - Name: "New Guild", - Level: 1, - FormedDate: time.Now(), - MOTD: "New guild MOTD", - EXPCurrent: 111, - EXPToNextLevel: 2521, - RecruitingShortDesc: "Short description", - RecruitingFullDesc: "Full description", - RecruitingMinLevel: 1, - RecruitingPlayStyle: 0, + Name: "Delete Test Guild", + MOTD: "To be deleted", + Level: 1, + EXPCurrent: 0, + EXPToNextLevel: 1000, + FormedDate: time.Now(), } - // Test creating guild + // Create the guild guildID, err := dgm.CreateGuild(ctx, guildData) if err != nil { t.Fatalf("Failed to create guild: %v", err) } if guildID <= 0 { - t.Errorf("Expected positive guild ID, got %d", guildID) + t.Errorf("Expected valid guild ID, got %d", guildID) } - // Verify the guild was created - createdGuild, err := dgm.LoadGuild(ctx, guildID) + // Verify it exists + guild, err := dgm.LoadGuild(ctx, guildID) if err != nil { t.Fatalf("Failed to load created guild: %v", err) } - if createdGuild.Name != "New Guild" { - t.Errorf("Expected created name 'New Guild', got '%s'", createdGuild.Name) - } -} - -// TestDatabaseGuildManager_DeleteGuild tests deleting a guild -func TestDatabaseGuildManager_DeleteGuild(t *testing.T) { - db := createTestDB(t) - defer db.Close() - - dgm := NewDatabaseGuildManager(db) - ctx := context.Background() - - // Insert test guild - err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'To Delete')`) - if err != nil { - t.Fatalf("Failed to insert test guild: %v", err) + if guild.Name != "Delete Test Guild" { + t.Errorf("Expected name 'Delete Test Guild', got '%s'", guild.Name) } - // Insert test member - err = db.Exec(`INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (1, 1, 100, 'Member')`) - if err != nil { - t.Fatalf("Failed to insert test member: %v", err) - } - - // Verify guild exists - _, err = dgm.LoadGuild(ctx, 1) - if err != nil { - t.Fatalf("Guild should exist before deletion: %v", err) - } - - // Test deleting guild - err = dgm.DeleteGuild(ctx, 1) + // Delete the guild + err = dgm.DeleteGuild(ctx, guildID) if err != nil { t.Fatalf("Failed to delete guild: %v", err) } - // Verify guild was deleted - _, err = dgm.LoadGuild(ctx, 1) + // Verify it's gone + _, err = dgm.LoadGuild(ctx, guildID) if err == nil { - t.Error("Guild should not exist after deletion") + t.Error("Expected error loading deleted guild, got nil") } - - // Verify members were deleted - members, err := dgm.LoadGuildMembers(ctx, 1) - if err != nil { - t.Fatalf("Failed to check members after guild deletion: %v", err) - } - if len(members) != 0 { - t.Errorf("Expected 0 members after guild deletion, got %d", len(members)) - } -} - -// TestDatabaseGuildManager_GetGuildIDByCharacterID tests getting guild ID by character ID -func TestDatabaseGuildManager_GetGuildIDByCharacterID(t *testing.T) { - db := createTestDB(t) - defer db.Close() - - dgm := NewDatabaseGuildManager(db) - ctx := context.Background() - - // Insert test guild - err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Test Guild')`) - if err != nil { - t.Fatalf("Failed to insert test guild: %v", err) - } - - // Insert test member - err = db.Exec(`INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (123, 1, 100, 'TestPlayer')`) - if err != nil { - t.Fatalf("Failed to insert test member: %v", err) - } - - // Test getting guild ID for character - guildID, err := dgm.GetGuildIDByCharacterID(ctx, 123) - if err != nil { - t.Fatalf("Failed to get guild ID by character ID: %v", err) - } - - if guildID != 1 { - t.Errorf("Expected guild ID 1, got %d", guildID) - } - - // Test getting guild ID for non-existent character - _, err = dgm.GetGuildIDByCharacterID(ctx, 999) - if err == nil { - t.Error("Expected error for non-existent character") - } -} - -// TestDatabaseGuildManager_ConcurrentOperations tests concurrent database operations -// Now enabled with sqlitex.Pool for proper connection management -func TestDatabaseGuildManager_ConcurrentOperations(t *testing.T) { - db := createTestDB(t) - defer db.Close() - - dgm := NewDatabaseGuildManager(db) - ctx := context.Background() - - // Insert initial guild - err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Concurrent Test')`) - if err != nil { - t.Fatalf("Failed to insert test guild: %v", err) - } - - const numGoroutines = 5 // Reduce concurrency to avoid SQLite issues - done := make(chan error, numGoroutines) - - // Test concurrent reads - for i := 0; i < numGoroutines; i++ { - go func(id int) { - // Add small delay to reduce contention - time.Sleep(time.Duration(id) * time.Millisecond) - _, err := dgm.LoadGuild(ctx, 1) - done <- err - }(i) - } - - // Wait for all reads to complete - for i := 0; i < numGoroutines; i++ { - if err := <-done; err != nil { - t.Errorf("Concurrent read failed: %v", err) - } - } - - // Test concurrent member additions - for i := 0; i < numGoroutines; i++ { - go func(id int) { - memberSQL := `INSERT INTO guild_members (char_id, guild_id, account_id, name) VALUES (?, 1, 100, ?)` - err := db.Exec(memberSQL, 100+id, fmt.Sprintf("Player%d", id)) - done <- err - }(i) - } - - // Wait for all insertions - successCount := 0 - for i := 0; i < numGoroutines; i++ { - if err := <-done; err == nil { - successCount++ - } - } - - if successCount != numGoroutines { - t.Logf("Only %d out of %d concurrent insertions succeeded (some conflicts expected)", successCount, numGoroutines) - } -} - -// TestDatabaseGuildManager_TransactionRollback tests transaction rollback on errors -func TestDatabaseGuildManager_TransactionRollback(t *testing.T) { - db := createTestDB(t) - defer db.Close() - - dgm := NewDatabaseGuildManager(db) - ctx := context.Background() - - // Try to create a guild with invalid data that should trigger a rollback - invalidGuildData := GuildData{ - Name: "", // Empty name should cause validation error - Level: 1, - FormedDate: time.Now(), - } - - // This should fail but we test that it handles errors gracefully - _, err := dgm.CreateGuild(ctx, invalidGuildData) - - // We don't expect specific error behavior here since the implementation - // may or may not have validation, but we test that it doesn't crash - t.Logf("Create guild with invalid data returned: %v", err) - - // Verify no partial data was left behind - guilds, err := dgm.LoadGuilds(ctx) - if err != nil { - t.Fatalf("Failed to load guilds after rollback test: %v", err) - } - - // Should have no guilds (or any existing test data, but no new invalid guild) - t.Logf("Found %d guilds after invalid creation attempt", len(guilds)) } // BenchmarkDatabaseGuildManager_LoadGuilds benchmarks loading guilds func BenchmarkDatabaseGuildManager_LoadGuilds(b *testing.B) { - // Create test database in temp directory - tempDir := b.TempDir() - dbPath := filepath.Join(tempDir, "bench_guilds.db") - - db, err := database.Open(dbPath) - if err != nil { - b.Fatalf("Failed to create benchmark database: %v", err) - } - defer db.Close() + pool := createTestPool(&testing.T{}) + defer pool.Close() - if err := createGuildTables(db); err != nil { - b.Fatalf("Failed to create guild tables: %v", err) - } - - dgm := NewDatabaseGuildManager(db) + dgm := NewDatabaseGuildManager(pool) ctx := context.Background() - // Insert test guilds - for i := 0; i < 100; i++ { - insertSQL := `INSERT INTO guilds (id, name, level, xp, xp_needed, formed_on) VALUES (?, ?, 1, 111, 2521, ?)` - formedTime := time.Now().Unix() - if err := db.Exec(insertSQL, i+1, fmt.Sprintf("Guild%d", i+1), formedTime); err != nil { - b.Fatalf("Failed to insert test guild %d: %v", i+1, err) - } + // Insert test data + for i := 1; i <= 100; i++ { + execSQL(&testing.T{}, pool, + `INSERT INTO guilds (id, name, motd, level, xp, xp_needed, formed_on) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + i, fmt.Sprintf("Guild %d", i), "Welcome!", i%10+1, i*100, i*1000, time.Now().Unix()) } b.ResetTimer() - + for i := 0; i < b.N; i++ { - _, err := dgm.LoadGuilds(ctx) + guilds, err := dgm.LoadGuilds(ctx) if err != nil { - b.Fatalf("Failed to load guilds in benchmark: %v", err) + b.Fatalf("Failed to load guilds: %v", err) + } + if len(guilds) != 100 { + b.Errorf("Expected 100 guilds, got %d", len(guilds)) } } } -// BenchmarkDatabaseGuildManager_LoadGuildMembers benchmarks loading guild members -func BenchmarkDatabaseGuildManager_LoadGuildMembers(b *testing.B) { - // Create test database in temp directory - tempDir := b.TempDir() - dbPath := filepath.Join(tempDir, "bench_members.db") - - db, err := database.Open(dbPath) - if err != nil { - b.Fatalf("Failed to create benchmark database: %v", err) - } - defer db.Close() +// BenchmarkDatabaseGuildManager_SaveGuild benchmarks saving guild data +func BenchmarkDatabaseGuildManager_SaveGuild(b *testing.B) { + pool := createTestPool(&testing.T{}) + defer pool.Close() - if err := createGuildTables(db); err != nil { - b.Fatalf("Failed to create guild tables: %v", err) - } - - dgm := NewDatabaseGuildManager(db) + dgm := NewDatabaseGuildManager(pool) ctx := context.Background() - // Insert test guild - if err := db.Exec(`INSERT INTO guilds (id, name) VALUES (1, 'Benchmark Guild')`); err != nil { - b.Fatalf("Failed to insert benchmark guild: %v", err) - } - - // Insert test members - joinTime := time.Now().Unix() - for i := 0; i < 100; i++ { - memberSQL := `INSERT INTO guild_members (char_id, guild_id, account_id, name, rank, join_date, last_login_date) - VALUES (?, 1, 100, ?, ?, ?, ?)` - if err := db.Exec(memberSQL, i+1, fmt.Sprintf("Player%d", i+1), RankMember, joinTime, joinTime); err != nil { - b.Fatalf("Failed to insert test member %d: %v", i+1, err) - } + // Create a test guild + guild := &Guild{ + id: 1, + name: "Benchmark Guild", + motd: "Benchmark MOTD", + level: 50, + expCurrent: 50000, + expToNextLevel: 100000, + formedDate: time.Now(), } b.ResetTimer() - + for i := 0; i < b.N; i++ { - _, err := dgm.LoadGuildMembers(ctx, 1) + err := dgm.SaveGuild(ctx, guild) if err != nil { - b.Fatalf("Failed to load guild members in benchmark: %v", err) + b.Fatalf("Failed to save guild: %v", err) } } } -