OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
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
 

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,

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,
 

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
     `)

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
     `)
 

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")

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