package guilds import ( "context" "fmt" "time" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // DatabaseGuildManager implements GuildDatabase interface using zombiezen/go-sqlite type DatabaseGuildManager struct { pool *sqlitex.Pool } // NewDatabaseGuildManager creates a new database guild manager func NewDatabaseGuildManager(pool *sqlitex.Pool) *DatabaseGuildManager { return &DatabaseGuildManager{ pool: pool, } } // LoadGuilds retrieves all guilds from database func (dgm *DatabaseGuildManager) LoadGuilds(ctx context.Context) ([]GuildData, error) { 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 for { hasRow, err := stmt.Step() if err != nil { return nil, fmt.Errorf("failed to query guilds: %w", err) } 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 guilds, nil } // LoadGuild retrieves a specific guild from database func (dgm *DatabaseGuildManager) LoadGuild(ctx context.Context, guildID int32) (*GuildData, error) { 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` 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 !hasRow { return nil, fmt.Errorf("guild %d not found", guildID) } 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) return &guild, nil } // LoadGuildMembers retrieves all members for a guild func (dgm *DatabaseGuildManager) LoadGuildMembers(ctx context.Context, guildID int32) ([]GuildMemberData, error) { 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)) var members []GuildMemberData 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(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 stmt.ColumnType(17) != sqlite.TypeNull { member.OfficerNote = stmt.ColumnText(17) } if stmt.ColumnType(18) != sqlite.TypeNull { member.RecruiterDescription = stmt.ColumnText(18) } 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 members, nil } // LoadGuildEvents retrieves events for a guild func (dgm *DatabaseGuildManager) LoadGuildEvents(ctx context.Context, guildID int32) ([]GuildEventData, error) { 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 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 = 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 events, nil } // LoadGuildRanks retrieves custom rank names for a guild func (dgm *DatabaseGuildManager) LoadGuildRanks(ctx context.Context, guildID int32) ([]GuildRankData, error) { 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 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(stmt.ColumnInt64(0)) rank.Rank = int8(stmt.ColumnInt64(1)) rank.Name = stmt.ColumnText(2) ranks = append(ranks, rank) } return ranks, nil } // LoadGuildPermissions retrieves permissions for a guild func (dgm *DatabaseGuildManager) LoadGuildPermissions(ctx context.Context, guildID int32) ([]GuildPermissionData, error) { 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 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(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 permissions, nil } // LoadGuildEventFilters retrieves event filters for a guild func (dgm *DatabaseGuildManager) LoadGuildEventFilters(ctx context.Context, guildID int32) ([]GuildEventFilterData, error) { 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 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(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 filters, nil } // LoadGuildRecruiting retrieves recruiting settings for a guild func (dgm *DatabaseGuildManager) LoadGuildRecruiting(ctx context.Context, guildID int32) ([]GuildRecruitingData, error) { 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 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(stmt.ColumnInt64(0)) recruit.Flag = int8(stmt.ColumnInt64(1)) recruit.Value = int8(stmt.ColumnInt64(2)) recruiting = append(recruiting, recruit) } return recruiting, nil } // LoadPointHistory retrieves point history for a member func (dgm *DatabaseGuildManager) LoadPointHistory(ctx context.Context, characterID int32) ([]PointHistoryData, error) { 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 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(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 history, nil } // SaveGuild saves guild basic data func (dgm *DatabaseGuildManager) SaveGuild(ctx context.Context, guild *Guild) 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) 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() 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) } return nil } // SaveGuildMembers saves all guild members func (dgm *DatabaseGuildManager) SaveGuildMembers(ctx context.Context, guildID int32, members []*GuildMember) error { if len(members) == 0 { return nil } 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)) _, err = insertStmt.Step() if err != nil { return fmt.Errorf("failed to insert guild member %d: %w", member.GetCharacterID(), err) } } return nil } // SaveGuildEvents saves guild events func (dgm *DatabaseGuildManager) SaveGuildEvents(ctx context.Context, guildID int32, events []GuildEvent) error { if len(events) == 0 { return nil } 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 { continue } dateTimestamp := event.Date.Unix() 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) } } return nil } // SaveGuildRanks saves guild rank names func (dgm *DatabaseGuildManager) SaveGuildRanks(ctx context.Context, guildID int32, ranks map[int8]string) 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 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 } // SaveGuildPermissions saves guild permissions func (dgm *DatabaseGuildManager) SaveGuildPermissions(ctx context.Context, guildID int32, permissions map[int8]map[int8]int8) 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 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 } // SaveGuildEventFilters saves guild event filters func (dgm *DatabaseGuildManager) SaveGuildEventFilters(ctx context.Context, guildID int32, filters map[int8]map[int8]int8) 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 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 } // SaveGuildRecruiting saves guild recruiting settings func (dgm *DatabaseGuildManager) SaveGuildRecruiting(ctx context.Context, guildID int32, flags, descTags map[int8]int8) 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 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 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) } } // 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 func (dgm *DatabaseGuildManager) SavePointHistory(ctx context.Context, characterID int32, history []PointHistory) error { if len(history) == 0 { return nil } 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 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 fmt.Errorf("failed to delete existing point history: %w", err) } // Insert all history entries insertStmt := conn.Prep("INSERT INTO guild_point_history (char_id, date, modified_by, comment, points) VALUES (?, ?, ?, ?, ?)") defer insertStmt.Finalize() for _, entry := range history { dateTimestamp := entry.Date.Unix() 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 }