package guilds import ( "context" "fmt" "time" "eq2emu/internal/database" ) // DatabaseGuildManager implements GuildDatabase interface using the existing database wrapper type DatabaseGuildManager struct { db *database.DB } // NewDatabaseGuildManager creates a new database guild manager func NewDatabaseGuildManager(db *database.DB) *DatabaseGuildManager { return &DatabaseGuildManager{ db: db, } } // 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`" rows, err := dgm.db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("failed to query guilds: %w", err) } defer rows.Close() var guilds []GuildData for rows.Next() { var guild GuildData var motd *string var formedOnTimestamp int64 err := rows.Scan( &guild.ID, &guild.Name, &motd, &guild.Level, &guild.EXPCurrent, &guild.EXPToNextLevel, &formedOnTimestamp, ) if err != nil { return nil, fmt.Errorf("failed to scan guild row: %w", err) } // Handle nullable MOTD field if motd != nil { guild.MOTD = *motd } // Convert timestamp to time guild.FormedDate = time.Unix(formedOnTimestamp, 0) guilds = append(guilds, guild) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating guild rows: %w", err) } return guilds, nil } // 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` = ?" var guild GuildData var motd *string var formedOnTimestamp int64 err := dgm.db.QueryRowContext(ctx, query, guildID).Scan( &guild.ID, &guild.Name, &motd, &guild.Level, &guild.EXPCurrent, &guild.EXPToNextLevel, &formedOnTimestamp, ) if err != nil { return nil, fmt.Errorf("failed to load guild %d: %w", guildID, err) } // Handle nullable MOTD field if motd != nil { guild.MOTD = *motd } // Convert timestamp to time guild.FormedDate = time.Unix(formedOnTimestamp, 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 = ?` rows, err := dgm.db.QueryContext(ctx, query, guildID) if err != nil { return nil, fmt.Errorf("failed to query guild members for guild %d: %w", guildID, err) } defer rows.Close() var members []GuildMemberData for rows.Next() { var member GuildMemberData var joinDateTimestamp int64 var lastLoginTimestamp int64 var note, officerNote, recruiterDesc *string var pictureData []byte err := rows.Scan( &member.CharacterID, &member.GuildID, &member.AccountID, &member.RecruiterID, &member.Name, &member.GuildStatus, &member.Points, &member.AdventureClass, &member.AdventureLevel, &member.TradeskillClass, &member.TradeskillLevel, &member.Rank, &member.MemberFlags, &member.Zone, &joinDateTimestamp, &lastLoginTimestamp, ¬e, &officerNote, &recruiterDesc, &pictureData, &member.RecruitingShowAdventureClass, ) if err != nil { return nil, fmt.Errorf("failed to scan guild member row: %w", err) } // Handle nullable fields if note != nil { member.Note = *note } if officerNote != nil { member.OfficerNote = *officerNote } if recruiterDesc != nil { member.RecruiterDescription = *recruiterDesc } member.RecruiterPictureData = pictureData // Convert timestamps to time member.JoinDate = time.Unix(joinDateTimestamp, 0) member.LastLoginDate = time.Unix(lastLoginTimestamp, 0) members = append(members, member) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating guild member rows: %w", 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 ?` rows, err := dgm.db.QueryContext(ctx, query, guildID, MaxEvents) if err != nil { return nil, fmt.Errorf("failed to query guild events for guild %d: %w", guildID, err) } defer rows.Close() var events []GuildEventData for rows.Next() { var event GuildEventData var dateTimestamp int64 err := rows.Scan( &event.EventID, &event.GuildID, &dateTimestamp, &event.Type, &event.Description, &event.Locked, ) if err != nil { return nil, fmt.Errorf("failed to scan guild event row: %w", err) } // Convert timestamp to time event.Date = time.Unix(dateTimestamp, 0) events = append(events, event) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating guild event rows: %w", err) } return events, nil } // 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 = ?" rows, err := dgm.db.QueryContext(ctx, query, guildID) if err != nil { return nil, fmt.Errorf("failed to query guild ranks for guild %d: %w", guildID, err) } defer rows.Close() var ranks []GuildRankData for rows.Next() { var rank GuildRankData err := rows.Scan( &rank.GuildID, &rank.Rank, &rank.Name, ) if err != nil { return nil, fmt.Errorf("failed to scan guild rank row: %w", err) } ranks = append(ranks, rank) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating guild rank rows: %w", err) } return ranks, nil } // 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 = ?" rows, err := dgm.db.QueryContext(ctx, query, guildID) if err != nil { return nil, fmt.Errorf("failed to query guild permissions for guild %d: %w", guildID, err) } defer rows.Close() var permissions []GuildPermissionData for rows.Next() { var permission GuildPermissionData err := rows.Scan( &permission.GuildID, &permission.Rank, &permission.Permission, &permission.Value, ) if err != nil { return nil, fmt.Errorf("failed to scan guild permission row: %w", err) } permissions = append(permissions, permission) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating guild permission rows: %w", err) } return permissions, nil } // 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 = ?" rows, err := dgm.db.QueryContext(ctx, query, guildID) if err != nil { return nil, fmt.Errorf("failed to query guild event filters for guild %d: %w", guildID, err) } defer rows.Close() var filters []GuildEventFilterData for rows.Next() { var filter GuildEventFilterData err := rows.Scan( &filter.GuildID, &filter.EventID, &filter.Category, &filter.Value, ) if err != nil { return nil, fmt.Errorf("failed to scan guild event filter row: %w", err) } filters = append(filters, filter) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating guild event filter rows: %w", err) } return filters, nil } // 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 = ?" rows, err := dgm.db.QueryContext(ctx, query, guildID) if err != nil { return nil, fmt.Errorf("failed to query guild recruiting for guild %d: %w", guildID, err) } defer rows.Close() var recruiting []GuildRecruitingData for rows.Next() { var recruit GuildRecruitingData err := rows.Scan( &recruit.GuildID, &recruit.Flag, &recruit.Value, ) if err != nil { return nil, fmt.Errorf("failed to scan guild recruiting row: %w", err) } recruiting = append(recruiting, recruit) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating guild recruiting rows: %w", err) } return recruiting, nil } // 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 ?` rows, err := dgm.db.QueryContext(ctx, query, characterID, MaxPointHistory) if err != nil { return nil, fmt.Errorf("failed to query point history for character %d: %w", characterID, err) } defer rows.Close() var history []PointHistoryData for rows.Next() { var entry PointHistoryData var dateTimestamp int64 err := rows.Scan( &entry.CharacterID, &dateTimestamp, &entry.ModifiedBy, &entry.Comment, &entry.Points, ) if err != nil { return nil, fmt.Errorf("failed to scan point history row: %w", err) } // Convert timestamp to time entry.Date = time.Unix(dateTimestamp, 0) history = append(history, entry) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating point history rows: %w", err) } return history, nil } // 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 (?, ?, ?, ?, ?, ?, ?)` guildInfo := guild.GetGuildInfo() formedTimestamp := guild.GetFormedDate().Unix() _, err := dgm.db.ExecContext(ctx, query, guild.GetID(), guild.GetName(), guild.GetMOTD(), guild.GetLevel(), guild.GetEXPCurrent(), guild.GetEXPToNextLevel(), formedTimestamp, ) 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 } // Use a transaction for atomic updates tx, err := dgm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing members for this guild _, err = tx.ExecContext(ctx, "DELETE FROM guild_members WHERE guild_id = ?", guildID) if err != nil { return fmt.Errorf("failed to delete existing guild members: %w", err) } // 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() _, err = tx.ExecContext(ctx, 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, ) if err != nil { return fmt.Errorf("failed to insert guild member %d: %w", member.GetCharacterID(), err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", 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 } query := `INSERT OR REPLACE INTO guild_events (event_id, guild_id, date, type, description, locked) VALUES (?, ?, ?, ?, ?, ?)` for _, event := range events { if !event.SaveNeeded { continue } dateTimestamp := event.Date.Unix() _, err := dgm.db.ExecContext(ctx, query, event.EventID, guildID, dateTimestamp, event.Type, event.Description, event.Locked, ) 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 { // Use a transaction for atomic updates tx, err := dgm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing ranks for this guild _, err = tx.ExecContext(ctx, "DELETE FROM guild_ranks WHERE guild_id = ?", guildID) if err != nil { return fmt.Errorf("failed to delete existing guild ranks: %w", err) } // Insert all ranks insertQuery := "INSERT INTO guild_ranks (guild_id, rank, name) VALUES (?, ?, ?)" for rank, name := range ranks { // Only save non-default rank names if defaultName, exists := DefaultRankNames[rank]; !exists || name != defaultName { _, err = tx.ExecContext(ctx, insertQuery, guildID, rank, name) if err != nil { return fmt.Errorf("failed to insert guild rank %d: %w", rank, err) } } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } 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 tx, err := dgm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing permissions for this guild _, err = tx.ExecContext(ctx, "DELETE FROM guild_permissions WHERE guild_id = ?", guildID) if err != nil { return fmt.Errorf("failed to delete existing guild permissions: %w", err) } // Insert all permissions insertQuery := "INSERT INTO guild_permissions (guild_id, rank, permission, value) VALUES (?, ?, ?, ?)" for rank, rankPermissions := range permissions { for permission, value := range rankPermissions { _, err = tx.ExecContext(ctx, insertQuery, guildID, rank, permission, value) if err != nil { return fmt.Errorf("failed to insert guild permission %d/%d: %w", rank, permission, err) } } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } 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 tx, err := dgm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing filters for this guild _, err = tx.ExecContext(ctx, "DELETE FROM guild_event_filters WHERE guild_id = ?", guildID) if err != nil { return fmt.Errorf("failed to delete existing guild event filters: %w", err) } // Insert all filters insertQuery := "INSERT INTO guild_event_filters (guild_id, event_id, category, value) VALUES (?, ?, ?, ?)" for eventID, eventFilters := range filters { for category, value := range eventFilters { _, err = tx.ExecContext(ctx, insertQuery, guildID, eventID, category, value) if err != nil { return fmt.Errorf("failed to insert guild event filter %d/%d: %w", eventID, category, err) } } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } 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 tx, err := dgm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing recruiting settings for this guild _, err = tx.ExecContext(ctx, "DELETE FROM guild_recruiting WHERE guild_id = ?", guildID) if err != nil { return fmt.Errorf("failed to delete existing guild recruiting: %w", err) } // Insert recruiting flags insertQuery := "INSERT INTO guild_recruiting (guild_id, flag, value) VALUES (?, ?, ?)" for flag, value := range flags { _, err = tx.ExecContext(ctx, insertQuery, guildID, flag, value) 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 { _, err = tx.ExecContext(ctx, insertQuery, guildID, -tag-1, value) // Negative to distinguish from flags if err != nil { return fmt.Errorf("failed to insert guild recruiting desc tag %d: %w", tag, err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", 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 } // Use a transaction for atomic updates tx, err := dgm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing history for this character _, err = tx.ExecContext(ctx, "DELETE FROM guild_point_history WHERE char_id = ?", characterID) if 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() _, err = tx.ExecContext(ctx, insertQuery, characterID, dateTimestamp, entry.ModifiedBy, entry.Comment, entry.Points, ) if err != nil { return fmt.Errorf("failed to insert point history entry: %w", err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %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 = ?" var guildID int32 err := dgm.db.QueryRowContext(ctx, query, characterID).Scan(&guildID) if err != nil { return 0, fmt.Errorf("failed to get guild ID for character %d: %w", characterID, err) } 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() result, err := dgm.db.ExecContext(ctx, query, guildData.Name, guildData.MOTD, guildData.Level, guildData.EXPCurrent, guildData.EXPToNextLevel, formedTimestamp, ) if err != nil { return 0, fmt.Errorf("failed to create guild: %w", err) } id, err := result.LastInsertId() if err != nil { return 0, fmt.Errorf("failed to get new guild ID: %w", 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 tx, err := dgm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // 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: need to join with guild_members to get char_ids query = fmt.Sprintf("DELETE ph FROM %s ph JOIN guild_members gm ON ph.char_id = gm.char_id WHERE gm.guild_id = ?", table) } else { query = fmt.Sprintf("DELETE FROM %s WHERE guild_id = ?", table) } _, err = tx.ExecContext(ctx, query, guildID) if err != nil { return fmt.Errorf("failed to delete from %s: %w", table, err) } } // Finally delete the guild itself _, err = tx.ExecContext(ctx, "DELETE FROM guilds WHERE id = ?", guildID) if err != nil { return fmt.Errorf("failed to delete guild: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %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" var nextID int32 err := dgm.db.QueryRowContext(ctx, query).Scan(&nextID) if err != nil { return 0, fmt.Errorf("failed to get next guild ID: %w", err) } 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 = ?" var nextID int64 err := dgm.db.QueryRowContext(ctx, query, guildID).Scan(&nextID) if err != nil { return 0, fmt.Errorf("failed to get next event ID for guild %d: %w", guildID, err) } 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 )`, } for i, query := range queries { _, err := dgm.db.ExecContext(ctx, query) if err != nil { return fmt.Errorf("failed to create guild table %d: %w", i+1, err) } } // 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 i, query := range indexes { _, err := dgm.db.ExecContext(ctx, query) if err != nil { return fmt.Errorf("failed to create guild index %d: %w", i+1, err) } } return nil }