OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      b15d5b8d9c9d
DATE      2025-12-25
SUBJECT   ookstats: cleanup player ranking duplication
FILES     3 CHANGED
HASH      b15d5b8d9c9d
DATE      2025-12-25
SUBJECT   ookstats: cleanup player ranking
          duplication
FILES     3 CHANGED
 

diff --git a/nix/pkgs/ookstats/src/cmd/cleanup.go b/nix/pkgs/ookstats/src/cmd/clea
nup.go
new file mode 100644
index 0000000..fd979c9
--- /dev/null
+++ b/nix/pkgs/ookstats/src/cmd/cleanup.go
@@ -0,0 +1,150 @@
+package cmd
+
+import (
+	"database/sql"
+	"fmt"
+	"time"
+
+	"github.com/charmbracelet/log"
+	"github.com/spf13/cobra"
+	"ookstats/internal/database"
+)
+
+var cleanupCmd = &cobra.Command{
+	Use:   "cleanup",
+	Short: "Database cleanup and maintenance operations",
+	Long:  `Perform various database cleanup and maintenance operations to optimize 
space and performance.`,
+}
+
+var cleanupPlayerRankingsCmd = &cobra.Command{
+	Use:   "player-rankings",
+	Short: "Remove duplicate player_rankings entries",
+	Long: `Removes duplicate player_rankings entries, keeping only the most recent r
anking
+for each (player_id, ranking_type, ranking_scope) combination.
+
+This cleanup is safe to run and will significantly reduce database size if duplic
ates exist.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		dryRun, _ := cmd.Flags().GetBool("dry-run")
+		vacuum, _ := cmd.Flags().GetBool("vacuum")
+
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("db connect: %w", err)
+		}
+		defer db.Close()
+
+		return runCleanupPlayerRankings(db, dryRun, vacuum)
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(cleanupCmd)
+	cleanupCmd.AddCommand(cleanupPlayerRankingsCmd)
+
+	cleanupPlayerRankingsCmd.Flags().Bool("dry-run", false, "Show what would be dele
ted without actually deleting")
+	cleanupPlayerRankingsCmd.Flags().Bool("vacuum", true, "Run VACUUM after cleanup 
to reclaim space")
+}
+
+func runCleanupPlayerRankings(db *sql.DB, dryRun bool, vacuum bool) error {
+	log.Info("analyzing player_rankings table")
+
+	// count total rows
+	var totalRows int64
+	if err := db.QueryRow("SELECT COUNT(*) FROM player_rankings").Scan(&totalRows); 
err != nil {
+		return fmt.Errorf("count total rows: %w", err)
+	}
+	log.Info("current state", "total_rows", totalRows)
+
+	// Count unique combinations
+	var uniqueCombos int64
+	if err := db.QueryRow(`
+		SELECT COUNT(DISTINCT player_id || '|' || ranking_type || '|' || ranking_scope)
+		FROM player_rankings
+	`).Scan(&uniqueCombos); err != nil {
+		return fmt.Errorf("count unique combos: %w", err)
+	}
+
+	duplicateRows := totalRows - uniqueCombos
+	log.Info("duplicate analysis",
+		"unique_combinations", uniqueCombos,
+		"duplicate_rows", duplicateRows,
+		"duplicate_percentage", fmt.Sprintf("%.1f%%", float64(duplicateRows)/float64(to
talRows)*100))
+
+	if duplicateRows == 0 {
+		log.Info("no duplicates found - table is clean!")
+		return nil
+	}
+
+	// count exact duplicates (same player, type, scope, AND rank)
+	var exactDuplicates int64
+	if err := db.QueryRow(`
+		SELECT COUNT(*) FROM (
+			SELECT player_id, ranking_type, ranking_scope, ranking, COUNT(*) as cnt
+			FROM player_rankings
+			GROUP BY player_id, ranking_type, ranking_scope, ranking
+			HAVING cnt > 1
+		)
+	`).Scan(&exactDuplicates); err != nil {
+		return fmt.Errorf("count exact duplicates: %w", err)
+	}
+	if exactDuplicates > 0 {
+		log.Warn("exact duplicates found",
+			"count", exactDuplicates,
+			"note", "same player/type/scope/rank appearing multiple times")
+	}
+
+	if dryRun {
+		log.Info("[DRY RUN] would delete", "rows", duplicateRows)
+		log.Info("[DRY RUN] would keep", "rows", uniqueCombos)
+		estimatedSizeMB := float64(duplicateRows) * 100 / 1024 / 1024 // rough estimate
+		log.Info("[DRY RUN] estimated space savings", "mb", fmt.Sprintf("%.2f MB", esti
matedSizeMB))
+		return nil
+	}
+
+	log.Info("starting cleanup - this may take a few minutes...")
+	startTime := time.Now()
+
+	// delete duplicates, keeping only the row with the highest rowid (most recent)
+	result, err := db.Exec(`
+		DELETE FROM player_rankings
+		WHERE rowid NOT IN (
+			SELECT MAX(rowid)
+			FROM player_rankings
+			GROUP BY player_id, ranking_type, ranking_scope
+		)
+	`)
+	if err != nil {
+		return fmt.Errorf("delete duplicates: %w", err)
+	}
+
+	rowsDeleted, _ := result.RowsAffected()
+	duration := time.Since(startTime)
+	log.Info("cleanup complete",
+		"rows_deleted", rowsDeleted,
+		"duration", duration.Round(time.Millisecond))
+
+	// verify cleanup
+	var newTotal int64
+	if err := db.QueryRow("SELECT COUNT(*) FROM player_rankings").Scan(&newTotal); e
rr != nil {
+		return fmt.Errorf("count after cleanup: %w", err)
+	}
+	log.Info("verification",
+		"rows_before", totalRows,
+		"rows_after", newTotal,
+		"rows_removed", totalRows-newTotal)
+
+	if vacuum {
+		log.Info("running VACUUM to reclaim disk space...")
+		vacuumStart := time.Now()
+		if _, err := db.Exec("VACUUM"); err != nil {
+			log.Warn("vacuum failed", "error", err)
+		} else {
+			log.Info("vacuum complete", "duration", time.Since(vacuumStart).Round(time.Mil
lisecond))
+		}
+	} else {
+		log.Info("skipping VACUUM (use --vacuum to reclaim disk space)")
+	}
+
+	log.Info("[SUCCESS] player_rankings cleanup completed successfully!")
+	return nil
+}

diff --git a/nix/pkgs/ookstats/src/cmd/clean
up.go b/nix/pkgs/ookstats/src/cmd/cleanup.go
new file mode 100644
index 0000000..fd979c9
--- /dev/null
+++ b/nix/pkgs/ookstats/src/cmd/cleanup.go
@@ -0,0 +1,150 @@
+package cmd
+
+import (
+	"database/sql"
+	"fmt"
+	"time"
+
+	"github.com/charmbracelet/log"
+	"github.com/spf13/cobra"
+	"ookstats/internal/database"
+)
+
+var cleanupCmd = &cobra.Command{
+	Use:   "cleanup",
+	Short: "Database cleanup and maintenance o
perations",
+	Long:  `Perform various database cleanup a
nd maintenance operations to optimize space 
and performance.`,
+}
+
+var cleanupPlayerRankingsCmd = &cobra.Comma
nd{
+	Use:   "player-rankings",
+	Short: "Remove duplicate player_rankings e
ntries",
+	Long: `Removes duplicate player_rankings e
ntries, keeping only the most recent ranking
+for each (player_id, ranking_type, ranking_
scope) combination.
+
+This cleanup is safe to run and will signif
icantly reduce database size if duplicates e
xist.`,
+	RunE: func(cmd *cobra.Command, args []stri
ng) error {
+		dryRun, _ := cmd.Flags().GetBool("dry-run
")
+		vacuum, _ := cmd.Flags().GetBool("vacuum"
)
+
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("db connect: %w", err)
+		}
+		defer db.Close()
+
+		return runCleanupPlayerRankings(db, dryRu
n, vacuum)
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(cleanupCmd)
+	cleanupCmd.AddCommand(cleanupPlayerRanking
sCmd)
+
+	cleanupPlayerRankingsCmd.Flags().Bool("dry
-run", false, "Show what would be deleted wi
thout actually deleting")
+	cleanupPlayerRankingsCmd.Flags().Bool("vac
uum", true, "Run VACUUM after cleanup to rec
laim space")
+}
+
+func runCleanupPlayerRankings(db *sql.DB, d
ryRun bool, vacuum bool) error {
+	log.Info("analyzing player_rankings table"
)
+
+	// count total rows
+	var totalRows int64
+	if err := db.QueryRow("SELECT COUNT(*) FRO
M player_rankings").Scan(&totalRows); err !=
 nil {
+		return fmt.Errorf("count total rows: %w",
 err)
+	}
+	log.Info("current state", "total_rows", to
talRows)
+
+	// Count unique combinations
+	var uniqueCombos int64
+	if err := db.QueryRow(`
+		SELECT COUNT(DISTINCT player_id || '|' ||
 ranking_type || '|' || ranking_scope)
+		FROM player_rankings
+	`).Scan(&uniqueCombos); err != nil {
+		return fmt.Errorf("count unique combos: %
w", err)
+	}
+
+	duplicateRows := totalRows - uniqueCombos
+	log.Info("duplicate analysis",
+		"unique_combinations", uniqueCombos,
+		"duplicate_rows", duplicateRows,
+		"duplicate_percentage", fmt.Sprintf("%.1f
%%", float64(duplicateRows)/float64(totalRow
s)*100))
+
+	if duplicateRows == 0 {
+		log.Info("no duplicates found - table is 
clean!")
+		return nil
+	}
+
+	// count exact duplicates (same player, ty
pe, scope, AND rank)
+	var exactDuplicates int64
+	if err := db.QueryRow(`
+		SELECT COUNT(*) FROM (
+			SELECT player_id, ranking_type, ranking_
scope, ranking, COUNT(*) as cnt
+			FROM player_rankings
+			GROUP BY player_id, ranking_type, rankin
g_scope, ranking
+			HAVING cnt > 1
+		)
+	`).Scan(&exactDuplicates); err != nil {
+		return fmt.Errorf("count exact duplicates
: %w", err)
+	}
+	if exactDuplicates > 0 {
+		log.Warn("exact duplicates found",
+			"count", exactDuplicates,
+			"note", "same player/type/scope/rank app
earing multiple times")
+	}
+
+	if dryRun {
+		log.Info("[DRY RUN] would delete", "rows"
, duplicateRows)
+		log.Info("[DRY RUN] would keep", "rows", 
uniqueCombos)
+		estimatedSizeMB := float64(duplicateRows)
 * 100 / 1024 / 1024 // rough estimate
+		log.Info("[DRY RUN] estimated space savin
gs", "mb", fmt.Sprintf("%.2f MB", estimatedS
izeMB))
+		return nil
+	}
+
+	log.Info("starting cleanup - this may take
 a few minutes...")
+	startTime := time.Now()
+
+	// delete duplicates, keeping only the row
 with the highest rowid (most recent)
+	result, err := db.Exec(`
+		DELETE FROM player_rankings
+		WHERE rowid NOT IN (
+			SELECT MAX(rowid)
+			FROM player_rankings
+			GROUP BY player_id, ranking_type, rankin
g_scope
+		)
+	`)
+	if err != nil {
+		return fmt.Errorf("delete duplicates: %w"
, err)
+	}
+
+	rowsDeleted, _ := result.RowsAffected()
+	duration := time.Since(startTime)
+	log.Info("cleanup complete",
+		"rows_deleted", rowsDeleted,
+		"duration", duration.Round(time.Milliseco
nd))
+
+	// verify cleanup
+	var newTotal int64
+	if err := db.QueryRow("SELECT COUNT(*) FRO
M player_rankings").Scan(&newTotal); err != 
nil {
+		return fmt.Errorf("count after cleanup: %
w", err)
+	}
+	log.Info("verification",
+		"rows_before", totalRows,
+		"rows_after", newTotal,
+		"rows_removed", totalRows-newTotal)
+
+	if vacuum {
+		log.Info("running VACUUM to reclaim disk 
space...")
+		vacuumStart := time.Now()
+		if _, err := db.Exec("VACUUM"); err != ni
l {
+			log.Warn("vacuum failed", "error", err)
+		} else {
+			log.Info("vacuum complete", "duration", 
time.Since(vacuumStart).Round(time.Milliseco
nd))
+		}
+	} else {
+		log.Info("skipping VACUUM (use --vacuum t
o reclaim disk space)")
+	}
+
+	log.Info("[SUCCESS] player_rankings cleanu
p completed successfully!")
+	return nil
+}
 

diff --git a/nix/pkgs/ookstats/src/internal/database/client.go b/nix/pkgs/ookstats
/src/internal/database/client.go
index 34821d0..3ff3b44 100644
--- a/nix/pkgs/ookstats/src/internal/database/client.go
+++ b/nix/pkgs/ookstats/src/internal/database/client.go
@@ -242,12 +242,13 @@ func EnsureCompleteSchema(db *sql.DB) error {
 		)`,
 
 		`CREATE TABLE IF NOT EXISTS player_rankings (
-			player_id INTEGER,
-			ranking_type TEXT,
-			ranking_scope TEXT,
+			player_id INTEGER NOT NULL,
+			ranking_type TEXT NOT NULL,
+			ranking_scope TEXT NOT NULL,
 			ranking INTEGER,
 			combined_best_time INTEGER,
-			last_updated INTEGER
+			last_updated INTEGER,
+			PRIMARY KEY (player_id, ranking_type, ranking_scope)
 		)`,
 
 		`CREATE TABLE IF NOT EXISTS player_seasonal_rankings (
@@ -403,6 +404,11 @@ func EnsureCompleteSchema(db *sql.DB) error {
 		return err
 	}
 
+	// Migrate player_rankings to add PRIMARY KEY constraint
+	if err := migratePlayerRankingsPrimaryKey(db); err != nil {
+		return err
+	}
+
 	// Create indexes
 	return ensureRecommendedIndexes(db)
 }
@@ -435,6 +441,9 @@ func ensureRecommendedIndexes(db *sql.DB) error {
 		"CREATE INDEX IF NOT EXISTS idx_player_best_runs_season ON player_best_runs(sea
son_id, player_id)",
 		"CREATE INDEX IF NOT EXISTS idx_player_profiles_season ON player_profiles(seaso
n_id, global_ranking)",
 		"CREATE INDEX IF NOT EXISTS idx_player_profiles_season_coverage ON player_profi
les(season_id, has_complete_coverage, combined_best_time)",
+		// Player rankings indexes
+		"CREATE INDEX IF NOT EXISTS idx_player_rankings_scope ON player_rankings(rankin
g_type, ranking_scope)",
+		"CREATE INDEX IF NOT EXISTS idx_player_rankings_player ON player_rankings(playe
r_id)",
 	}
 	for _, s := range stmts {
 		if _, err := db.Exec(s); err != nil {
@@ -641,3 +650,80 @@ func columnExists(db *sql.DB, table, column string) (bool, er
ror) {
 	}
 	return false, rows.Err()
 }
+
+// migratePlayerRankingsPrimaryKey adds PRIMARY KEY constraint to player_rankings
 table if missing
+func migratePlayerRankingsPrimaryKey(db *sql.DB) error {
+	// Check if table already has PRIMARY KEY by inspecting schema
+	var createSQL string
+	err := db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='p
layer_rankings'`).Scan(&createSQL)
+	if err != nil {
+		// Table doesn't exist yet, skip migration
+		return nil
+	}
+
+	// If PRIMARY KEY already exists, skip migration
+	if strings.Contains(strings.ToUpper(createSQL), "PRIMARY KEY") {
+		return nil
+	}
+
+	fmt.Printf("Migrating player_rankings table to add PRIMARY KEY constraint...\n")
+
+	// Begin transaction for safe migration
+	tx, err := db.Begin()
+	if err != nil {
+		return fmt.Errorf("begin transaction: %w", err)
+	}
+	defer tx.Rollback()
+
+	// Create new table with PRIMARY KEY
+	if _, err := tx.Exec(`
+		CREATE TABLE player_rankings_new (
+			player_id INTEGER NOT NULL,
+			ranking_type TEXT NOT NULL,
+			ranking_scope TEXT NOT NULL,
+			ranking INTEGER,
+			combined_best_time INTEGER,
+			last_updated INTEGER,
+			PRIMARY KEY (player_id, ranking_type, ranking_scope)
+		)
+	`); err != nil {
+		return fmt.Errorf("create new table: %w", err)
+	}
+
+	// Copy data, keeping only the most recent entry per (player_id, ranking_type, r
anking_scope)
+	if _, err := tx.Exec(`
+		INSERT INTO player_rankings_new
+		SELECT
+			player_id,
+			ranking_type,
+			ranking_scope,
+			ranking,
+			combined_best_time,
+			last_updated
+		FROM player_rankings
+		WHERE rowid IN (
+			SELECT MAX(rowid)
+			FROM player_rankings
+			GROUP BY player_id, ranking_type, ranking_scope
+		)
+	`); err != nil {
+		return fmt.Errorf("copy data: %w", err)
+	}
+
+	// Drop old table
+	if _, err := tx.Exec(`DROP TABLE player_rankings`); err != nil {
+		return fmt.Errorf("drop old table: %w", err)
+	}
+
+	// Rename new table
+	if _, err := tx.Exec(`ALTER TABLE player_rankings_new RENAME TO player_rankings`
); err != nil {
+		return fmt.Errorf("rename new table: %w", err)
+	}
+
+	if err := tx.Commit(); err != nil {
+		return fmt.Errorf("commit transaction: %w", err)
+	}
+
+	fmt.Printf("[OK] player_rankings table migrated - added PRIMARY KEY and removed 
duplicates\n")
+	return nil
+}

diff --git a/nix/pkgs/ookstats/src/internal/
database/client.go b/nix/pkgs/ookstats/src/i
nternal/database/client.go
index 34821d0..3ff3b44 100644
--- a/nix/pkgs/ookstats/src/internal/databas
e/client.go
+++ b/nix/pkgs/ookstats/src/internal/databas
e/client.go
@@ -242,12 +242,13 @@ func EnsureCompleteSch
ema(db *sql.DB) error {
 		)`,
 
 		`CREATE TABLE IF NOT EXISTS player_rankin
gs (
-			player_id INTEGER,
-			ranking_type TEXT,
-			ranking_scope TEXT,
+			player_id INTEGER NOT NULL,
+			ranking_type TEXT NOT NULL,
+			ranking_scope TEXT NOT NULL,
 			ranking INTEGER,
 			combined_best_time INTEGER,
-			last_updated INTEGER
+			last_updated INTEGER,
+			PRIMARY KEY (player_id, ranking_type, ra
nking_scope)
 		)`,
 
 		`CREATE TABLE IF NOT EXISTS player_season
al_rankings (
@@ -403,6 +404,11 @@ func EnsureCompleteSche
ma(db *sql.DB) error {
 		return err
 	}
 
+	// Migrate player_rankings to add PRIMARY 
KEY constraint
+	if err := migratePlayerRankingsPrimaryKey(
db); err != nil {
+		return err
+	}
+
 	// Create indexes
 	return ensureRecommendedIndexes(db)
 }
@@ -435,6 +441,9 @@ func ensureRecommendedIn
dexes(db *sql.DB) error {
 		"CREATE INDEX IF NOT EXISTS idx_player_be
st_runs_season ON player_best_runs(season_id
, player_id)",
 		"CREATE INDEX IF NOT EXISTS idx_player_pr
ofiles_season ON player_profiles(season_id, 
global_ranking)",
 		"CREATE INDEX IF NOT EXISTS idx_player_pr
ofiles_season_coverage ON player_profiles(se
ason_id, has_complete_coverage, combined_bes
t_time)",
+		// Player rankings indexes
+		"CREATE INDEX IF NOT EXISTS idx_player_ra
nkings_scope ON player_rankings(ranking_type
, ranking_scope)",
+		"CREATE INDEX IF NOT EXISTS idx_player_ra
nkings_player ON player_rankings(player_id)"
,
 	}
 	for _, s := range stmts {
 		if _, err := db.Exec(s); err != nil {
@@ -641,3 +650,80 @@ func columnExists(db *s
ql.DB, table, column string) (bool, error) {
 	}
 	return false, rows.Err()
 }
+
+// migratePlayerRankingsPrimaryKey adds PRI
MARY KEY constraint to player_rankings table
 if missing
+func migratePlayerRankingsPrimaryKey(db *sq
l.DB) error {
+	// Check if table already has PRIMARY KEY 
by inspecting schema
+	var createSQL string
+	err := db.QueryRow(`SELECT sql FROM sqlite
_master WHERE type='table' AND name='player_
rankings'`).Scan(&createSQL)
+	if err != nil {
+		// Table doesn't exist yet, skip migratio
n
+		return nil
+	}
+
+	// If PRIMARY KEY already exists, skip mig
ration
+	if strings.Contains(strings.ToUpper(create
SQL), "PRIMARY KEY") {
+		return nil
+	}
+
+	fmt.Printf("Migrating player_rankings tabl
e to add PRIMARY KEY constraint...\n")
+
+	// Begin transaction for safe migration
+	tx, err := db.Begin()
+	if err != nil {
+		return fmt.Errorf("begin transaction: %w"
, err)
+	}
+	defer tx.Rollback()
+
+	// Create new table with PRIMARY KEY
+	if _, err := tx.Exec(`
+		CREATE TABLE player_rankings_new (
+			player_id INTEGER NOT NULL,
+			ranking_type TEXT NOT NULL,
+			ranking_scope TEXT NOT NULL,
+			ranking INTEGER,
+			combined_best_time INTEGER,
+			last_updated INTEGER,
+			PRIMARY KEY (player_id, ranking_type, ra
nking_scope)
+		)
+	`); err != nil {
+		return fmt.Errorf("create new table: %w",
 err)
+	}
+
+	// Copy data, keeping only the most recent
 entry per (player_id, ranking_type, ranking
_scope)
+	if _, err := tx.Exec(`
+		INSERT INTO player_rankings_new
+		SELECT
+			player_id,
+			ranking_type,
+			ranking_scope,
+			ranking,
+			combined_best_time,
+			last_updated
+		FROM player_rankings
+		WHERE rowid IN (
+			SELECT MAX(rowid)
+			FROM player_rankings
+			GROUP BY player_id, ranking_type, rankin
g_scope
+		)
+	`); err != nil {
+		return fmt.Errorf("copy data: %w", err)
+	}
+
+	// Drop old table
+	if _, err := tx.Exec(`DROP TABLE player_ra
nkings`); err != nil {
+		return fmt.Errorf("drop old table: %w", e
rr)
+	}
+
+	// Rename new table
+	if _, err := tx.Exec(`ALTER TABLE player_r
ankings_new RENAME TO player_rankings`); err
 != nil {
+		return fmt.Errorf("rename new table: %w",
 err)
+	}
+
+	if err := tx.Commit(); err != nil {
+		return fmt.Errorf("commit transaction: %w
", err)
+	}
+
+	fmt.Printf("[OK] player_rankings table mig
rated - added PRIMARY KEY and removed duplic
ates\n")
+	return nil
+}
 

diff --git a/nix/pkgs/ookstats/src/internal/pipeline/process.go b/nix/pkgs/ookstat
s/src/internal/pipeline/process.go
index 89a7455..b3feee8 100644
--- a/nix/pkgs/ookstats/src/internal/pipeline/process.go
+++ b/nix/pkgs/ookstats/src/internal/pipeline/process.go
@@ -417,7 +417,7 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 		}
 
 		_, err = tx.Exec(`
-			INSERT INTO player_rankings (player_id, ranking_type, ranking_scope, ranking, 
combined_best_time, last_updated)
+			INSERT OR REPLACE INTO player_rankings (player_id, ranking_type, ranking_scope
, ranking, combined_best_time, last_updated)
 			SELECT
 				player_id, 'best_overall', 'global', global_ranking, combined_best_time, ?
 			FROM player_profiles
@@ -473,7 +473,7 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 		}
 
 		_, err = tx.Exec(`
-			INSERT INTO player_rankings (player_id, ranking_type, ranking_scope, ranking, 
combined_best_time, last_updated)
+			INSERT OR REPLACE INTO player_rankings (player_id, ranking_type, ranking_scope
, ranking, combined_best_time, last_updated)
 			SELECT
 				pp.player_id, 'best_overall', r.region, pp.regional_ranking, pp.combined_best
_time, ?
 			FROM player_profiles pp
@@ -548,7 +548,7 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 		}
 
 		_, err = tx.Exec(`
-			INSERT INTO player_rankings (player_id, ranking_type, ranking_scope, ranking, 
combined_best_time, last_updated)
+			INSERT OR REPLACE INTO player_rankings (player_id, ranking_type, ranking_scope
, ranking, combined_best_time, last_updated)
 			SELECT
 				player_id, 'best_overall', CAST(realm_id AS TEXT), realm_ranking, combined_be
st_time, ?
 			FROM player_profiles

diff --git a/nix/pkgs/ookstats/src/internal/
pipeline/process.go b/nix/pkgs/ookstats/src/
internal/pipeline/process.go
index 89a7455..b3feee8 100644
--- a/nix/pkgs/ookstats/src/internal/pipelin
e/process.go
+++ b/nix/pkgs/ookstats/src/internal/pipelin
e/process.go
@@ -417,7 +417,7 @@ func computePlayerRankin
gs(tx *sql.Tx) (int, error) {
 		}
 
 		_, err = tx.Exec(`
-			INSERT INTO player_rankings (player_id, 
ranking_type, ranking_scope, ranking, combin
ed_best_time, last_updated)
+			INSERT OR REPLACE INTO player_rankings (
player_id, ranking_type, ranking_scope, rank
ing, combined_best_time, last_updated)
 			SELECT
 				player_id, 'best_overall', 'global', gl
obal_ranking, combined_best_time, ?
 			FROM player_profiles
@@ -473,7 +473,7 @@ func computePlayerRankin
gs(tx *sql.Tx) (int, error) {
 		}
 
 		_, err = tx.Exec(`
-			INSERT INTO player_rankings (player_id, 
ranking_type, ranking_scope, ranking, combin
ed_best_time, last_updated)
+			INSERT OR REPLACE INTO player_rankings (
player_id, ranking_type, ranking_scope, rank
ing, combined_best_time, last_updated)
 			SELECT
 				pp.player_id, 'best_overall', r.region,
 pp.regional_ranking, pp.combined_best_time,
 ?
 			FROM player_profiles pp
@@ -548,7 +548,7 @@ func computePlayerRankin
gs(tx *sql.Tx) (int, error) {
 		}
 
 		_, err = tx.Exec(`
-			INSERT INTO player_rankings (player_id, 
ranking_type, ranking_scope, ranking, combin
ed_best_time, last_updated)
+			INSERT OR REPLACE INTO player_rankings (
player_id, ranking_type, ranking_scope, rank
ing, combined_best_time, last_updated)
 			SELECT
 				player_id, 'best_overall', CAST(realm_i
d AS TEXT), realm_ranking, combined_best_tim
e, ?
 			FROM player_profiles
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET