HASH f3faba3ee50f
DATE 2025-11-17
SUBJECT ookstats: fetch based on seasons
FILES 10 CHANGED
HASH f3faba3ee50f
DATE 2025-11-17
SUBJECT ookstats: fetch based on seasons
FILES 10 CHANGED
┌─ nix/pkgs/ookstats/src/cmd/analyze.go ─────────────────────────────────────┐│ diff --git a/nix/pkgs/ookstats/src/cmd/analyze.go b/nix/pkgs/ookstats/src/cmd/anal ││ yze.go ││ index 551b305..6a30341 100644 ││ --- a/nix/pkgs/ookstats/src/cmd/analyze.go ││ +++ b/nix/pkgs/ookstats/src/cmd/analyze.go ││ @@ -2,6 +2,7 @@ package cmd ││ ││ import ( ││ "context" ││ + "database/sql" ││ "encoding/json" ││ "errors" ││ "fmt" ││ @@ -14,6 +15,7 @@ import ( ││ ││ "github.com/spf13/cobra" ││ "ookstats/internal/blizzard" ││ + "ookstats/internal/database" ││ ) ││ ││ // analyzeCmd performs a quick multi-period sweep to summarize latest runs per re ││ alm ││ @@ -29,6 +31,13 @@ var analyzeCmd = &cobra.Command{ ││ rng, _ := cmd.Flags().GetString("range") ││ concurrency, _ := cmd.Flags().GetInt("concurrency") ││ ││ + // Connect to database (needed for period fetching from seasons) ││ + db, err := database.Connect() ││ + if err != nil { ││ + return fmt.Errorf("db connect: %w", err) ││ + } ││ + defer db.Close() ││ + ││ client, err := blizzard.NewClient() ││ if err != nil { ││ return fmt.Errorf("blizzard client: %w", err) ││ @@ -71,7 +80,7 @@ var analyzeCmd = &cobra.Command{ ││ periodsSpec = strings.Join(periodParts, ",") ││ } ││ ││ - return runAnalyze(client, realms, dungeons, periodsSpec, outPath, statusD ││ ir) ││ + return runAnalyze(db, client, realms, dungeons, periodsSpec, outPath, sta ││ tusDir) ││ }, ││ } ││ ││ @@ -85,7 +94,8 @@ func init() { ││ analyzeCmd.Flags().Int("concurrency", 20, "Max concurrent API requests") ││ } ││ ││ -func runAnalyze(client *blizzard.Client, realms map[string]blizzard.RealmInfo, du ││ ngeons []blizzard.DungeonInfo, periodsSpec string, outPath, statusDir string) erro ││ r { ││ +func runAnalyze(db *sql.DB, client *blizzard.Client, realms map[string]blizzard.R ││ ealmInfo, dungeons []blizzard.DungeonInfo, periodsSpec string, outPath, statusDir ││ string) error { ││ + dbService := database.NewDatabaseService(db) ││ fmt.Printf("Analyze: %d realms, %d dungeons\n", len(realms), len(dungeons ││ )) ││ ││ type latest struct{ ││ @@ -153,13 +163,27 @@ func runAnalyze(client *blizzard.Client, realms map[string]b ││ lizzard.RealmInfo, d ││ } ││ fmt.Printf("Using user-specified periods: %v (%d periods)\n", per ││ iods, len(periods)) ││ } else { ││ - // Fetch periods dynamically from Blizzard API for this region ││ - fmt.Printf("Fetching period list dynamically from Blizzard API fo ││ r %s...\n", strings.ToUpper(region)) ││ - periods, err = client.GetDynamicPeriodList(region) ││ + // Fetch periods from database (populated by season sync) ││ + fmt.Printf("Fetching period list from database for %s...\n", stri ││ ngs.ToUpper(region)) ││ + periodInts, err := dbService.GetPeriodsForRegion(region) ││ if err != nil { ││ - fmt.Printf("Failed to fetch period list for %s: %v - skipping ││ region\n", strings.ToUpper(region), err) ││ + fmt.Printf("Failed to fetch periods from database for %s: %v ││ - skipping region\n", strings.ToUpper(region), err) ││ continue ││ } ││ + ││ + // Convert []int to []string for compatibility ││ + periods = make([]string, len(periodInts)) ││ + for i, p := range periodInts { ││ + periods[i] = fmt.Sprintf("%d", p) ││ + } ││ + ││ + if len(periods) == 0 { ││ + fmt.Printf("No periods found in database for %s (run season s ││ ync first) - skipping region\n", strings.ToUpper(region)) ││ + continue ││ + } ││ + ││ + fmt.Printf("[OK] Fetched %d periods from database for %s (newest: ││ %s, oldest: %s)\n", ││ + len(periods), strings.ToUpper(region), periods[0], periods[le ││ n(periods)-1]) ││ } ││ ││ if len(periods) == 0 { │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...s/ookstats/src/cmd/analyze.go ───┐│ diff --git a/nix/pkgs/ookstats/src/cmd/analy ││ ze.go b/nix/pkgs/ookstats/src/cmd/analyze.go ││ index 551b305..6a30341 100644 ││ --- a/nix/pkgs/ookstats/src/cmd/analyze.go ││ +++ b/nix/pkgs/ookstats/src/cmd/analyze.go ││ @@ -2,6 +2,7 @@ package cmd ││ ││ import ( ││ "context" ││ + "database/sql" ││ "encoding/json" ││ "errors" ││ "fmt" ││ @@ -14,6 +15,7 @@ import ( ││ ││ "github.com/spf13/cobra" ││ "ookstats/internal/blizzard" ││ + "ookstats/internal/database" ││ ) ││ ││ // analyzeCmd performs a quick multi-period ││ sweep to summarize latest runs per realm ││ @@ -29,6 +31,13 @@ var analyzeCmd = &cobra.C ││ ommand{ ││ rng, _ := cmd.Flags().GetString("ra ││ nge") ││ concurrency, _ := cmd.Flags().GetIn ││ t("concurrency") ││ ││ + // Connect to database (needed for ││ period fetching from seasons) ││ + db, err := database.Connect() ││ + if err != nil { ││ + return fmt.Errorf("db connect: ││ %w", err) ││ + } ││ + defer db.Close() ││ + ││ client, err := blizzard.NewClient() ││ if err != nil { ││ return fmt.Errorf("blizzard cli ││ ent: %w", err) ││ @@ -71,7 +80,7 @@ var analyzeCmd = &cobra.Co ││ mmand{ ││ periodsSpec = strings.Join(peri ││ odParts, ",") ││ } ││ ││ - return runAnalyze(client, realms, d ││ ungeons, periodsSpec, outPath, statusDir) ││ + return runAnalyze(db, client, realm ││ s, dungeons, periodsSpec, outPath, statusDir ││ ) ││ }, ││ } ││ ││ @@ -85,7 +94,8 @@ func init() { ││ analyzeCmd.Flags().Int("concurrency", 2 ││ 0, "Max concurrent API requests") ││ } ││ ││ -func runAnalyze(client *blizzard.Client, re ││ alms map[string]blizzard.RealmInfo, dungeons ││ []blizzard.DungeonInfo, periodsSpec string, ││ outPath, statusDir string) error { ││ +func runAnalyze(db *sql.DB, client *blizzar ││ d.Client, realms map[string]blizzard.RealmIn ││ fo, dungeons []blizzard.DungeonInfo, periods ││ Spec string, outPath, statusDir string) erro ││ r { ││ + dbService := database.NewDatabaseSe ││ rvice(db) ││ fmt.Printf("Analyze: %d realms, %d ││ dungeons\n", len(realms), len(dungeons)) ││ ││ type latest struct{ ││ @@ -153,13 +163,27 @@ func runAnalyze(client ││ *blizzard.Client, realms map[string]blizzar ││ d.RealmInfo, d ││ } ││ fmt.Printf("Using user-spec ││ ified periods: %v (%d periods)\n", periods, ││ len(periods)) ││ } else { ││ - // Fetch periods dynamicall ││ y from Blizzard API for this region ││ - fmt.Printf("Fetching period ││ list dynamically from Blizzard API for %s.. ││ .\n", strings.ToUpper(region)) ││ - periods, err = client.GetDy ││ namicPeriodList(region) ││ + // Fetch periods from datab ││ ase (populated by season sync) ││ + fmt.Printf("Fetching period ││ list from database for %s...\n", strings.To ││ Upper(region)) ││ + periodInts, err := dbServic ││ e.GetPeriodsForRegion(region) ││ if err != nil { ││ - fmt.Printf("Failed to f ││ etch period list for %s: %v - skipping regio ││ n\n", strings.ToUpper(region), err) ││ + fmt.Printf("Failed to f ││ etch periods from database for %s: %v - skip ││ ping region\n", strings.ToUpper(region), err ││ ) ││ continue ││ } ││ + ││ + // Convert []int to []strin ││ g for compatibility ││ + periods = make([]string, le ││ n(periodInts)) ││ + for i, p := range periodInt ││ s { ││ + periods[i] = fmt.Sprint ││ f("%d", p) ││ + } ││ + ││ + if len(periods) == 0 { ││ + fmt.Printf("No periods ││ found in database for %s (run season sync fi ││ rst) - skipping region\n", strings.ToUpper(r ││ egion)) ││ + continue ││ + } ││ + ││ + fmt.Printf("[OK] Fetched %d ││ periods from database for %s (newest: %s, o ││ ldest: %s)\n", ││ + len(periods), strings.T ││ oUpper(region), periods[0], periods[len(peri ││ ods)-1]) ││ } ││ ││ if len(periods) == 0 { │└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/cmd/build.go ───────────────────────────────────────┐│ diff --git a/nix/pkgs/ookstats/src/cmd/build.go b/nix/pkgs/ookstats/src/cmd/build. ││ go ││ index 9bee107..eb19768 100644 ││ --- a/nix/pkgs/ookstats/src/cmd/build.go ││ +++ b/nix/pkgs/ookstats/src/cmd/build.go ││ @@ -177,7 +177,7 @@ var buildCmd = &cobra.Command{ ││ // Get realms and dungeons for analyze ││ _, dungeons := blizzard.GetHardcodedPeriodAndDungeons() ││ allRealms := blizzard.GetAllRealms() ││ - if err := runAnalyze(client, allRealms, dungeons, periodsCSV, outPath, st ││ atusDir); err != nil { ││ + if err := runAnalyze(db, client, allRealms, dungeons, periodsCSV, outPath ││ , statusDir); err != nil { ││ return fmt.Errorf("analyze status: %w", err) ││ } ││ ││ @@ -308,76 +308,88 @@ func summarizeBuild(db *sql.DB, outParent string) { ││ } ││ } ││ ││ -// syncSeasons syncs season metadata from Blizzard API ││ +// syncSeasons syncs season metadata from Blizzard API for all regions ││ func syncSeasons(db *sql.DB, client *blizzard.Client, regionsCSV string) error { ││ dbService := database.NewDatabaseService(db) ││ ││ - // Only need one region since seasons are global ││ - region := "us" ││ + // Parse regions from CSV (seasons and periods are region-specific) ││ + var regions []string ││ if strings.TrimSpace(regionsCSV) != "" { ││ - parts := strings.Split(regionsCSV, ",") ││ - if len(parts) > 0 { ││ - region = strings.TrimSpace(parts[0]) ││ + for _, r := range strings.Split(regionsCSV, ",") { ││ + if trimmed := strings.TrimSpace(r); trimmed != "" { ││ + regions = append(regions, trimmed) ││ + } ││ } ││ } ││ ││ - fmt.Printf("Fetching season metadata from %s...\n", strings.ToUpper(region)) ││ - ││ - // Fetch season index ││ - seasonIndex, err := client.FetchSeasonIndex(region) ││ - if err != nil { ││ - return fmt.Errorf("failed to fetch season index: %w", err) ││ - } ││ - ││ - if len(seasonIndex.Seasons) == 0 { ││ - fmt.Println("No seasons found in API") ││ - return nil ││ + // Default to all regions if none specified ││ + if len(regions) == 0 { ││ + regions = []string{"us", "eu", "kr", "tw"} ││ } ││ ││ - fmt.Printf("Found %d seasons\n", len(seasonIndex.Seasons)) ││ + fmt.Printf("Fetching season metadata from %d regions...\n", len(regions)) ││ ││ - // Process each season ││ - for _, seasonRef := range seasonIndex.Seasons { ││ - seasonID := seasonRef.ID ││ - fmt.Printf(" Season %d: ", seasonID) ││ + // Process each region ││ + for _, region := range regions { ││ + fmt.Printf("\n=== Region: %s ===\n", strings.ToUpper(region)) ││ ││ - // Fetch season details ││ - seasonDetail, err := client.FetchSeasonDetail(region, seasonID) ││ + // Fetch season index for this region ││ + seasonIndex, err := client.FetchSeasonIndex(region) ││ if err != nil { ││ - fmt.Printf("error fetching details - %v\n", err) ││ + fmt.Printf("Failed to fetch season index for %s: %v - skipping region\n", stri ││ ngs.ToUpper(region), err) ││ continue ││ } ││ ││ - // Upsert season ││ - err = dbService.UpsertSeason(seasonDetail.ID, seasonDetail.SeasonName, seasonDe ││ tail.StartTimestamp) ││ - if err != nil { ││ - fmt.Printf("error upserting - %v\n", err) ││ + if len(seasonIndex.Seasons) == 0 { ││ + fmt.Printf("No seasons found for %s\n", strings.ToUpper(region)) ││ continue ││ } ││ ││ - // Link periods to season ││ - if len(seasonDetail.Periods) > 0 { ││ - firstPeriod := seasonDetail.Periods[0].ID ││ - lastPeriod := seasonDetail.Periods[len(seasonDetail.Periods)-1].ID ││ + fmt.Printf("Found %d seasons in %s\n", len(seasonIndex.Seasons), strings.ToUppe ││ r(region)) ││ + ││ + // Process each season for this region ││ + for _, seasonRef := range seasonIndex.Seasons { ││ + seasonID := seasonRef.ID ││ + fmt.Printf(" Season %d: ", seasonID) ││ ││ - // Update period range ││ - err = dbService.UpdateSeasonPeriodRange(seasonDetail.ID, firstPeriod, lastPeri ││ od) ││ + // Fetch season details ││ + seasonDetail, err := client.FetchSeasonDetail(region, seasonID) ││ if err != nil { ││ - fmt.Printf("error updating period range - %v\n", err) ││ + fmt.Printf("error fetching details - %v\n", err) ││ + continue ││ } ││ ││ - // Link each period ││ - for _, periodRef := range seasonDetail.Periods { ││ - err = dbService.LinkPeriodToSeason(periodRef.ID, seasonDetail.ID) ││ + // Upsert season with region ││ + dbSeasonID, err := dbService.UpsertSeason(seasonDetail.ID, region, seasonDetai ││ l.SeasonName, seasonDetail.StartTimestamp) ││ + if err != nil { ││ + fmt.Printf("error upserting - %v\n", err) ││ + continue ││ + } ││ + ││ + // Link periods to season ││ + if len(seasonDetail.Periods) > 0 { ││ + firstPeriod := seasonDetail.Periods[0].ID ││ + lastPeriod := seasonDetail.Periods[len(seasonDetail.Periods)-1].ID ││ + ││ + // Update period range ││ + err = dbService.UpdateSeasonPeriodRange(dbSeasonID, firstPeriod, lastPeriod) ││ if err != nil { ││ - fmt.Printf("error linking period %d - %v\n", periodRef.ID, err) ││ + fmt.Printf("error updating period range - %v\n", err) ││ + } ││ + ││ + // Link each period ││ + for _, periodRef := range seasonDetail.Periods { ││ + err = dbService.LinkPeriodToSeason(periodRef.ID, dbSeasonID) ││ + if err != nil { ││ + fmt.Printf("error linking period %d - %v\n", periodRef.ID, err) ││ + } ││ } ││ } ││ + fmt.Printf("%s (%d periods)\n", seasonDetail.SeasonName, len(seasonDetail.Peri ││ ods)) ││ } ││ - fmt.Printf("%s (%d periods)\n", seasonDetail.SeasonName, len(seasonDetail.Perio ││ ds)) ││ } ││ ││ - fmt.Println("[OK] Season metadata synced") ││ + fmt.Println("\n[OK] Season metadata synced for all regions") ││ return nil ││ } ││ │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...kgs/ookstats/src/cmd/build.go ───┐│ diff --git a/nix/pkgs/ookstats/src/cmd/build ││ .go b/nix/pkgs/ookstats/src/cmd/build.go ││ index 9bee107..eb19768 100644 ││ --- a/nix/pkgs/ookstats/src/cmd/build.go ││ +++ b/nix/pkgs/ookstats/src/cmd/build.go ││ @@ -177,7 +177,7 @@ var buildCmd = &cobra.Co ││ mmand{ ││ // Get realms and dungeons for anal ││ yze ││ _, dungeons := blizzard.GetHardcode ││ dPeriodAndDungeons() ││ allRealms := blizzard.GetAllRealms( ││ ) ││ - if err := runAnalyze(client, allRea ││ lms, dungeons, periodsCSV, outPath, statusDi ││ r); err != nil { ││ + if err := runAnalyze(db, client, al ││ lRealms, dungeons, periodsCSV, outPath, stat ││ usDir); err != nil { ││ return fmt.Errorf("analyze stat ││ us: %w", err) ││ } ││ ││ @@ -308,76 +308,88 @@ func summarizeBuild(db ││ *sql.DB, outParent string) { ││ } ││ } ││ ││ -// syncSeasons syncs season metadata from B ││ lizzard API ││ +// syncSeasons syncs season metadata from B ││ lizzard API for all regions ││ func syncSeasons(db *sql.DB, client *blizza ││ rd.Client, regionsCSV string) error { ││ dbService := database.NewDatabaseService(d ││ b) ││ ││ - // Only need one region since seasons are ││ global ││ - region := "us" ││ + // Parse regions from CSV (seasons and per ││ iods are region-specific) ││ + var regions []string ││ if strings.TrimSpace(regionsCSV) != "" { ││ - parts := strings.Split(regionsCSV, ",") ││ - if len(parts) > 0 { ││ - region = strings.TrimSpace(parts[0]) ││ + for _, r := range strings.Split(regionsCS ││ V, ",") { ││ + if trimmed := strings.TrimSpace(r); trim ││ med != "" { ││ + regions = append(regions, trimmed) ││ + } ││ } ││ } ││ ││ - fmt.Printf("Fetching season metadata from ││ %s...\n", strings.ToUpper(region)) ││ - ││ - // Fetch season index ││ - seasonIndex, err := client.FetchSeasonInde ││ x(region) ││ - if err != nil { ││ - return fmt.Errorf("failed to fetch season ││ index: %w", err) ││ - } ││ - ││ - if len(seasonIndex.Seasons) == 0 { ││ - fmt.Println("No seasons found in API") ││ - return nil ││ + // Default to all regions if none specifie ││ d ││ + if len(regions) == 0 { ││ + regions = []string{"us", "eu", "kr", "tw" ││ } ││ } ││ ││ - fmt.Printf("Found %d seasons\n", len(seaso ││ nIndex.Seasons)) ││ + fmt.Printf("Fetching season metadata from ││ %d regions...\n", len(regions)) ││ ││ - // Process each season ││ - for _, seasonRef := range seasonIndex.Seas ││ ons { ││ - seasonID := seasonRef.ID ││ - fmt.Printf(" Season %d: ", seasonID) ││ + // Process each region ││ + for _, region := range regions { ││ + fmt.Printf("\n=== Region: %s ===\n", stri ││ ngs.ToUpper(region)) ││ ││ - // Fetch season details ││ - seasonDetail, err := client.FetchSeasonDe ││ tail(region, seasonID) ││ + // Fetch season index for this region ││ + seasonIndex, err := client.FetchSeasonInd ││ ex(region) ││ if err != nil { ││ - fmt.Printf("error fetching details - %v\ ││ n", err) ││ + fmt.Printf("Failed to fetch season index ││ for %s: %v - skipping region\n", strings.To ││ Upper(region), err) ││ continue ││ } ││ ││ - // Upsert season ││ - err = dbService.UpsertSeason(seasonDetail ││ .ID, seasonDetail.SeasonName, seasonDetail.S ││ tartTimestamp) ││ - if err != nil { ││ - fmt.Printf("error upserting - %v\n", err ││ ) ││ + if len(seasonIndex.Seasons) == 0 { ││ + fmt.Printf("No seasons found for %s\n", ││ strings.ToUpper(region)) ││ continue ││ } ││ ││ - // Link periods to season ││ - if len(seasonDetail.Periods) > 0 { ││ - firstPeriod := seasonDetail.Periods[0].I ││ D ││ - lastPeriod := seasonDetail.Periods[len(s ││ easonDetail.Periods)-1].ID ││ + fmt.Printf("Found %d seasons in %s\n", le ││ n(seasonIndex.Seasons), strings.ToUpper(regi ││ on)) ││ + ││ + // Process each season for this region ││ + for _, seasonRef := range seasonIndex.Sea ││ sons { ││ + seasonID := seasonRef.ID ││ + fmt.Printf(" Season %d: ", seasonID) ││ ││ - // Update period range ││ - err = dbService.UpdateSeasonPeriodRange( ││ seasonDetail.ID, firstPeriod, lastPeriod) ││ + // Fetch season details ││ + seasonDetail, err := client.FetchSeasonD ││ etail(region, seasonID) ││ if err != nil { ││ - fmt.Printf("error updating period range ││ - %v\n", err) ││ + fmt.Printf("error fetching details - %v ││ \n", err) ││ + continue ││ } ││ ││ - // Link each period ││ - for _, periodRef := range seasonDetail.P ││ eriods { ││ - err = dbService.LinkPeriodToSeason(peri ││ odRef.ID, seasonDetail.ID) ││ + // Upsert season with region ││ + dbSeasonID, err := dbService.UpsertSeaso ││ n(seasonDetail.ID, region, seasonDetail.Seas ││ onName, seasonDetail.StartTimestamp) ││ + if err != nil { ││ + fmt.Printf("error upserting - %v\n", er ││ r) ││ + continue ││ + } ││ + ││ + // Link periods to season ││ + if len(seasonDetail.Periods) > 0 { ││ + firstPeriod := seasonDetail.Periods[0]. ││ ID ││ + lastPeriod := seasonDetail.Periods[len( ││ seasonDetail.Periods)-1].ID ││ + ││ + // Update period range ││ + err = dbService.UpdateSeasonPeriodRange ││ (dbSeasonID, firstPeriod, lastPeriod) ││ if err != nil { ││ - fmt.Printf("error linking period %d - ││ %v\n", periodRef.ID, err) ││ + fmt.Printf("error updating period rang ││ e - %v\n", err) ││ + } ││ + ││ + // Link each period ││ + for _, periodRef := range seasonDetail. ││ Periods { ││ + err = dbService.LinkPeriodToSeason(per ││ iodRef.ID, dbSeasonID) ││ + if err != nil { ││ + fmt.Printf("error linking period %d - ││ %v\n", periodRef.ID, err) ││ + } ││ } ││ } ││ + fmt.Printf("%s (%d periods)\n", seasonDe ││ tail.SeasonName, len(seasonDetail.Periods)) ││ } ││ - fmt.Printf("%s (%d periods)\n", seasonDet ││ ail.SeasonName, len(seasonDetail.Periods)) ││ } ││ ││ - fmt.Println("[OK] Season metadata synced") ││ + fmt.Println("\n[OK] Season metadata synced ││ for all regions") ││ return nil ││ } ││ │└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/cmd/fetch.go ───────────────────────────────────────┐│ diff --git a/nix/pkgs/ookstats/src/cmd/fetch.go b/nix/pkgs/ookstats/src/cmd/fetch. ││ go ││ index 0432389..8e6daed 100644 ││ --- a/nix/pkgs/ookstats/src/cmd/fetch.go ││ +++ b/nix/pkgs/ookstats/src/cmd/fetch.go ││ @@ -259,7 +259,7 @@ var fetchSeasonsCmd = &cobra.Command{ ││ } ││ ││ // Upsert season ││ - err = dbService.UpsertSeason(seasonDetail.ID, seasonDetail.SeasonName, season ││ Detail.StartTimestamp) ││ + dbSeasonID, err := dbService.UpsertSeason(seasonDetail.ID, region, seasonDeta ││ il.SeasonName, seasonDetail.StartTimestamp) ││ if err != nil { ││ fmt.Printf("Error upserting season %d: %v\n", seasonID, err) ││ continue ││ @@ -276,14 +276,14 @@ var fetchSeasonsCmd = &cobra.Command{ ││ lastPeriod := seasonDetail.Periods[len(seasonDetail.Periods)-1].ID ││ ││ // Update period range ││ - err = dbService.UpdateSeasonPeriodRange(seasonDetail.ID, firstPeriod, lastPe ││ riod) ││ + err = dbService.UpdateSeasonPeriodRange(dbSeasonID, firstPeriod, lastPeriod) ││ if err != nil { ││ fmt.Printf("Error updating season period range: %v\n", err) ││ } ││ ││ // Link each period ││ for _, periodRef := range seasonDetail.Periods { ││ - err = dbService.LinkPeriodToSeason(periodRef.ID, seasonDetail.ID) ││ + err = dbService.LinkPeriodToSeason(periodRef.ID, dbSeasonID) ││ if err != nil { ││ fmt.Printf("Error linking period %d to season %d: %v\n", periodRef.ID, sea ││ sonDetail.ID, err) ││ } else { ││ @@ -294,10 +294,7 @@ var fetchSeasonsCmd = &cobra.Command{ ││ } ││ } ││ ││ - // Note: We only need to sync from one region since season IDs and periods are ││ global ││ - // Breaking after first successful region to avoid redundant work ││ - fmt.Printf("\n[OK] Synced seasons from %s (seasons are global across regions)\ ││ n", strings.ToUpper(region)) ││ - break ││ + fmt.Printf("\n[OK] Synced seasons for %s\n", strings.ToUpper(region)) ││ } ││ ││ fmt.Printf("\n=== Sync Complete ===\n") │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...kgs/ookstats/src/cmd/fetch.go ───┐│ diff --git a/nix/pkgs/ookstats/src/cmd/fetch ││ .go b/nix/pkgs/ookstats/src/cmd/fetch.go ││ index 0432389..8e6daed 100644 ││ --- a/nix/pkgs/ookstats/src/cmd/fetch.go ││ +++ b/nix/pkgs/ookstats/src/cmd/fetch.go ││ @@ -259,7 +259,7 @@ var fetchSeasonsCmd = &c ││ obra.Command{ ││ } ││ ││ // Upsert season ││ - err = dbService.UpsertSeason(seasonDeta ││ il.ID, seasonDetail.SeasonName, seasonDetail ││ .StartTimestamp) ││ + dbSeasonID, err := dbService.UpsertSeas ││ on(seasonDetail.ID, region, seasonDetail.Sea ││ sonName, seasonDetail.StartTimestamp) ││ if err != nil { ││ fmt.Printf("Error upserting season %d: ││ %v\n", seasonID, err) ││ continue ││ @@ -276,14 +276,14 @@ var fetchSeasonsCmd = ││ &cobra.Command{ ││ lastPeriod := seasonDetail.Periods[len ││ (seasonDetail.Periods)-1].ID ││ ││ // Update period range ││ - err = dbService.UpdateSeasonPeriodRang ││ e(seasonDetail.ID, firstPeriod, lastPeriod) ││ + err = dbService.UpdateSeasonPeriodRang ││ e(dbSeasonID, firstPeriod, lastPeriod) ││ if err != nil { ││ fmt.Printf("Error updating season per ││ iod range: %v\n", err) ││ } ││ ││ // Link each period ││ for _, periodRef := range seasonDetail ││ .Periods { ││ - err = dbService.LinkPeriodToSeason(pe ││ riodRef.ID, seasonDetail.ID) ││ + err = dbService.LinkPeriodToSeason(pe ││ riodRef.ID, dbSeasonID) ││ if err != nil { ││ fmt.Printf("Error linking period %d ││ to season %d: %v\n", periodRef.ID, seasonDet ││ ail.ID, err) ││ } else { ││ @@ -294,10 +294,7 @@ var fetchSeasonsCmd = & ││ cobra.Command{ ││ } ││ } ││ ││ - // Note: We only need to sync from one r ││ egion since season IDs and periods are globa ││ l ││ - // Breaking after first successful regio ││ n to avoid redundant work ││ - fmt.Printf("\n[OK] Synced seasons from % ││ s (seasons are global across regions)\n", st ││ rings.ToUpper(region)) ││ - break ││ + fmt.Printf("\n[OK] Synced seasons for %s ││ \n", strings.ToUpper(region)) ││ } ││ ││ fmt.Printf("\n=== Sync Complete ===\n") │└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/blizzard/client.go ────────────────────────┐│ diff --git a/nix/pkgs/ookstats/src/internal/blizzard/client.go b/nix/pkgs/ookstats ││ /src/internal/blizzard/client.go ││ index 3407c4a..28b2e7e 100644 ││ --- a/nix/pkgs/ookstats/src/internal/blizzard/client.go ││ +++ b/nix/pkgs/ookstats/src/internal/blizzard/client.go ││ @@ -489,94 +489,6 @@ type PlayerInfo struct { ││ Region string ││ } ││ ││ -// FetchPeriodIndex fetches the list of available periods for a region ││ -func (c *Client) FetchPeriodIndex(region string) (*PeriodIndexResponse, error) { ││ - namespace := fmt.Sprintf("dynamic-classic-%s", region) ││ - url := fmt.Sprintf( ││ - "https://%s.api.blizzard.com/data/wow/mythic-keystone/period/index?namespace=%s ││ &locale=en_US", ││ - region, namespace, ││ - ) ││ - ││ - const maxRetries = 3 ││ - const baseDelay = 1 * time.Second ││ - ││ - var lastErr error ││ - for attempt := 0; attempt < maxRetries; attempt++ { ││ - if attempt > 0 { ││ - delay := time.Duration(1<<uint(attempt-1)) * baseDelay ││ - time.Sleep(delay) ││ - } ││ - ││ - result, err := c.fetchPeriodIndexOnce(url) ││ - if err == nil { ││ - return result, nil ││ - } ││ - ││ - lastErr = err ││ - ││ - // don't retry on 4xx errors ││ - if strings.Contains(err.Error(), "status 4") { ││ - break ││ - } ││ - } ││ - ││ - return nil, fmt.Errorf("failed to fetch period index after %d attempts: %w", max ││ Retries, lastErr) ││ -} ││ - ││ -// fetchPeriodIndexOnce performs a single period index fetch attempt ││ -func (c *Client) fetchPeriodIndexOnce(url string) (*PeriodIndexResponse, error) { ││ - time.Sleep(50 * time.Millisecond) ││ - ││ - req, err := http.NewRequest("GET", url, nil) ││ - if err != nil { ││ - return nil, fmt.Errorf("failed to create request: %w", err) ││ - } ││ - ││ - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token)) ││ - req.Header.Set("User-Agent", "WoWStatsDB/1.0") ││ - ││ - resp, err := c.HTTPClient.Do(req) ││ - if err != nil { ││ - return nil, fmt.Errorf("HTTP request failed: %w", err) ││ - } ││ - defer resp.Body.Close() ││ - ││ - if resp.StatusCode != http.StatusOK { ││ - bodyBytes, _ := io.ReadAll(resp.Body) ││ - return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode ││ , string(bodyBytes)) ││ - } ││ - ││ - var result PeriodIndexResponse ││ - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { ││ - return nil, fmt.Errorf("failed to decode response: %w", err) ││ - } ││ - ││ - return &result, nil ││ -} ││ - ││ -// GetDynamicPeriodList fetches the period list from the API and returns it in re ││ verse order (newest first) ││ -func (c *Client) GetDynamicPeriodList(region string) ([]string, error) { ││ - index, err := c.FetchPeriodIndex(region) ││ - if err != nil { ││ - return nil, fmt.Errorf("failed to fetch period list for %s: %w", region, err) ││ - } ││ - ││ - if len(index.Periods) == 0 { ││ - return nil, fmt.Errorf("period index returned empty list for %s", region) ││ - } ││ - ││ - // Extract period IDs and reverse the list (newest first) ││ - periods := make([]string, len(index.Periods)) ││ - for i, p := range index.Periods { ││ - periods[len(index.Periods)-1-i] = fmt.Sprintf("%d", p.ID) ││ - } ││ - ││ - fmt.Printf("[OK] Fetched %d periods dynamically for %s (newest: %s, oldest: %s)\ ││ n", ││ - len(periods), strings.ToUpper(region), periods[0], periods[len(periods)-1]) ││ - ││ - return periods, nil ││ -} ││ - ││ // FetchSeasonIndex fetches the list of available seasons for a region ││ func (c *Client) FetchSeasonIndex(region string) (*SeasonIndexResponse, error) { ││ namespace := fmt.Sprintf("dynamic-classic-%s", region) │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...c/internal/blizzard/client.go ───┐│ diff --git a/nix/pkgs/ookstats/src/internal/ ││ blizzard/client.go b/nix/pkgs/ookstats/src/i ││ nternal/blizzard/client.go ││ index 3407c4a..28b2e7e 100644 ││ --- a/nix/pkgs/ookstats/src/internal/blizzar ││ d/client.go ││ +++ b/nix/pkgs/ookstats/src/internal/blizzar ││ d/client.go ││ @@ -489,94 +489,6 @@ type PlayerInfo struct ││ { ││ Region string ││ } ││ ││ -// FetchPeriodIndex fetches the list of ava ││ ilable periods for a region ││ -func (c *Client) FetchPeriodIndex(region st ││ ring) (*PeriodIndexResponse, error) { ││ - namespace := fmt.Sprintf("dynamic-classic- ││ %s", region) ││ - url := fmt.Sprintf( ││ - "https://%s.api.blizzard.com/data/wow/myt ││ hic-keystone/period/index?namespace=%s&local ││ e=en_US", ││ - region, namespace, ││ - ) ││ - ││ - const maxRetries = 3 ││ - const baseDelay = 1 * time.Second ││ - ││ - var lastErr error ││ - for attempt := 0; attempt < maxRetries; at ││ tempt++ { ││ - if attempt > 0 { ││ - delay := time.Duration(1<<uint(attempt-1 ││ )) * baseDelay ││ - time.Sleep(delay) ││ - } ││ - ││ - result, err := c.fetchPeriodIndexOnce(url ││ ) ││ - if err == nil { ││ - return result, nil ││ - } ││ - ││ - lastErr = err ││ - ││ - // don't retry on 4xx errors ││ - if strings.Contains(err.Error(), "status ││ 4") { ││ - break ││ - } ││ - } ││ - ││ - return nil, fmt.Errorf("failed to fetch pe ││ riod index after %d attempts: %w", maxRetrie ││ s, lastErr) ││ -} ││ - ││ -// fetchPeriodIndexOnce performs a single p ││ eriod index fetch attempt ││ -func (c *Client) fetchPeriodIndexOnce(url s ││ tring) (*PeriodIndexResponse, error) { ││ - time.Sleep(50 * time.Millisecond) ││ - ││ - req, err := http.NewRequest("GET", url, ni ││ l) ││ - if err != nil { ││ - return nil, fmt.Errorf("failed to create ││ request: %w", err) ││ - } ││ - ││ - req.Header.Set("Authorization", fmt.Sprint ││ f("Bearer %s", c.Token)) ││ - req.Header.Set("User-Agent", "WoWStatsDB/1 ││ .0") ││ - ││ - resp, err := c.HTTPClient.Do(req) ││ - if err != nil { ││ - return nil, fmt.Errorf("HTTP request fail ││ ed: %w", err) ││ - } ││ - defer resp.Body.Close() ││ - ││ - if resp.StatusCode != http.StatusOK { ││ - bodyBytes, _ := io.ReadAll(resp.Body) ││ - return nil, fmt.Errorf("API request faile ││ d with status %d: %s", resp.StatusCode, stri ││ ng(bodyBytes)) ││ - } ││ - ││ - var result PeriodIndexResponse ││ - if err := json.NewDecoder(resp.Body).Decod ││ e(&result); err != nil { ││ - return nil, fmt.Errorf("failed to decode ││ response: %w", err) ││ - } ││ - ││ - return &result, nil ││ -} ││ - ││ -// GetDynamicPeriodList fetches the period ││ list from the API and returns it in reverse ││ order (newest first) ││ -func (c *Client) GetDynamicPeriodList(regio ││ n string) ([]string, error) { ││ - index, err := c.FetchPeriodIndex(region) ││ - if err != nil { ││ - return nil, fmt.Errorf("failed to fetch p ││ eriod list for %s: %w", region, err) ││ - } ││ - ││ - if len(index.Periods) == 0 { ││ - return nil, fmt.Errorf("period index retu ││ rned empty list for %s", region) ││ - } ││ - ││ - // Extract period IDs and reverse the list ││ (newest first) ││ - periods := make([]string, len(index.Period ││ s)) ││ - for i, p := range index.Periods { ││ - periods[len(index.Periods)-1-i] = fmt.Spr ││ intf("%d", p.ID) ││ - } ││ - ││ - fmt.Printf("[OK] Fetched %d periods dynami ││ cally for %s (newest: %s, oldest: %s)\n", ││ - len(periods), strings.ToUpper(region), pe ││ riods[0], periods[len(periods)-1]) ││ - ││ - return periods, nil ││ -} ││ - ││ // FetchSeasonIndex fetches the list of ava ││ ilable seasons for a region ││ func (c *Client) FetchSeasonIndex(region st ││ ring) (*SeasonIndexResponse, error) { ││ namespace := fmt.Sprintf("dynamic-classic- ││ %s", region) │└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/blizzard/types.go ─────────────────────────┐│ diff --git a/nix/pkgs/ookstats/src/internal/blizzard/types.go b/nix/pkgs/ookstats/ ││ src/internal/blizzard/types.go ││ index 25f338c..0932f7c 100644 ││ --- a/nix/pkgs/ookstats/src/internal/blizzard/types.go ││ +++ b/nix/pkgs/ookstats/src/internal/blizzard/types.go ││ @@ -242,22 +242,6 @@ type MediaAsset struct { ││ Value string `json:"value"` ││ } ││ ││ -// PeriodIndexResponse represents the response from the mythic keystone period in ││ dex API ││ -type PeriodIndexResponse struct { ││ - Periods []struct { ││ - Key struct { ││ - Href string `json:"href"` ││ - } `json:"key"` ││ - ID int `json:"id"` ││ - } `json:"periods"` ││ - CurrentPeriod struct { ││ - Key struct { ││ - Href string `json:"href"` ││ - } `json:"key"` ││ - ID int `json:"id"` ││ - } `json:"current_period"` ││ -} ││ - ││ // PeriodDetailResponse represents the response from a specific period detail API ││ type PeriodDetailResponse struct { ││ ID int `json:"id"` │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...rc/internal/blizzard/types.go ───┐│ diff --git a/nix/pkgs/ookstats/src/internal/ ││ blizzard/types.go b/nix/pkgs/ookstats/src/in ││ ternal/blizzard/types.go ││ index 25f338c..0932f7c 100644 ││ --- a/nix/pkgs/ookstats/src/internal/blizzar ││ d/types.go ││ +++ b/nix/pkgs/ookstats/src/internal/blizzar ││ d/types.go ││ @@ -242,22 +242,6 @@ type MediaAsset struct ││ { ││ Value string `json:"value"` ││ } ││ ││ -// PeriodIndexResponse represents the respo ││ nse from the mythic keystone period index AP ││ I ││ -type PeriodIndexResponse struct { ││ - Periods []struct { ││ - Key struct { ││ - Href string `json:"href"` ││ - } `json:"key"` ││ - ID int `json:"id"` ││ - } `json:"periods"` ││ - CurrentPeriod struct { ││ - Key struct { ││ - Href string `json:"href"` ││ - } `json:"key"` ││ - ID int `json:"id"` ││ - } `json:"current_period"` ││ -} ││ - ││ // PeriodDetailResponse represents the resp ││ onse from a specific period detail API ││ type PeriodDetailResponse struct { ││ ID int `json:"id"` │└──────────────────────────────────────────────┘
┌─ 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 469e4f1..2a94fa6 100644 ││ --- a/nix/pkgs/ookstats/src/internal/database/client.go ││ +++ b/nix/pkgs/ookstats/src/internal/database/client.go ││ @@ -324,13 +324,15 @@ func EnsureCompleteSchema(db *sql.DB) error { ││ ││ // Season tables ││ `CREATE TABLE IF NOT EXISTS seasons ( ││ - id INTEGER PRIMARY KEY, ││ - season_number INTEGER UNIQUE, ││ + id INTEGER PRIMARY KEY AUTOINCREMENT, ││ + season_number INTEGER NOT NULL, ││ + region TEXT NOT NULL, ││ start_timestamp INTEGER, ││ end_timestamp INTEGER, ││ season_name TEXT, ││ first_period_id INTEGER, ││ - last_period_id INTEGER ││ + last_period_id INTEGER, ││ + UNIQUE(season_number, region) ││ )`, ││ ││ `CREATE TABLE IF NOT EXISTS period_seasons ( ││ @@ -353,6 +355,11 @@ fmt.Printf("[OK] All tables created\n") ││ return err ││ } ││ ││ + // Migrate seasons schema if needed (add region column) ││ + if err := migrateSeasonsAddRegion(db); err != nil { ││ + return err ││ + } ││ + ││ // Create indexes ││ return ensureRecommendedIndexes(db) ││ } ││ @@ -443,3 +450,55 @@ func migrateRealmsCompositeSlug(db *sql.DB) error { ││ fmt.Printf("[OK] Realms table migrated\n") ││ return nil ││ } ││ + ││ +// migrateSeasonsAddRegion adds region column to seasons table if it doesn't exis ││ t ││ +func migrateSeasonsAddRegion(db *sql.DB) error { ││ + // Check if region column already exists ││ + var createSQL string ││ + _ = db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='s ││ easons'`).Scan(&createSQL) ││ + ││ + // If table already has region column, nothing to do ││ + if strings.Contains(strings.ToLower(createSQL), "region text") { ││ + return nil ││ + } ││ + ││ + fmt.Printf("[MIGRATE] Adding region column to seasons table...\n") ││ + tx, err := db.Begin() ││ + if err != nil { return err } ││ + defer tx.Rollback() ││ + ││ + // Create new table with region column ││ + if _, err := tx.Exec(` ││ + CREATE TABLE IF NOT EXISTS seasons_new ( ││ + id INTEGER PRIMARY KEY AUTOINCREMENT, ││ + season_number INTEGER NOT NULL, ││ + region TEXT NOT NULL, ││ + start_timestamp INTEGER, ││ + end_timestamp INTEGER, ││ + season_name TEXT, ││ + first_period_id INTEGER, ││ + last_period_id INTEGER, ││ + UNIQUE(season_number, region) ││ + ) ││ + `); err != nil { return fmt.Errorf("create seasons_new: %w", err) } ││ + ││ + // Copy existing data, defaulting region to 'us' for backward compatibility ││ + if _, err := tx.Exec(` ││ + INSERT INTO seasons_new (id, season_number, region, start_timestamp, end_ ││ timestamp, season_name, first_period_id, last_period_id) ││ + SELECT id, season_number, 'us', start_timestamp, end_timestamp, season_na ││ me, first_period_id, last_period_id ││ + FROM seasons ││ + `); err != nil { ││ + return fmt.Errorf("copy seasons: %w", err) ││ + } ││ + ││ + // Rename old and new ││ + if _, err := tx.Exec(`ALTER TABLE seasons RENAME TO seasons_old`); err != nil ││ { return fmt.Errorf("rename old: %w", err) } ││ + if _, err := tx.Exec(`ALTER TABLE seasons_new RENAME TO seasons`); err != nil ││ { return fmt.Errorf("rename new: %w", err) } ││ + ││ + // Drop old ││ + if _, err := tx.Exec(`DROP TABLE seasons_old`); err != nil { return fmt.Error ││ f("drop old: %w", err) } ││ + ││ + if err := tx.Commit(); err != nil { return err } ││ + fmt.Printf("[OK] Seasons table migrated - added region column\n") ││ + return nil ││ +} │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...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 469e4f1..2a94fa6 100644 ││ --- a/nix/pkgs/ookstats/src/internal/databas ││ e/client.go ││ +++ b/nix/pkgs/ookstats/src/internal/databas ││ e/client.go ││ @@ -324,13 +324,15 @@ func EnsureCompleteSch ││ ema(db *sql.DB) error { ││ ││ // Season tables ││ `CREATE TABLE IF NOT EXISTS seasons ( ││ - id INTEGER PRIMARY KEY, ││ - season_number INTEGER UNIQUE, ││ + id INTEGER PRIMARY KEY AUTOINCREMENT, ││ + season_number INTEGER NOT NULL, ││ + region TEXT NOT NULL, ││ start_timestamp INTEGER, ││ end_timestamp INTEGER, ││ season_name TEXT, ││ first_period_id INTEGER, ││ - last_period_id INTEGER ││ + last_period_id INTEGER, ││ + UNIQUE(season_number, region) ││ )`, ││ ││ `CREATE TABLE IF NOT EXISTS period_season ││ s ( ││ @@ -353,6 +355,11 @@ fmt.Printf("[OK] All ta ││ bles created\n") ││ return err ││ } ││ ││ + // Migrate seasons schema if needed (ad ││ d region column) ││ + if err := migrateSeasonsAddRegion(db); ││ err != nil { ││ + return err ││ + } ││ + ││ // Create indexes ││ return ensureRecommendedIndexes(db) ││ } ││ @@ -443,3 +450,55 @@ func migrateRealmsCompo ││ siteSlug(db *sql.DB) error { ││ fmt.Printf("[OK] Realms table migrated\ ││ n") ││ return nil ││ } ││ + ││ +// migrateSeasonsAddRegion adds region colu ││ mn to seasons table if it doesn't exist ││ +func migrateSeasonsAddRegion(db *sql.DB) er ││ ror { ││ + // Check if region column already exist ││ s ││ + var createSQL string ││ + _ = db.QueryRow(`SELECT sql FROM sqlite ││ _master WHERE type='table' AND name='seasons ││ '`).Scan(&createSQL) ││ + ││ + // If table already has region column, ││ nothing to do ││ + if strings.Contains(strings.ToLower(cre ││ ateSQL), "region text") { ││ + return nil ││ + } ││ + ││ + fmt.Printf("[MIGRATE] Adding region col ││ umn to seasons table...\n") ││ + tx, err := db.Begin() ││ + if err != nil { return err } ││ + defer tx.Rollback() ││ + ││ + // Create new table with region column ││ + if _, err := tx.Exec(` ││ + CREATE TABLE IF NOT EXISTS seasons_ ││ new ( ││ + id INTEGER PRIMARY KEY AUTOINCR ││ EMENT, ││ + season_number INTEGER NOT NULL, ││ + region TEXT NOT NULL, ││ + start_timestamp INTEGER, ││ + end_timestamp INTEGER, ││ + season_name TEXT, ││ + first_period_id INTEGER, ││ + last_period_id INTEGER, ││ + UNIQUE(season_number, region) ││ + ) ││ + `); err != nil { return fmt.Errorf("cre ││ ate seasons_new: %w", err) } ││ + ││ + // Copy existing data, defaulting regio ││ n to 'us' for backward compatibility ││ + if _, err := tx.Exec(` ││ + INSERT INTO seasons_new (id, season ││ _number, region, start_timestamp, end_timest ││ amp, season_name, first_period_id, last_peri ││ od_id) ││ + SELECT id, season_number, 'us', sta ││ rt_timestamp, end_timestamp, season_name, fi ││ rst_period_id, last_period_id ││ + FROM seasons ││ + `); err != nil { ││ + return fmt.Errorf("copy seasons: %w ││ ", err) ││ + } ││ + ││ + // Rename old and new ││ + if _, err := tx.Exec(`ALTER TABLE seaso ││ ns RENAME TO seasons_old`); err != nil { ret ││ urn fmt.Errorf("rename old: %w", err) } ││ + if _, err := tx.Exec(`ALTER TABLE seaso ││ ns_new RENAME TO seasons`); err != nil { ret ││ urn fmt.Errorf("rename new: %w", err) } ││ + ││ + // Drop old ││ + if _, err := tx.Exec(`DROP TABLE season ││ s_old`); err != nil { return fmt.Errorf("dro ││ p old: %w", err) } ││ + ││ + if err := tx.Commit(); err != nil { ret ││ urn err } ││ + fmt.Printf("[OK] Seasons table migrated ││ - added region column\n") ││ + return nil ││ +} │└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/database/operations.go ────────────────────┐│ diff --git a/nix/pkgs/ookstats/src/internal/database/operations.go b/nix/pkgs/ooks ││ tats/src/internal/database/operations.go ││ index 1518169..18675c8 100644 ││ --- a/nix/pkgs/ookstats/src/internal/database/operations.go ││ +++ b/nix/pkgs/ookstats/src/internal/database/operations.go ││ @@ -1181,17 +1181,19 @@ func (ds *DatabaseService) insertPlayerEquipmentTx(tx *sql ││ .Tx, playerID int, equ ││ ││ // Season management operations ││ ││ -// UpsertSeason inserts or updates a season record ││ -func (ds *DatabaseService) UpsertSeason(seasonID int, seasonName string, startTim ││ estamp int64) error { ││ +// UpsertSeason inserts or updates a season record and returns the auto-increment ││ ID ││ +func (ds *DatabaseService) UpsertSeason(seasonID int, region string, seasonName s ││ tring, startTimestamp int64) (int, error) { ││ query := ` ││ - INSERT INTO seasons (id, season_number, season_name, start_timestamp) ││ + INSERT INTO seasons (season_number, region, season_name, start_timestamp) ││ VALUES (?, ?, ?, ?) ││ - ON CONFLICT(id) DO UPDATE SET ││ + ON CONFLICT(season_number, region) DO UPDATE SET ││ season_name = excluded.season_name, ││ start_timestamp = excluded.start_timestamp ││ + RETURNING id ││ ` ││ - _, err := ds.db.Exec(query, seasonID, seasonID, seasonName, startTimestamp) ││ - return err ││ + var id int ││ + err := ds.db.QueryRow(query, seasonID, region, seasonName, startTimestamp).Scan( ││ &id) ││ + return id, err ││ } ││ ││ // UpdateSeasonPeriodRange updates the first_period_id and last_period_id for a s ││ eason ││ @@ -1376,6 +1378,32 @@ func (ds *DatabaseService) GetPeriodsForSeason(seasonID int ││ ) ([]int, error) { ││ return periods, rows.Err() ││ } ││ ││ +// GetPeriodsForRegion retrieves all period IDs for all seasons in a given region ││ +func (ds *DatabaseService) GetPeriodsForRegion(region string) ([]int, error) { ││ + query := ` ││ + SELECT DISTINCT ps.period_id ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + WHERE s.region = ? ││ + ORDER BY ps.period_id DESC ││ + ` ││ + rows, err := ds.db.Query(query, region) ││ + if err != nil { ││ + return nil, err ││ + } ││ + defer rows.Close() ││ + ││ + var periods []int ││ + for rows.Next() { ││ + var periodID int ││ + if err := rows.Scan(&periodID); err != nil { ││ + return nil, err ││ + } ││ + periods = append(periods, periodID) ││ + } ││ + return periods, rows.Err() ││ +} ││ + ││ // GetRealmPoolIDs returns all realm IDs in a realm pool (parent + all children) ││ // For a child realm, it returns the parent and all siblings ││ // For a parent realm, it returns itself and all children │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ternal/database/operations.go ───┐│ diff --git a/nix/pkgs/ookstats/src/internal/ ││ database/operations.go b/nix/pkgs/ookstats/s ││ rc/internal/database/operations.go ││ index 1518169..18675c8 100644 ││ --- a/nix/pkgs/ookstats/src/internal/databas ││ e/operations.go ││ +++ b/nix/pkgs/ookstats/src/internal/databas ││ e/operations.go ││ @@ -1181,17 +1181,19 @@ func (ds *DatabaseSe ││ rvice) insertPlayerEquipmentTx(tx *sql.Tx, p ││ layerID int, equ ││ ││ // Season management operations ││ ││ -// UpsertSeason inserts or updates a season ││ record ││ -func (ds *DatabaseService) UpsertSeason(sea ││ sonID int, seasonName string, startTimestamp ││ int64) error { ││ +// UpsertSeason inserts or updates a season ││ record and returns the auto-increment ID ││ +func (ds *DatabaseService) UpsertSeason(sea ││ sonID int, region string, seasonName string, ││ startTimestamp int64) (int, error) { ││ query := ` ││ - INSERT INTO seasons (id, season_number, s ││ eason_name, start_timestamp) ││ + INSERT INTO seasons (season_number, regio ││ n, season_name, start_timestamp) ││ VALUES (?, ?, ?, ?) ││ - ON CONFLICT(id) DO UPDATE SET ││ + ON CONFLICT(season_number, region) DO UPD ││ ATE SET ││ season_name = excluded.season_name, ││ start_timestamp = excluded.start_timesta ││ mp ││ + RETURNING id ││ ` ││ - _, err := ds.db.Exec(query, seasonID, seas ││ onID, seasonName, startTimestamp) ││ - return err ││ + var id int ││ + err := ds.db.QueryRow(query, seasonID, reg ││ ion, seasonName, startTimestamp).Scan(&id) ││ + return id, err ││ } ││ ││ // UpdateSeasonPeriodRange updates the firs ││ t_period_id and last_period_id for a season ││ @@ -1376,6 +1378,32 @@ func (ds *DatabaseSer ││ vice) GetPeriodsForSeason(seasonID int) ([]i ││ nt, error) { ││ return periods, rows.Err() ││ } ││ ││ +// GetPeriodsForRegion retrieves all period ││ IDs for all seasons in a given region ││ +func (ds *DatabaseService) GetPeriodsForReg ││ ion(region string) ([]int, error) { ││ + query := ` ││ + SELECT DISTINCT ps.period_id ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + WHERE s.region = ? ││ + ORDER BY ps.period_id DESC ││ + ` ││ + rows, err := ds.db.Query(query, region) ││ + if err != nil { ││ + return nil, err ││ + } ││ + defer rows.Close() ││ + ││ + var periods []int ││ + for rows.Next() { ││ + var periodID int ││ + if err := rows.Scan(&periodID); err != ni ││ l { ││ + return nil, err ││ + } ││ + periods = append(periods, periodID) ││ + } ││ + return periods, rows.Err() ││ +} ││ + ││ // GetRealmPoolIDs returns all realm IDs in ││ a realm pool (parent + all children) ││ // For a child realm, it returns the parent ││ and all siblings ││ // For a parent realm, it returns itself an ││ d all children │└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/generator/leaderboards.go ─────────────────┐│ diff --git a/nix/pkgs/ookstats/src/internal/generator/leaderboards.go b/nix/pkgs/o ││ okstats/src/internal/generator/leaderboards.go ││ index 23770be..d616815 100644 ││ --- a/nix/pkgs/ookstats/src/internal/generator/leaderboards.go ││ +++ b/nix/pkgs/ookstats/src/internal/generator/leaderboards.go ││ @@ -156,9 +156,13 @@ type seasonInfo struct { ││ Name string ││ } ││ ││ -// loadSeasons loads all seasons from the database ││ +// loadSeasons loads all distinct season numbers from the database ││ func loadSeasons(db *sql.DB) ([]seasonInfo, error) { ││ - rows, err := db.Query(`SELECT id, season_name FROM seasons ORDER BY start_timest ││ amp ASC`) ││ + rows, err := db.Query(` ││ + SELECT DISTINCT season_number, season_name ││ + FROM seasons ││ + ORDER BY season_number ASC ││ + `) ││ if err != nil { ││ return nil, fmt.Errorf("seasons query: %w", err) ││ } │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...nal/generator/leaderboards.go ───┐│ diff --git a/nix/pkgs/ookstats/src/internal/ ││ generator/leaderboards.go b/nix/pkgs/ookstat ││ s/src/internal/generator/leaderboards.go ││ index 23770be..d616815 100644 ││ --- a/nix/pkgs/ookstats/src/internal/generat ││ or/leaderboards.go ││ +++ b/nix/pkgs/ookstats/src/internal/generat ││ or/leaderboards.go ││ @@ -156,9 +156,13 @@ type seasonInfo struct ││ { ││ Name string ││ } ││ ││ -// loadSeasons loads all seasons from the d ││ atabase ││ +// loadSeasons loads all distinct season nu ││ mbers from the database ││ func loadSeasons(db *sql.DB) ([]seasonInfo, ││ error) { ││ - rows, err := db.Query(`SELECT id, season_n ││ ame FROM seasons ORDER BY start_timestamp AS ││ C`) ││ + rows, err := db.Query(` ││ + SELECT DISTINCT season_number, season_nam ││ e ││ + FROM seasons ││ + ORDER BY season_number ASC ││ + `) ││ if err != nil { ││ return nil, fmt.Errorf("seasons query: %w ││ ", err) ││ } │└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/pipeline/fetch.go ─────────────────────────┐│ diff --git a/nix/pkgs/ookstats/src/internal/pipeline/fetch.go b/nix/pkgs/ookstats/ ││ src/internal/pipeline/fetch.go ││ index f6aa51c..40f743b 100644 ││ --- a/nix/pkgs/ookstats/src/internal/pipeline/fetch.go ││ +++ b/nix/pkgs/ookstats/src/internal/pipeline/fetch.go ││ @@ -136,20 +136,33 @@ func FetchChallengeMode(db *database.DatabaseService, client ││ *blizzard.Client, o ││ ││ // Determine periods for this region ││ var periods []string ││ - var err error ││ ││ if len(opts.Periods) > 0 { ││ // User-specified periods ││ periods = opts.Periods ││ fmt.Printf("Using user-specified periods: %v (%d periods)\n", periods, len(per ││ iods)) ││ } else { ││ - // Fetch periods dynamically from Blizzard API for this region ││ - fmt.Printf("Fetching period list dynamically from Blizzard API for %s...\n", s ││ trings.ToUpper(region)) ││ - periods, err = client.GetDynamicPeriodList(region) ││ + // Fetch periods from database (populated by season sync) ││ + fmt.Printf("Fetching period list from database for %s...\n", strings.ToUpper(r ││ egion)) ││ + periodInts, err := db.GetPeriodsForRegion(region) ││ if err != nil { ││ - fmt.Printf("Failed to fetch period list for %s: %v - skipping region\n", stri ││ ngs.ToUpper(region), err) ││ + fmt.Printf("Failed to fetch periods from database for %s: %v - skipping regio ││ n\n", strings.ToUpper(region), err) ││ continue ││ } ││ + ││ + // Convert []int to []string for compatibility ││ + periods = make([]string, len(periodInts)) ││ + for i, p := range periodInts { ││ + periods[i] = fmt.Sprintf("%d", p) ││ + } ││ + ││ + if len(periods) == 0 { ││ + fmt.Printf("No periods found in database for %s (run season sync first) - ski ││ pping region\n", strings.ToUpper(region)) ││ + continue ││ + } ││ + ││ + fmt.Printf("[OK] Fetched %d periods from database for %s (newest: %s, oldest: ││ %s)\n", ││ + len(periods), strings.ToUpper(region), periods[0], periods[len(periods)-1]) ││ } ││ ││ if len(periods) == 0 { │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...rc/internal/pipeline/fetch.go ───┐│ diff --git a/nix/pkgs/ookstats/src/internal/ ││ pipeline/fetch.go b/nix/pkgs/ookstats/src/in ││ ternal/pipeline/fetch.go ││ index f6aa51c..40f743b 100644 ││ --- a/nix/pkgs/ookstats/src/internal/pipelin ││ e/fetch.go ││ +++ b/nix/pkgs/ookstats/src/internal/pipelin ││ e/fetch.go ││ @@ -136,20 +136,33 @@ func FetchChallengeMod ││ e(db *database.DatabaseService, client *bliz ││ zard.Client, o ││ ││ // Determine periods for this region ││ var periods []string ││ - var err error ││ ││ if len(opts.Periods) > 0 { ││ // User-specified periods ││ periods = opts.Periods ││ fmt.Printf("Using user-specified periods ││ : %v (%d periods)\n", periods, len(periods)) ││ } else { ││ - // Fetch periods dynamically from Blizza ││ rd API for this region ││ - fmt.Printf("Fetching period list dynamic ││ ally from Blizzard API for %s...\n", strings ││ .ToUpper(region)) ││ - periods, err = client.GetDynamicPeriodLi ││ st(region) ││ + // Fetch periods from database (populate ││ d by season sync) ││ + fmt.Printf("Fetching period list from da ││ tabase for %s...\n", strings.ToUpper(region) ││ ) ││ + periodInts, err := db.GetPeriodsForRegio ││ n(region) ││ if err != nil { ││ - fmt.Printf("Failed to fetch period list ││ for %s: %v - skipping region\n", strings.To ││ Upper(region), err) ││ + fmt.Printf("Failed to fetch periods fro ││ m database for %s: %v - skipping region\n", ││ strings.ToUpper(region), err) ││ continue ││ } ││ + ││ + // Convert []int to []string for compati ││ bility ││ + periods = make([]string, len(periodInts) ││ ) ││ + for i, p := range periodInts { ││ + periods[i] = fmt.Sprintf("%d", p) ││ + } ││ + ││ + if len(periods) == 0 { ││ + fmt.Printf("No periods found in databas ││ e for %s (run season sync first) - skipping ││ region\n", strings.ToUpper(region)) ││ + continue ││ + } ││ + ││ + fmt.Printf("[OK] Fetched %d periods from ││ database for %s (newest: %s, oldest: %s)\n" ││ , ││ + len(periods), strings.ToUpper(region), ││ periods[0], periods[len(periods)-1]) ││ } ││ ││ if len(periods) == 0 { │└──────────────────────────────────────────────┘
┌─ 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 2e503b6..ca980a3 100644 ││ --- a/nix/pkgs/ookstats/src/internal/pipeline/process.go ││ +++ b/nix/pkgs/ookstats/src/internal/pipeline/process.go ││ @@ -113,13 +113,19 @@ func ProcessRunRankings(db *sql.DB, opts ProcessRunRankingsO ││ ptions) error { ││ return fmt.Errorf("failed to compute regional rankings: %w", err) ││ } ││ ││ + // step 3: compute realm run rankings (pool-based for connected realms) ││ + fmt.Println("\n3. Computing realm run rankings (pool-based)...") ││ + if err := computeRealmRankings(tx); err != nil { ││ + return fmt.Errorf("failed to compute realm rankings: %w", err) ││ + } ││ + ││ // commit all changes ││ if err := tx.Commit(); err != nil { ││ return fmt.Errorf("failed to commit run rankings: %w", err) ││ } ││ ││ // optimize database ││ - fmt.Println("\n3. Optimizing database...") ││ + fmt.Println("\n4. Optimizing database...") ││ if _, err := db.Exec("VACUUM"); err != nil { ││ fmt.Printf("Warning: database optimization failed: %v\n", err) ││ } ││ @@ -156,7 +162,7 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) { ││ cr.dungeon_id, ││ cr.id as run_id, ││ cr.duration, ││ - COALESCE(ps.season_id, 1) as season_id, ││ + COALESCE(s_agg.season_number, 1) as season_id, ││ cr.completed_timestamp, ││ rr_gf.ranking as global_ranking_filtered, ││ rr_rf.ranking as regional_ranking_filtered, ││ @@ -166,31 +172,41 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) { ││ rr_lf.percentile_bracket as realm_percentile_bracket ││ FROM run_members rm ││ INNER JOIN challenge_runs cr ON rm.run_id = cr.id ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ INNER JOIN ( ││ SELECT ││ rm2.player_id, ││ cr2.dungeon_id, ││ - COALESCE(ps2.season_id, 1) as season_id, ││ + COALESCE(s2_agg.season_number, 1) as season_id, ││ MIN(cr2.duration) as best_duration ││ FROM run_members rm2 ││ INNER JOIN challenge_runs cr2 ON rm2.run_id = cr2.id ││ - LEFT JOIN period_seasons ps2 ON cr2.period_id = ps2.period_id ││ - GROUP BY rm2.player_id, cr2.dungeon_id, COALESCE(ps2.season_id, 1) ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s2_agg ON cr2.period_id = s2_agg.period_id ││ + GROUP BY rm2.player_id, cr2.dungeon_id, COALESCE(s2_agg.season_number, 1) ││ ) best_times ON rm.player_id = best_times.player_id ││ AND cr.dungeon_id = best_times.dungeon_id ││ - AND COALESCE(ps.season_id, 1) = best_times.season_id ││ + AND COALESCE(s_agg.season_number, 1) = best_times.season_id ││ AND cr.duration = best_times.best_duration ││ LEFT JOIN run_rankings rr_gf ON cr.id = rr_gf.run_id ││ AND rr_gf.ranking_type = 'global' AND rr_gf.ranking_scope = 'filtered' ││ - AND rr_gf.season_id = COALESCE(ps.season_id, 1) ││ + AND rr_gf.season_id = COALESCE(s_agg.season_number, 1) ││ LEFT JOIN run_rankings rr_rf ON cr.id = rr_rf.run_id ││ AND rr_rf.ranking_type = 'regional' AND rr_rf.ranking_scope = 'filtered' ││ - AND rr_rf.season_id = COALESCE(ps.season_id, 1) ││ + AND rr_rf.season_id = COALESCE(s_agg.season_number, 1) ││ LEFT JOIN run_rankings rr_lf ON cr.id = rr_lf.run_id ││ AND rr_lf.ranking_type = 'realm' AND rr_lf.ranking_scope = 'filtered' ││ - AND rr_lf.season_id = COALESCE(ps.season_id, 1) ││ - GROUP BY rm.player_id, cr.dungeon_id, COALESCE(ps.season_id, 1) ││ + AND rr_lf.season_id = COALESCE(s_agg.season_number, 1) ││ + GROUP BY rm.player_id, cr.dungeon_id, COALESCE(s_agg.season_number, 1) ││ HAVING cr.id = MIN(cr.id) ││ `) ││ if err != nil { ││ @@ -225,11 +241,16 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) { ││ FROM players p ││ INNER JOIN player_best_runs pbr ON p.id = pbr.player_id ││ INNER JOIN ( ││ - SELECT rm.player_id, COALESCE(ps.season_id, 1) as season_id, COUNT(*) as run_c ││ ount ││ + SELECT rm.player_id, COALESCE(s_agg.season_number, 1) as season_id, COUNT(*) a ││ s run_count ││ FROM run_members rm ││ INNER JOIN challenge_runs cr ON rm.run_id = cr.id ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ - GROUP BY rm.player_id, COALESCE(ps.season_id, 1) ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + GROUP BY rm.player_id, COALESCE(s_agg.season_number, 1) ││ ) season_runs ON p.id = season_runs.player_id AND pbr.season_id = season_runs.s ││ eason_id ││ GROUP BY p.id, pbr.season_id, p.name, p.realm_id, season_runs.run_count ││ `, currentTime) ││ @@ -278,8 +299,8 @@ func computePlayerRankings(tx *sql.Tx) (int, error) { ││ ││ currentTime := time.Now().UnixMilli() ││ ││ - // Get all seasons ││ - seasonRows, err := tx.Query("SELECT DISTINCT season_id FROM player_profiles ORDE ││ R BY season_id") ││ + // Get all distinct season numbers (global seasons across all regions) ││ + seasonRows, err := tx.Query("SELECT DISTINCT season_number FROM seasons ORDER BY ││ season_number") ││ if err != nil { ││ return 0, fmt.Errorf("failed to query seasons: %w", err) ││ } ││ @@ -287,11 +308,11 @@ func computePlayerRankings(tx *sql.Tx) (int, error) { ││ ││ var seasons []int ││ for seasonRows.Next() { ││ - var seasonID int ││ - if err := seasonRows.Scan(&seasonID); err != nil { ││ + var seasonNumber int ││ + if err := seasonRows.Scan(&seasonNumber); err != nil { ││ return 0, err ││ } ││ - seasons = append(seasons, seasonID) ││ + seasons = append(seasons, seasonNumber) ││ } ││ ││ if len(seasons) == 0 { ││ @@ -547,11 +568,16 @@ func computeGlobalRankings(tx *sql.Tx) error { ││ cr.dungeon_id, ││ 'global' as ranking_type, ││ 'all' as ranking_scope, ││ - ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, COALESCE(ps.season_id, 1) ORDER ││ BY cr.duration ASC, cr.completed_timestamp ASC) as ranking, ││ - COALESCE(ps.season_id, 1) as season_id, ││ + ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, COALESCE(s_agg.season_number, 1 ││ ) ORDER BY cr.duration ASC, cr.completed_timestamp ASC) as ranking, ││ + COALESCE(s_agg.season_number, 1) as season_id, ││ ? as computed_at ││ FROM challenge_runs cr ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ `, currentTime) ││ if err != nil { ││ return err ││ @@ -616,7 +642,16 @@ func computeGlobalRankings(tx *sql.Tx) error { ││ } ││ ││ // Get all seasons (including fallback season 1 for unmapped periods) ││ - seasonRows, err := tx.Query("SELECT DISTINCT COALESCE(ps.season_id, 1) as season ││ _id FROM challenge_runs cr LEFT JOIN period_seasons ps ON cr.period_id = ps.period ││ _id") ││ + seasonRows, err := tx.Query(` ││ + SELECT DISTINCT COALESCE(s_agg.season_number, 1) as season_id ││ + FROM challenge_runs cr ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + `) ││ if err != nil { ││ return err ││ } ││ @@ -640,8 +675,13 @@ func computeGlobalRankings(tx *sql.Tx) error { ││ cr.team_signature, ││ MIN(cr.duration) as best_duration ││ FROM challenge_runs cr ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ - WHERE cr.dungeon_id = ? AND COALESCE(ps.season_id, 1) = ? ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + WHERE cr.dungeon_id = ? AND COALESCE(s_agg.season_number, 1) = ? ││ GROUP BY cr.team_signature ││ ), ││ filtered_runs AS ( ││ @@ -651,10 +691,15 @@ func computeGlobalRankings(tx *sql.Tx) error { ││ cr.completed_timestamp, ││ ROW_NUMBER() OVER (ORDER BY cr.duration ASC, cr.completed_timestamp ASC) as ││ filtered_rank ││ FROM challenge_runs cr ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ INNER JOIN best_team_runs btr ON cr.team_signature = btr.team_signature ││ AND cr.duration = btr.best_duration ││ - WHERE cr.dungeon_id = ? AND COALESCE(ps.season_id, 1) = ? ││ + WHERE cr.dungeon_id = ? AND COALESCE(s_agg.season_number, 1) = ? ││ GROUP BY cr.team_signature ││ HAVING cr.id = MIN(cr.id) ││ ) ││ @@ -757,12 +802,17 @@ func computeRegionalRankings(tx *sql.Tx) error { ││ cr.dungeon_id, ││ 'regional' as ranking_type, ││ ? as ranking_scope, ││ - ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, COALESCE(ps.season_id, 1) ORDE ││ R BY cr.duration ASC, cr.completed_timestamp ASC) as ranking, ││ - COALESCE(ps.season_id, 1) as season_id, ││ + ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, COALESCE(s_agg.season_number, ││ 1) ORDER BY cr.duration ASC, cr.completed_timestamp ASC) as ranking, ││ + COALESCE(s_agg.season_number, 1) as season_id, ││ ? as computed_at ││ FROM challenge_runs cr ││ INNER JOIN realms r ON cr.realm_id = r.id ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ WHERE r.region = ? ││ `, region, currentTime, region) ││ ││ @@ -830,10 +880,15 @@ func computeRegionalRankings(tx *sql.Tx) error { ││ ││ // Get all seasons for this region ││ seasonRows, err := tx.Query(` ││ - SELECT DISTINCT COALESCE(ps.season_id, 1) as season_id ││ + SELECT DISTINCT COALESCE(s_agg.season_number, 1) as season_id ││ FROM challenge_runs cr ││ INNER JOIN realms r ON cr.realm_id = r.id ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ WHERE r.region = ? ││ `, region) ││ if err != nil { ││ @@ -861,8 +916,13 @@ func computeRegionalRankings(tx *sql.Tx) error { ││ MIN(cr.duration) as best_duration ││ FROM challenge_runs cr ││ INNER JOIN realms r ON cr.realm_id = r.id ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ - WHERE cr.dungeon_id = ? AND r.region = ? AND COALESCE(ps.season_id, 1) = ? ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + WHERE cr.dungeon_id = ? AND r.region = ? AND COALESCE(s_agg.season_number, ││ 1) = ? ││ GROUP BY cr.team_signature ││ ), ││ filtered_runs AS ( ││ @@ -873,10 +933,15 @@ func computeRegionalRankings(tx *sql.Tx) error { ││ ROW_NUMBER() OVER (ORDER BY cr.duration ASC, cr.completed_timestamp ASC) a ││ s filtered_rank ││ FROM challenge_runs cr ││ INNER JOIN realms r ON cr.realm_id = r.id ││ - LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ INNER JOIN best_team_runs btr ON cr.team_signature = btr.team_signature ││ AND cr.duration = btr.best_duration ││ - WHERE cr.dungeon_id = ? AND r.region = ? AND COALESCE(ps.season_id, 1) = ? ││ + WHERE cr.dungeon_id = ? AND r.region = ? AND COALESCE(s_agg.season_number, ││ 1) = ? ││ GROUP BY cr.team_signature ││ HAVING cr.id = MIN(cr.id) ││ ) ││ @@ -944,3 +1009,276 @@ func computeRegionalRankings(tx *sql.Tx) error { ││ fmt.Printf("[OK] Computed regional rankings with percentile brackets for %d regi ││ ons\n", len(regions)) ││ return nil ││ } ││ + ││ +// computeRealmRankings computes realm rankings per realm pool (connected realms ││ grouped together) ││ +func computeRealmRankings(tx *sql.Tx) error { ││ + fmt.Printf("Computing realm rankings (pool-based for connected realms)...\n") ││ + ││ + currentTime := time.Now().UnixMilli() ││ + ││ + // clear existing realm rankings ││ + if _, err := tx.Exec("DELETE FROM run_rankings WHERE ranking_type = 'realm'"); e ││ rr != nil { ││ + return err ││ + } ││ + ││ + // get all realm pools (parent realms or independent realms that will be used as ││ pool identifiers) ││ + type RealmPool struct { ││ + PoolSlug string ││ + Region string ││ + } ││ + ││ + poolRows, err := tx.Query(` ││ + SELECT DISTINCT ││ + COALESCE(parent_r.slug, r.slug) as pool_slug, ││ + r.region ││ + FROM realms r ││ + LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.region = ││ parent_r.region ││ + ORDER BY r.region, pool_slug ││ + `) ││ + if err != nil { ││ + return err ││ + } ││ + defer poolRows.Close() ││ + ││ + var pools []RealmPool ││ + for poolRows.Next() { ││ + var pool RealmPool ││ + if err := poolRows.Scan(&pool.PoolSlug, &pool.Region); err != nil { ││ + return err ││ + } ││ + pools = append(pools, pool) ││ + } ││ + ││ + fmt.Printf("Found %d realm pools to process\n", len(pools)) ││ + ││ + for _, pool := range pools { ││ + // unfiltered realm rankings (per season) using pool-based partitioning ││ + _, err := tx.Exec(` ││ + INSERT INTO run_rankings (run_id, dungeon_id, ranking_type, ranking_scope, ran ││ king, season_id, computed_at) ││ + SELECT ││ + cr.id as run_id, ││ + cr.dungeon_id, ││ + 'realm' as ranking_type, ││ + ? as ranking_scope, ││ + ROW_NUMBER() OVER ( ││ + PARTITION BY cr.dungeon_id, COALESCE(s_agg.season_number, 1) ││ + ORDER BY cr.duration ASC, cr.completed_timestamp ASC ││ + ) as ranking, ││ + COALESCE(s_agg.season_number, 1) as season_id, ││ + ? as computed_at ││ + FROM challenge_runs cr ││ + INNER JOIN realms r ON cr.realm_id = r.id ││ + LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.region ││ = parent_r.region ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + WHERE r.region = ? AND COALESCE(parent_r.slug, r.slug) = ? ││ + `, pool.PoolSlug, currentTime, pool.Region, pool.PoolSlug) ││ + ││ + if err != nil { ││ + return err ││ + } ││ + ││ + // update percentile brackets for unfiltered realm rankings ││ + _, err = tx.Exec(` ││ + UPDATE run_rankings ││ + SET percentile_bracket = ( ││ + CASE ││ + WHEN counts.duration = counts.min_duration THEN 'artifact' ││ + ELSE ││ + CASE ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 1.0 THEN 'excellent' ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 5.0 THEN 'legendary' ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 20.0 THEN 'epic' ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 40.0 THEN 'rare' ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 60.0 THEN 'uncommon' ││ + ELSE 'common' ││ + END ││ + END ││ + ) ││ + FROM ( ││ + SELECT ││ + rr.run_id, ││ + rr.dungeon_id, ││ + rr.season_id, ││ + rr.ranking, ││ + cr.duration, ││ + MIN(cr.duration) OVER (PARTITION BY rr.dungeon_id, rr.season_id, rr.ranking_ ││ scope) as min_duration, ││ + COUNT(*) OVER (PARTITION BY rr.dungeon_id, rr.season_id, rr.ranking_scope) a ││ s total_in_pool_season_dungeon ││ + FROM run_rankings rr ││ + INNER JOIN challenge_runs cr ON rr.run_id = cr.id ││ + WHERE rr.ranking_type = 'realm' AND rr.ranking_scope = ? ││ + ) counts ││ + WHERE run_rankings.run_id = counts.run_id ││ + AND run_rankings.dungeon_id = counts.dungeon_id ││ + AND run_rankings.season_id = counts.season_id ││ + AND run_rankings.ranking_type = 'realm' ││ + AND run_rankings.ranking_scope = ? ││ + `, pool.PoolSlug, pool.PoolSlug) ││ + if err != nil { ││ + return err ││ + } ││ + } ││ + ││ + // now compute filtered rankings per pool x dungeon x season ││ + dungeonRows, err := tx.Query("SELECT id FROM dungeons") ││ + if err != nil { ││ + return err ││ + } ││ + ││ + var dungeonIDs []int ││ + for dungeonRows.Next() { ││ + var id int ││ + if err := dungeonRows.Scan(&id); err != nil { ││ + dungeonRows.Close() ││ + return err ││ + } ││ + dungeonIDs = append(dungeonIDs, id) ││ + } ││ + dungeonRows.Close() ││ + ││ + for _, pool := range pools { ││ + // Get seasons for this pool ││ + seasonRows, err := tx.Query(` ││ + SELECT DISTINCT COALESCE(s_agg.season_number, 1) as season_id ││ + FROM challenge_runs cr ││ + INNER JOIN realms r ON cr.realm_id = r.id ││ + LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.region ││ = parent_r.region ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + WHERE r.region = ? AND COALESCE(parent_r.slug, r.slug) = ? ││ + `, pool.Region, pool.PoolSlug) ││ + if err != nil { ││ + return err ││ + } ││ + ││ + var seasonIDs []int ││ + for seasonRows.Next() { ││ + var id int ││ + if err := seasonRows.Scan(&id); err != nil { ││ + seasonRows.Close() ││ + return err ││ + } ││ + seasonIDs = append(seasonIDs, id) ││ + } ││ + seasonRows.Close() ││ + ││ + // filtered realm rankings - per dungeon x season using pool-based partitioning ││ + for _, dungeonID := range dungeonIDs { ││ + for _, seasonID := range seasonIDs { ││ + _, err := tx.Exec(` ││ + WITH best_team_runs AS ( ││ + SELECT ││ + cr.team_signature, ││ + MIN(cr.duration) as best_duration ││ + FROM challenge_runs cr ││ + INNER JOIN realms r ON cr.realm_id = r.id ││ + LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.regi ││ on = parent_r.region ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + WHERE cr.dungeon_id = ? ││ + AND r.region = ? ││ + AND COALESCE(parent_r.slug, r.slug) = ? ││ + AND COALESCE(s_agg.season_number, 1) = ? ││ + GROUP BY cr.team_signature ││ + ), ││ + filtered_runs AS ( ││ + SELECT ││ + cr.id as run_id, ││ + cr.duration, ││ + cr.completed_timestamp, ││ + ROW_NUMBER() OVER (ORDER BY cr.duration ASC, cr.completed_timestamp ASC) a ││ s filtered_rank ││ + FROM challenge_runs cr ││ + INNER JOIN realms r ON cr.realm_id = r.id ││ + LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.regi ││ on = parent_r.region ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + INNER JOIN best_team_runs btr ON cr.team_signature = btr.team_signature ││ + AND cr.duration = btr.best_duration ││ + WHERE cr.dungeon_id = ? ││ + AND r.region = ? ││ + AND COALESCE(parent_r.slug, r.slug) = ? ││ + AND COALESCE(s_agg.season_number, 1) = ? ││ + GROUP BY cr.team_signature ││ + HAVING cr.id = MIN(cr.id) ││ + ) ││ + INSERT INTO run_rankings (run_id, dungeon_id, ranking_type, ranking_scope, r ││ anking, season_id, computed_at) ││ + SELECT ││ + run_id, ││ + ? as dungeon_id, ││ + 'realm' as ranking_type, ││ + ? as ranking_scope, ││ + filtered_rank as ranking, ││ + ? as season_id, ││ + ? as computed_at ││ + FROM filtered_runs ││ + `, dungeonID, pool.Region, pool.PoolSlug, seasonID, ││ + dungeonID, pool.Region, pool.PoolSlug, seasonID, ││ + dungeonID, pool.PoolSlug+"_filtered", seasonID, currentTime) ││ + ││ + if err != nil { ││ + return err ││ + } ││ + } ││ + } ││ + ││ + // update percentile brackets for filtered realm rankings ││ + filteredScope := pool.PoolSlug + "_filtered" ││ + _, err = tx.Exec(` ││ + UPDATE run_rankings ││ + SET percentile_bracket = ( ││ + CASE ││ + WHEN counts.duration = counts.min_duration THEN 'artifact' ││ + ELSE ││ + CASE ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 1.0 THEN 'excellent' ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 5.0 THEN 'legendary' ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 20.0 THEN 'epic' ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 40.0 THEN 'rare' ││ + WHEN (CAST(counts.ranking AS REAL) / CAST(counts.total_in_pool_season_dung ││ eon AS REAL) * 100) <= 60.0 THEN 'uncommon' ││ + ELSE 'common' ││ + END ││ + END ││ + ) ││ + FROM ( ││ + SELECT ││ + rr.run_id, ││ + rr.dungeon_id, ││ + rr.season_id, ││ + rr.ranking, ││ + cr.duration, ││ + MIN(cr.duration) OVER (PARTITION BY rr.dungeon_id, rr.season_id, rr.ranking_ ││ scope) as min_duration, ││ + COUNT(*) OVER (PARTITION BY rr.dungeon_id, rr.season_id, rr.ranking_scope) a ││ s total_in_pool_season_dungeon ││ + FROM run_rankings rr ││ + INNER JOIN challenge_runs cr ON rr.run_id = cr.id ││ + WHERE rr.ranking_type = 'realm' AND rr.ranking_scope = ? ││ + ) counts ││ + WHERE run_rankings.run_id = counts.run_id ││ + AND run_rankings.dungeon_id = counts.dungeon_id ││ + AND run_rankings.season_id = counts.season_id ││ + AND run_rankings.ranking_type = 'realm' ││ + AND run_rankings.ranking_scope = ? ││ + `, filteredScope, filteredScope) ││ + if err != nil { ││ + return err ││ + } ││ + } ││ + ││ + fmt.Printf("[OK] Computed realm rankings with percentile brackets for %d realm p ││ ools\n", len(pools)) ││ + return nil ││ +} │└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../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 2e503b6..ca980a3 100644 ││ --- a/nix/pkgs/ookstats/src/internal/pipelin ││ e/process.go ││ +++ b/nix/pkgs/ookstats/src/internal/pipelin ││ e/process.go ││ @@ -113,13 +113,19 @@ func ProcessRunRanking ││ s(db *sql.DB, opts ProcessRunRankingsOptions ││ ) error { ││ return fmt.Errorf("failed to compute regi ││ onal rankings: %w", err) ││ } ││ ││ + // step 3: compute realm run rankings (poo ││ l-based for connected realms) ││ + fmt.Println("\n3. Computing realm run rank ││ ings (pool-based)...") ││ + if err := computeRealmRankings(tx); err != ││ nil { ││ + return fmt.Errorf("failed to compute real ││ m rankings: %w", err) ││ + } ││ + ││ // commit all changes ││ if err := tx.Commit(); err != nil { ││ return fmt.Errorf("failed to commit run r ││ ankings: %w", err) ││ } ││ ││ // optimize database ││ - fmt.Println("\n3. Optimizing database...") ││ + fmt.Println("\n4. Optimizing database...") ││ if _, err := db.Exec("VACUUM"); err != nil ││ { ││ fmt.Printf("Warning: database optimizatio ││ n failed: %v\n", err) ││ } ││ @@ -156,7 +162,7 @@ func createPlayerAggrega ││ tions(tx *sql.Tx) (int, error) { ││ cr.dungeon_id, ││ cr.id as run_id, ││ cr.duration, ││ - COALESCE(ps.season_id, 1) as season_id, ││ + COALESCE(s_agg.season_number, 1) as seas ││ on_id, ││ cr.completed_timestamp, ││ rr_gf.ranking as global_ranking_filtered ││ , ││ rr_rf.ranking as regional_ranking_filter ││ ed, ││ @@ -166,31 +172,41 @@ func createPlayerAggre ││ gations(tx *sql.Tx) (int, error) { ││ rr_lf.percentile_bracket as realm_percen ││ tile_bracket ││ FROM run_members rm ││ INNER JOIN challenge_runs cr ON rm.run_id ││ = cr.id ││ - LEFT JOIN period_seasons ps ON cr.period_ ││ id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number ││ ) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ INNER JOIN ( ││ SELECT ││ rm2.player_id, ││ cr2.dungeon_id, ││ - COALESCE(ps2.season_id, 1) as season_id ││ , ││ + COALESCE(s2_agg.season_number, 1) as se ││ ason_id, ││ MIN(cr2.duration) as best_duration ││ FROM run_members rm2 ││ INNER JOIN challenge_runs cr2 ON rm2.run ││ _id = cr2.id ││ - LEFT JOIN period_seasons ps2 ON cr2.peri ││ od_id = ps2.period_id ││ - GROUP BY rm2.player_id, cr2.dungeon_id, ││ COALESCE(ps2.season_id, 1) ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_numbe ││ r) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s2_agg ON cr2.period_id = s2_agg.perio ││ d_id ││ + GROUP BY rm2.player_id, cr2.dungeon_id, ││ COALESCE(s2_agg.season_number, 1) ││ ) best_times ON rm.player_id = best_times ││ .player_id ││ AND cr.dungeon_id = best_times.dungeo ││ n_id ││ - AND COALESCE(ps.season_id, 1) = best_ ││ times.season_id ││ + AND COALESCE(s_agg.season_number, 1) ││ = best_times.season_id ││ AND cr.duration = best_times.best_dur ││ ation ││ LEFT JOIN run_rankings rr_gf ON cr.id = r ││ r_gf.run_id ││ AND rr_gf.ranking_type = 'global' AND rr ││ _gf.ranking_scope = 'filtered' ││ - AND rr_gf.season_id = COALESCE(ps.season ││ _id, 1) ││ + AND rr_gf.season_id = COALESCE(s_agg.sea ││ son_number, 1) ││ LEFT JOIN run_rankings rr_rf ON cr.id = r ││ r_rf.run_id ││ AND rr_rf.ranking_type = 'regional' AND ││ rr_rf.ranking_scope = 'filtered' ││ - AND rr_rf.season_id = COALESCE(ps.season ││ _id, 1) ││ + AND rr_rf.season_id = COALESCE(s_agg.sea ││ son_number, 1) ││ LEFT JOIN run_rankings rr_lf ON cr.id = r ││ r_lf.run_id ││ AND rr_lf.ranking_type = 'realm' AND rr_ ││ lf.ranking_scope = 'filtered' ││ - AND rr_lf.season_id = COALESCE(ps.season ││ _id, 1) ││ - GROUP BY rm.player_id, cr.dungeon_id, COA ││ LESCE(ps.season_id, 1) ││ + AND rr_lf.season_id = COALESCE(s_agg.sea ││ son_number, 1) ││ + GROUP BY rm.player_id, cr.dungeon_id, COA ││ LESCE(s_agg.season_number, 1) ││ HAVING cr.id = MIN(cr.id) ││ `) ││ if err != nil { ││ @@ -225,11 +241,16 @@ func createPlayerAggre ││ gations(tx *sql.Tx) (int, error) { ││ FROM players p ││ INNER JOIN player_best_runs pbr ON p.id = ││ pbr.player_id ││ INNER JOIN ( ││ - SELECT rm.player_id, COALESCE(ps.season_ ││ id, 1) as season_id, COUNT(*) as run_count ││ + SELECT rm.player_id, COALESCE(s_agg.seas ││ on_number, 1) as season_id, COUNT(*) as run_ ││ count ││ FROM run_members rm ││ INNER JOIN challenge_runs cr ON rm.run_i ││ d = cr.id ││ - LEFT JOIN period_seasons ps ON cr.period ││ _id = ps.period_id ││ - GROUP BY rm.player_id, COALESCE(ps.seaso ││ n_id, 1) ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_numbe ││ r) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_i ││ d ││ + GROUP BY rm.player_id, COALESCE(s_agg.se ││ ason_number, 1) ││ ) season_runs ON p.id = season_runs.playe ││ r_id AND pbr.season_id = season_runs.season_ ││ id ││ GROUP BY p.id, pbr.season_id, p.name, p.r ││ ealm_id, season_runs.run_count ││ `, currentTime) ││ @@ -278,8 +299,8 @@ func computePlayerRankin ││ gs(tx *sql.Tx) (int, error) { ││ ││ currentTime := time.Now().UnixMilli() ││ ││ - // Get all seasons ││ - seasonRows, err := tx.Query("SELECT DISTIN ││ CT season_id FROM player_profiles ORDER BY s ││ eason_id") ││ + // Get all distinct season numbers (global ││ seasons across all regions) ││ + seasonRows, err := tx.Query("SELECT DISTIN ││ CT season_number FROM seasons ORDER BY seaso ││ n_number") ││ if err != nil { ││ return 0, fmt.Errorf("failed to query sea ││ sons: %w", err) ││ } ││ @@ -287,11 +308,11 @@ func computePlayerRank ││ ings(tx *sql.Tx) (int, error) { ││ ││ var seasons []int ││ for seasonRows.Next() { ││ - var seasonID int ││ - if err := seasonRows.Scan(&seasonID); err ││ != nil { ││ + var seasonNumber int ││ + if err := seasonRows.Scan(&seasonNumber); ││ err != nil { ││ return 0, err ││ } ││ - seasons = append(seasons, seasonID) ││ + seasons = append(seasons, seasonNumber) ││ } ││ ││ if len(seasons) == 0 { ││ @@ -547,11 +568,16 @@ func computeGlobalRank ││ ings(tx *sql.Tx) error { ││ cr.dungeon_id, ││ 'global' as ranking_type, ││ 'all' as ranking_scope, ││ - ROW_NUMBER() OVER (PARTITION BY cr.dunge ││ on_id, COALESCE(ps.season_id, 1) ORDER BY cr ││ .duration ASC, cr.completed_timestamp ASC) a ││ s ranking, ││ - COALESCE(ps.season_id, 1) as season_id, ││ + ROW_NUMBER() OVER (PARTITION BY cr.dunge ││ on_id, COALESCE(s_agg.season_number, 1) ORDE ││ R BY cr.duration ASC, cr.completed_timestamp ││ ASC) as ranking, ││ + COALESCE(s_agg.season_number, 1) as seas ││ on_id, ││ ? as computed_at ││ FROM challenge_runs cr ││ - LEFT JOIN period_seasons ps ON cr.period_ ││ id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number ││ ) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ `, currentTime) ││ if err != nil { ││ return err ││ @@ -616,7 +642,16 @@ func computeGlobalRanki ││ ngs(tx *sql.Tx) error { ││ } ││ ││ // Get all seasons (including fallback sea ││ son 1 for unmapped periods) ││ - seasonRows, err := tx.Query("SELECT DISTIN ││ CT COALESCE(ps.season_id, 1) as season_id FR ││ OM challenge_runs cr LEFT JOIN period_season ││ s ps ON cr.period_id = ps.period_id") ││ + seasonRows, err := tx.Query(` ││ + SELECT DISTINCT COALESCE(s_agg.season_num ││ ber, 1) as season_id ││ + FROM challenge_runs cr ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_number ││ ) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_id ││ + `) ││ if err != nil { ││ return err ││ } ││ @@ -640,8 +675,13 @@ func computeGlobalRanki ││ ngs(tx *sql.Tx) error { ││ cr.team_signature, ││ MIN(cr.duration) as best_duration ││ FROM challenge_runs cr ││ - LEFT JOIN period_seasons ps ON cr.peri ││ od_id = ps.period_id ││ - WHERE cr.dungeon_id = ? AND COALESCE(p ││ s.season_id, 1) = ? ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_num ││ ber) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period ││ _id ││ + WHERE cr.dungeon_id = ? AND COALESCE(s ││ _agg.season_number, 1) = ? ││ GROUP BY cr.team_signature ││ ), ││ filtered_runs AS ( ││ @@ -651,10 +691,15 @@ func computeGlobalRank ││ ings(tx *sql.Tx) error { ││ cr.completed_timestamp, ││ ROW_NUMBER() OVER (ORDER BY cr.durati ││ on ASC, cr.completed_timestamp ASC) as filte ││ red_rank ││ FROM challenge_runs cr ││ - LEFT JOIN period_seasons ps ON cr.peri ││ od_id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_num ││ ber) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period ││ _id ││ INNER JOIN best_team_runs btr ON cr.te ││ am_signature = btr.team_signature ││ AND cr.duration = btr.best_dur ││ ation ││ - WHERE cr.dungeon_id = ? AND COALESCE(p ││ s.season_id, 1) = ? ││ + WHERE cr.dungeon_id = ? AND COALESCE(s ││ _agg.season_number, 1) = ? ││ GROUP BY cr.team_signature ││ HAVING cr.id = MIN(cr.id) ││ ) ││ @@ -757,12 +802,17 @@ func computeRegionalRa ││ nkings(tx *sql.Tx) error { ││ cr.dungeon_id, ││ 'regional' as ranking_type, ││ ? as ranking_scope, ││ - ROW_NUMBER() OVER (PARTITION BY cr.dung ││ eon_id, COALESCE(ps.season_id, 1) ORDER BY c ││ r.duration ASC, cr.completed_timestamp ASC) ││ as ranking, ││ - COALESCE(ps.season_id, 1) as season_id, ││ + ROW_NUMBER() OVER (PARTITION BY cr.dung ││ eon_id, COALESCE(s_agg.season_number, 1) ORD ││ ER BY cr.duration ASC, cr.completed_timestam ││ p ASC) as ranking, ││ + COALESCE(s_agg.season_number, 1) as sea ││ son_id, ││ ? as computed_at ││ FROM challenge_runs cr ││ INNER JOIN realms r ON cr.realm_id = r.i ││ d ││ - LEFT JOIN period_seasons ps ON cr.period ││ _id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_numbe ││ r) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_i ││ d ││ WHERE r.region = ? ││ `, region, currentTime, region) ││ ││ @@ -830,10 +880,15 @@ func computeRegionalRa ││ nkings(tx *sql.Tx) error { ││ ││ // Get all seasons for this region ││ seasonRows, err := tx.Query(` ││ - SELECT DISTINCT COALESCE(ps.season_id, 1 ││ ) as season_id ││ + SELECT DISTINCT COALESCE(s_agg.season_nu ││ mber, 1) as season_id ││ FROM challenge_runs cr ││ INNER JOIN realms r ON cr.realm_id = r.i ││ d ││ - LEFT JOIN period_seasons ps ON cr.period ││ _id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_numbe ││ r) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_i ││ d ││ WHERE r.region = ? ││ `, region) ││ if err != nil { ││ @@ -861,8 +916,13 @@ func computeRegionalRan ││ kings(tx *sql.Tx) error { ││ MIN(cr.duration) as best_duration ││ FROM challenge_runs cr ││ INNER JOIN realms r ON cr.realm_id = ││ r.id ││ - LEFT JOIN period_seasons ps ON cr.per ││ iod_id = ps.period_id ││ - WHERE cr.dungeon_id = ? AND r.region ││ = ? AND COALESCE(ps.season_id, 1) = ? ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_nu ││ mber) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.i ││ d ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.perio ││ d_id ││ + WHERE cr.dungeon_id = ? AND r.region ││ = ? AND COALESCE(s_agg.season_number, 1) = ? ││ GROUP BY cr.team_signature ││ ), ││ filtered_runs AS ( ││ @@ -873,10 +933,15 @@ func computeRegionalRa ││ nkings(tx *sql.Tx) error { ││ ROW_NUMBER() OVER (ORDER BY cr.durat ││ ion ASC, cr.completed_timestamp ASC) as filt ││ ered_rank ││ FROM challenge_runs cr ││ INNER JOIN realms r ON cr.realm_id = ││ r.id ││ - LEFT JOIN period_seasons ps ON cr.per ││ iod_id = ps.period_id ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_nu ││ mber) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.i ││ d ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.perio ││ d_id ││ INNER JOIN best_team_runs btr ON cr.t ││ eam_signature = btr.team_signature ││ AND cr.duration = btr.best_du ││ ration ││ - WHERE cr.dungeon_id = ? AND r.region ││ = ? AND COALESCE(ps.season_id, 1) = ? ││ + WHERE cr.dungeon_id = ? AND r.region ││ = ? AND COALESCE(s_agg.season_number, 1) = ? ││ GROUP BY cr.team_signature ││ HAVING cr.id = MIN(cr.id) ││ ) ││ @@ -944,3 +1009,276 @@ func computeRegionalR ││ ankings(tx *sql.Tx) error { ││ fmt.Printf("[OK] Computed regional ranking ││ s with percentile brackets for %d regions\n" ││ , len(regions)) ││ return nil ││ } ││ + ││ +// computeRealmRankings computes realm rank ││ ings per realm pool (connected realms groupe ││ d together) ││ +func computeRealmRankings(tx *sql.Tx) error ││ { ││ + fmt.Printf("Computing realm rankings (pool ││ -based for connected realms)...\n") ││ + ││ + currentTime := time.Now().UnixMilli() ││ + ││ + // clear existing realm rankings ││ + if _, err := tx.Exec("DELETE FROM run_rank ││ ings WHERE ranking_type = 'realm'"); err != ││ nil { ││ + return err ││ + } ││ + ││ + // get all realm pools (parent realms or i ││ ndependent realms that will be used as pool ││ identifiers) ││ + type RealmPool struct { ││ + PoolSlug string ││ + Region string ││ + } ││ + ││ + poolRows, err := tx.Query(` ││ + SELECT DISTINCT ││ + COALESCE(parent_r.slug, r.slug) as pool_ ││ slug, ││ + r.region ││ + FROM realms r ││ + LEFT JOIN realms parent_r ON r.parent_rea ││ lm_slug = parent_r.slug AND r.region = paren ││ t_r.region ││ + ORDER BY r.region, pool_slug ││ + `) ││ + if err != nil { ││ + return err ││ + } ││ + defer poolRows.Close() ││ + ││ + var pools []RealmPool ││ + for poolRows.Next() { ││ + var pool RealmPool ││ + if err := poolRows.Scan(&pool.PoolSlug, & ││ pool.Region); err != nil { ││ + return err ││ + } ││ + pools = append(pools, pool) ││ + } ││ + ││ + fmt.Printf("Found %d realm pools to proces ││ s\n", len(pools)) ││ + ││ + for _, pool := range pools { ││ + // unfiltered realm rankings (per season) ││ using pool-based partitioning ││ + _, err := tx.Exec(` ││ + INSERT INTO run_rankings (run_id, dungeo ││ n_id, ranking_type, ranking_scope, ranking, ││ season_id, computed_at) ││ + SELECT ││ + cr.id as run_id, ││ + cr.dungeon_id, ││ + 'realm' as ranking_type, ││ + ? as ranking_scope, ││ + ROW_NUMBER() OVER ( ││ + PARTITION BY cr.dungeon_id, COALESCE(s ││ _agg.season_number, 1) ││ + ORDER BY cr.duration ASC, cr.completed ││ _timestamp ASC ││ + ) as ranking, ││ + COALESCE(s_agg.season_number, 1) as sea ││ son_id, ││ + ? as computed_at ││ + FROM challenge_runs cr ││ + INNER JOIN realms r ON cr.realm_id = r.i ││ d ││ + LEFT JOIN realms parent_r ON r.parent_re ││ alm_slug = parent_r.slug AND r.region = pare ││ nt_r.region ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_numbe ││ r) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_i ││ d ││ + WHERE r.region = ? AND COALESCE(parent_r ││ .slug, r.slug) = ? ││ + `, pool.PoolSlug, currentTime, pool.Regio ││ n, pool.PoolSlug) ││ + ││ + if err != nil { ││ + return err ││ + } ││ + ││ + // update percentile brackets for unfilte ││ red realm rankings ││ + _, err = tx.Exec(` ││ + UPDATE run_rankings ││ + SET percentile_bracket = ( ││ + CASE ││ + WHEN counts.duration = counts.min_dura ││ tion THEN 'artifact' ││ + ELSE ││ + CASE ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 1.0 THEN 'excellent' ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 5.0 THEN 'legendary' ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 20.0 THEN 'epic' ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 40.0 THEN 'rare' ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 60.0 THEN 'uncommon' ││ + ELSE 'common' ││ + END ││ + END ││ + ) ││ + FROM ( ││ + SELECT ││ + rr.run_id, ││ + rr.dungeon_id, ││ + rr.season_id, ││ + rr.ranking, ││ + cr.duration, ││ + MIN(cr.duration) OVER (PARTITION BY rr ││ .dungeon_id, rr.season_id, rr.ranking_scope) ││ as min_duration, ││ + COUNT(*) OVER (PARTITION BY rr.dungeon ││ _id, rr.season_id, rr.ranking_scope) as tota ││ l_in_pool_season_dungeon ││ + FROM run_rankings rr ││ + INNER JOIN challenge_runs cr ON rr.run_ ││ id = cr.id ││ + WHERE rr.ranking_type = 'realm' AND rr. ││ ranking_scope = ? ││ + ) counts ││ + WHERE run_rankings.run_id = counts.run_i ││ d ││ + AND run_rankings.dungeon_id = counts.dun ││ geon_id ││ + AND run_rankings.season_id = counts.seas ││ on_id ││ + AND run_rankings.ranking_type = 'realm' ││ + AND run_rankings.ranking_scope = ? ││ + `, pool.PoolSlug, pool.PoolSlug) ││ + if err != nil { ││ + return err ││ + } ││ + } ││ + ││ + // now compute filtered rankings per pool ││ x dungeon x season ││ + dungeonRows, err := tx.Query("SELECT id FR ││ OM dungeons") ││ + if err != nil { ││ + return err ││ + } ││ + ││ + var dungeonIDs []int ││ + for dungeonRows.Next() { ││ + var id int ││ + if err := dungeonRows.Scan(&id); err != n ││ il { ││ + dungeonRows.Close() ││ + return err ││ + } ││ + dungeonIDs = append(dungeonIDs, id) ││ + } ││ + dungeonRows.Close() ││ + ││ + for _, pool := range pools { ││ + // Get seasons for this pool ││ + seasonRows, err := tx.Query(` ││ + SELECT DISTINCT COALESCE(s_agg.season_nu ││ mber, 1) as season_id ││ + FROM challenge_runs cr ││ + INNER JOIN realms r ON cr.realm_id = r.i ││ d ││ + LEFT JOIN realms parent_r ON r.parent_re ││ alm_slug = parent_r.slug AND r.region = pare ││ nt_r.region ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_numbe ││ r) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.id ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.period_i ││ d ││ + WHERE r.region = ? AND COALESCE(parent_r ││ .slug, r.slug) = ? ││ + `, pool.Region, pool.PoolSlug) ││ + if err != nil { ││ + return err ││ + } ││ + ││ + var seasonIDs []int ││ + for seasonRows.Next() { ││ + var id int ││ + if err := seasonRows.Scan(&id); err != n ││ il { ││ + seasonRows.Close() ││ + return err ││ + } ││ + seasonIDs = append(seasonIDs, id) ││ + } ││ + seasonRows.Close() ││ + ││ + // filtered realm rankings - per dungeon ││ x season using pool-based partitioning ││ + for _, dungeonID := range dungeonIDs { ││ + for _, seasonID := range seasonIDs { ││ + _, err := tx.Exec(` ││ + WITH best_team_runs AS ( ││ + SELECT ││ + cr.team_signature, ││ + MIN(cr.duration) as best_duration ││ + FROM challenge_runs cr ││ + INNER JOIN realms r ON cr.realm_id = ││ r.id ││ + LEFT JOIN realms parent_r ON r.parent ││ _realm_slug = parent_r.slug AND r.region = p ││ arent_r.region ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_nu ││ mber) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.i ││ d ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.perio ││ d_id ││ + WHERE cr.dungeon_id = ? ││ + AND r.region = ? ││ + AND COALESCE(parent_r.slug, r.slug) ││ = ? ││ + AND COALESCE(s_agg.season_number, 1) ││ = ? ││ + GROUP BY cr.team_signature ││ + ), ││ + filtered_runs AS ( ││ + SELECT ││ + cr.id as run_id, ││ + cr.duration, ││ + cr.completed_timestamp, ││ + ROW_NUMBER() OVER (ORDER BY cr.durat ││ ion ASC, cr.completed_timestamp ASC) as filt ││ ered_rank ││ + FROM challenge_runs cr ││ + INNER JOIN realms r ON cr.realm_id = ││ r.id ││ + LEFT JOIN realms parent_r ON r.parent ││ _realm_slug = parent_r.slug AND r.region = p ││ arent_r.region ││ + LEFT JOIN ( ││ + SELECT ps.period_id, MIN(s.season_nu ││ mber) as season_number ││ + FROM period_seasons ps ││ + JOIN seasons s ON ps.season_id = s.i ││ d ││ + GROUP BY ps.period_id ││ + ) s_agg ON cr.period_id = s_agg.perio ││ d_id ││ + INNER JOIN best_team_runs btr ON cr.t ││ eam_signature = btr.team_signature ││ + AND cr.duration = btr.best_du ││ ration ││ + WHERE cr.dungeon_id = ? ││ + AND r.region = ? ││ + AND COALESCE(parent_r.slug, r.slug) ││ = ? ││ + AND COALESCE(s_agg.season_number, 1) ││ = ? ││ + GROUP BY cr.team_signature ││ + HAVING cr.id = MIN(cr.id) ││ + ) ││ + INSERT INTO run_rankings (run_id, dung ││ eon_id, ranking_type, ranking_scope, ranking ││ , season_id, computed_at) ││ + SELECT ││ + run_id, ││ + ? as dungeon_id, ││ + 'realm' as ranking_type, ││ + ? as ranking_scope, ││ + filtered_rank as ranking, ││ + ? as season_id, ││ + ? as computed_at ││ + FROM filtered_runs ││ + `, dungeonID, pool.Region, pool.PoolSlu ││ g, seasonID, ││ + dungeonID, pool.Region, pool.PoolSlug, ││ seasonID, ││ + dungeonID, pool.PoolSlug+"_filtered", ││ seasonID, currentTime) ││ + ││ + if err != nil { ││ + return err ││ + } ││ + } ││ + } ││ + ││ + // update percentile brackets for filtere ││ d realm rankings ││ + filteredScope := pool.PoolSlug + "_filter ││ ed" ││ + _, err = tx.Exec(` ││ + UPDATE run_rankings ││ + SET percentile_bracket = ( ││ + CASE ││ + WHEN counts.duration = counts.min_dura ││ tion THEN 'artifact' ││ + ELSE ││ + CASE ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 1.0 THEN 'excellent' ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 5.0 THEN 'legendary' ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 20.0 THEN 'epic' ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 40.0 THEN 'rare' ││ + WHEN (CAST(counts.ranking AS REAL) / ││ CAST(counts.total_in_pool_season_dungeon AS ││ REAL) * 100) <= 60.0 THEN 'uncommon' ││ + ELSE 'common' ││ + END ││ + END ││ + ) ││ + FROM ( ││ + SELECT ││ + rr.run_id, ││ + rr.dungeon_id, ││ + rr.season_id, ││ + rr.ranking, ││ + cr.duration, ││ + MIN(cr.duration) OVER (PARTITION BY rr ││ .dungeon_id, rr.season_id, rr.ranking_scope) ││ as min_duration, ││ + COUNT(*) OVER (PARTITION BY rr.dungeon ││ _id, rr.season_id, rr.ranking_scope) as tota ││ l_in_pool_season_dungeon ││ + FROM run_rankings rr ││ + INNER JOIN challenge_runs cr ON rr.run_ ││ id = cr.id ││ + WHERE rr.ranking_type = 'realm' AND rr. ││ ranking_scope = ? ││ + ) counts ││ + WHERE run_rankings.run_id = counts.run_i ││ d ││ + AND run_rankings.dungeon_id = counts.dun ││ geon_id ││ + AND run_rankings.season_id = counts.seas ││ on_id ││ + AND run_rankings.ranking_type = 'realm' ││ + AND run_rankings.ranking_scope = ? ││ + `, filteredScope, filteredScope) ││ + if err != nil { ││ + return err ││ + } ││ + } ││ + ││ + fmt.Printf("[OK] Computed realm rankings w ││ ith percentile brackets for %d realm pools\n ││ ", len(pools)) ││ + return nil ││ +} │└──────────────────────────────────────────────┘