HASH d9b1deb35ccd
DATE 2025-11-17
SUBJECT ookstats: derive player class from main spec
FILES 3 CHANGED
HASH d9b1deb35ccd
DATE 2025-11-17
SUBJECT ookstats: derive player class from
main spec
FILES 3 CHANGED
┌─ nix/pkgs/ookstats/src/internal/database/client.go ────────────────────────┐
│ diff --git a/nix/pkgs/ookstats/src/internal/database/client.go b/nix/pkgs/ookstats │
│ /src/internal/database/client.go │
│ index 2a94fa6..29591ff 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/database/client.go │
│ +++ b/nix/pkgs/ookstats/src/internal/database/client.go │
│ @@ -180,6 +180,7 @@ func EnsureCompleteSchema(db *sql.DB) error { │
│ name TEXT, │
│ realm_id INTEGER, │
│ main_spec_id INTEGER, │
│ + class_name TEXT, │
│ dungeons_completed INTEGER DEFAULT 0, │
│ total_runs INTEGER DEFAULT 0, │
│ combined_best_time INTEGER, │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...c/internal/database/client.go ───┐
│ diff --git a/nix/pkgs/ookstats/src/internal/ │
│ database/client.go b/nix/pkgs/ookstats/src/i │
│ nternal/database/client.go │
│ index 2a94fa6..29591ff 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/databas │
│ e/client.go │
│ +++ b/nix/pkgs/ookstats/src/internal/databas │
│ e/client.go │
│ @@ -180,6 +180,7 @@ func EnsureCompleteSchem │
│ a(db *sql.DB) error { │
│ name TEXT, │
│ realm_id INTEGER, │
│ main_spec_id INTEGER, │
│ + class_name TEXT, │
│ dungeons_completed INTEGER DEFAULT 0, │
│ total_runs INTEGER DEFAULT 0, │
│ combined_best_time INTEGER, │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/generator/search.go ───────────────────────┐
│ diff --git a/nix/pkgs/ookstats/src/internal/generator/search.go b/nix/pkgs/ookstat │
│ s/src/internal/generator/search.go │
│ index 6868f13..04dd0c1 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/generator/search.go │
│ +++ b/nix/pkgs/ookstats/src/internal/generator/search.go │
│ @@ -31,11 +31,10 @@ func GenerateSearchIndex(db *sql.DB, out string, shardSize int │
│ ) error { │
│ │
│ rows, err := db.Query(` │
│ SELECT p.id, p.name, r.region, r.slug, r.name, │
│ - COALESCE(pd.class_name, ''), pp.global_ranking, COALESCE(pp.global │
│ _ranking_bracket,'') │
│ + COALESCE(pp.class_name, ''), pp.global_ranking, COALESCE(pp.global │
│ _ranking_bracket,'') │
│ FROM players p │
│ JOIN player_profiles pp ON p.id = pp.player_id │
│ JOIN realms r ON p.realm_id = r.id │
│ - LEFT JOIN player_details pd ON p.id = pd.player_id │
│ WHERE pp.has_complete_coverage = 1 │
│ ORDER BY pp.global_ranking ASC, p.name ASC │
│ `) │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../internal/generator/search.go ───┐
│ diff --git a/nix/pkgs/ookstats/src/internal/ │
│ generator/search.go b/nix/pkgs/ookstats/src/ │
│ internal/generator/search.go │
│ index 6868f13..04dd0c1 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/generat │
│ or/search.go │
│ +++ b/nix/pkgs/ookstats/src/internal/generat │
│ or/search.go │
│ @@ -31,11 +31,10 @@ func GenerateSearchIndex │
│ (db *sql.DB, out string, shardSize int) erro │
│ r { │
│ │
│ rows, err := db.Query(` │
│ SELECT p.id, p.name, r.region, r.sl │
│ ug, r.name, │
│ - COALESCE(pd.class_name, ''), │
│ pp.global_ranking, COALESCE(pp.global_ranki │
│ ng_bracket,'') │
│ + COALESCE(pp.class_name, ''), │
│ pp.global_ranking, COALESCE(pp.global_ranki │
│ ng_bracket,'') │
│ FROM players p │
│ JOIN player_profiles pp ON p.id = p │
│ p.player_id │
│ JOIN realms r ON p.realm_id = r.id │
│ - LEFT JOIN player_details pd ON p.id │
│ = pd.player_id │
│ WHERE pp.has_complete_coverage = 1 │
│ ORDER BY pp.global_ranking ASC, p.n │
│ ame ASC │
│ `) │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/pipeline/process.go ───────────────────────┐
│ diff --git a/nix/pkgs/ookstats/src/internal/pipeline/process.go b/nix/pkgs/ookstat │
│ s/src/internal/pipeline/process.go │
│ index ca980a3..f31333d 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/pipeline/process.go │
│ +++ b/nix/pkgs/ookstats/src/internal/pipeline/process.go │
│ @@ -4,6 +4,8 @@ import ( │
│ "database/sql" │
│ "fmt" │
│ "time" │
│ + │
│ + "ookstats/internal/wow" │
│ ) │
│ │
│ // ProcessPlayersOptions contains options for player processing │
│ @@ -290,9 +292,69 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) { │
│ } │
│ fmt.Printf("[OK] Updated main specs\n") │
│ │
│ + // step 5: derive class from main spec │
│ + fmt.Printf("Step 5: Deriving class names from main specs...\n") │
│ + if err := deriveClassFromMainSpec(tx); err != nil { │
│ + return 0, fmt.Errorf("failed to derive class names: %w", err) │
│ + } │
│ + fmt.Printf("[OK] Updated class names\n") │
│ + │
│ return profilesCount, nil │
│ } │
│ │
│ +// deriveClassFromMainSpec derives class_name from main_spec_id for all player pr │
│ ofiles │
│ +func deriveClassFromMainSpec(tx *sql.Tx) error { │
│ + // Query all player profiles with a main_spec_id │
│ + rows, err := tx.Query(` │
│ + SELECT player_id, season_id, main_spec_id │
│ + FROM player_profiles │
│ + WHERE main_spec_id IS NOT NULL │
│ + `) │
│ + if err != nil { │
│ + return fmt.Errorf("failed to query player profiles: %w", err) │
│ + } │
│ + defer rows.Close() │
│ + │
│ + // Prepare update statement │
│ + updateStmt, err := tx.Prepare(` │
│ + UPDATE player_profiles │
│ + SET class_name = ? │
│ + WHERE player_id = ? AND season_id = ? │
│ + `) │
│ + if err != nil { │
│ + return fmt.Errorf("failed to prepare update statement: %w", err) │
│ + } │
│ + defer updateStmt.Close() │
│ + │
│ + updatedCount := 0 │
│ + for rows.Next() { │
│ + var playerID, seasonID, mainSpecID int │
│ + if err := rows.Scan(&playerID, &seasonID, &mainSpecID); err != nil { │
│ + return fmt.Errorf("failed to scan row: %w", err) │
│ + } │
│ + │
│ + // Use wow package to get class from spec │
│ + className, _, ok := wow.GetClassAndSpec(mainSpecID) │
│ + if !ok { │
│ + // If spec not found, skip this player (leave class_name NULL) │
│ + continue │
│ + } │
│ + │
│ + // Update player_profiles with derived class │
│ + if _, err := updateStmt.Exec(className, playerID, seasonID); err != nil { │
│ + return fmt.Errorf("failed to update class for player %d season %d: %w", player │
│ ID, seasonID, err) │
│ + } │
│ + updatedCount++ │
│ + } │
│ + │
│ + if err := rows.Err(); err != nil { │
│ + return fmt.Errorf("error iterating rows: %w", err) │
│ + } │
│ + │
│ + fmt.Printf(" Derived class for %d player profiles\n", updatedCount) │
│ + return nil │
│ +} │
│ + │
│ // computePlayerRankings computes rankings for players with complete coverage per │
│ season │
│ func computePlayerRankings(tx *sql.Tx) (int, error) { │
│ fmt.Printf("Computing player rankings per season...\n") │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../internal/pipeline/process.go ───┐
│ diff --git a/nix/pkgs/ookstats/src/internal/ │
│ pipeline/process.go b/nix/pkgs/ookstats/src/ │
│ internal/pipeline/process.go │
│ index ca980a3..f31333d 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/pipelin │
│ e/process.go │
│ +++ b/nix/pkgs/ookstats/src/internal/pipelin │
│ e/process.go │
│ @@ -4,6 +4,8 @@ import ( │
│ "database/sql" │
│ "fmt" │
│ "time" │
│ + │
│ + "ookstats/internal/wow" │
│ ) │
│ │
│ // ProcessPlayersOptions contains options f │
│ or player processing │
│ @@ -290,9 +292,69 @@ func createPlayerAggreg │
│ ations(tx *sql.Tx) (int, error) { │
│ } │
│ fmt.Printf("[OK] Updated main specs\n") │
│ │
│ + // step 5: derive class from main spec │
│ + fmt.Printf("Step 5: Deriving class names f │
│ rom main specs...\n") │
│ + if err := deriveClassFromMainSpec(tx); err │
│ != nil { │
│ + return 0, fmt.Errorf("failed to derive cl │
│ ass names: %w", err) │
│ + } │
│ + fmt.Printf("[OK] Updated class names\n") │
│ + │
│ return profilesCount, nil │
│ } │
│ │
│ +// deriveClassFromMainSpec derives class_na │
│ me from main_spec_id for all player profiles │
│ +func deriveClassFromMainSpec(tx *sql.Tx) er │
│ ror { │
│ + // Query all player profiles with a main_s │
│ pec_id │
│ + rows, err := tx.Query(` │
│ + SELECT player_id, season_id, main_spec_id │
│ + FROM player_profiles │
│ + WHERE main_spec_id IS NOT NULL │
│ + `) │
│ + if err != nil { │
│ + return fmt.Errorf("failed to query player │
│ profiles: %w", err) │
│ + } │
│ + defer rows.Close() │
│ + │
│ + // Prepare update statement │
│ + updateStmt, err := tx.Prepare(` │
│ + UPDATE player_profiles │
│ + SET class_name = ? │
│ + WHERE player_id = ? AND season_id = ? │
│ + `) │
│ + if err != nil { │
│ + return fmt.Errorf("failed to prepare upda │
│ te statement: %w", err) │
│ + } │
│ + defer updateStmt.Close() │
│ + │
│ + updatedCount := 0 │
│ + for rows.Next() { │
│ + var playerID, seasonID, mainSpecID int │
│ + if err := rows.Scan(&playerID, &seasonID, │
│ &mainSpecID); err != nil { │
│ + return fmt.Errorf("failed to scan row: % │
│ w", err) │
│ + } │
│ + │
│ + // Use wow package to get class from spec │
│ + className, _, ok := wow.GetClassAndSpec(m │
│ ainSpecID) │
│ + if !ok { │
│ + // If spec not found, skip this player ( │
│ leave class_name NULL) │
│ + continue │
│ + } │
│ + │
│ + // Update player_profiles with derived cl │
│ ass │
│ + if _, err := updateStmt.Exec(className, p │
│ layerID, seasonID); err != nil { │
│ + return fmt.Errorf("failed to update clas │
│ s for player %d season %d: %w", playerID, se │
│ asonID, err) │
│ + } │
│ + updatedCount++ │
│ + } │
│ + │
│ + if err := rows.Err(); err != nil { │
│ + return fmt.Errorf("error iterating rows: │
│ %w", err) │
│ + } │
│ + │
│ + fmt.Printf(" Derived class for %d player │
│ profiles\n", updatedCount) │
│ + return nil │
│ +} │
│ + │
│ // computePlayerRankings computes rankings │
│ for players with complete coverage per seaso │
│ n │
│ func computePlayerRankings(tx *sql.Tx) (int │
│ , error) { │
│ fmt.Printf("Computing player rankings per │
│ season...\n") │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET