OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 365 LINES
 
[ HISTORY ]  [ UP ]
 

package database

import (
	"context"
	"database/sql"
	"fmt"
	"ookstats/internal/blizzard"
	"ookstats/internal/utils"
	"strings"
	"time"
)

// InsertLeaderboardData inserts leaderboard data and returns the number of runs a
nd players inserted
func (ds *DatabaseService) InsertLeaderboardData(leaderboard *blizzard.Leaderboard
Response, realmInfo blizzard.RealmInfo, dungeon blizzard.DungeonInfo) (int, int, e
rror) {
	if leaderboard == nil || len(leaderboard.LeadingGroups) == 0 {
		return 0, 0, nil
	}

	tx, err := ds.db.Begin()
	if err != nil {
		return 0, 0, fmt.Errorf("failed to begin transaction: %w", err)
	}
	defer tx.Rollback()

	runs, players, err := ds.insertLeaderboardDataTx(tx, leaderboard, realmInfo, dung
eon)
	if err != nil {
		return 0, 0, err
	}

	if err := tx.Commit(); err != nil {
		return 0, 0, fmt.Errorf("failed to commit transaction: %w", err)
	}
	return runs, players, nil
}

// BatchProcessFetchResults processes fetch results concurrently with transaction 
batching
func (ds *DatabaseService) BatchProcessFetchResults(ctx context.Context, results <
-chan blizzard.FetchResult) (int, int, error) {
	totalRuns := 0
	totalPlayers := 0
	processedCount := 0
	errorCount := 0

	batch := make([]blizzard.FetchResult, 0, 10)
	batchNumber := 0

	fmt.Printf("[INFO] Starting to process API results...\n")
	for result := range results {
		processedCount++
		ds.recordFetchStatusResult(result)

		if result.Error != nil {
			errorCount++
			if verbose {
				fmt.Printf("[ERROR] API error [%d] %s/%s: %v\n", processedCount, result.RealmI
nfo.Name, result.Dungeon.Name, result.Error)
			} else if !strings.Contains(strings.ToLower(result.Error.Error()), "404") {
				fmt.Printf("[ERROR] API error [%d] %s/%s: %v\n", processedCount, result.RealmI
nfo.Name, result.Dungeon.Name, result.Error)
			}
			continue
		}

		batch = append(batch, result)

		if len(batch) >= 10 {
			batchNumber++
			runs, players, err := ds.processBatch(batch)
			if err != nil {
				fmt.Printf("[ERROR] Batch %d failed: %v\n", batchNumber, err)
			} else {
				totalRuns += runs
				totalPlayers += players
				if runs > 0 || players > 0 {
					fmt.Printf("[INFO] Batch %d: +%d runs, +%d players (total: %d runs, %d player
s)\n",
						batchNumber, runs, players, totalRuns, totalPlayers)
				}
			}
			batch = batch[:0]
		}

		if processedCount%10 == 0 {
			fmt.Printf("[INFO] Progress: %d requests processed, %d errors\n", processedCoun
t, errorCount)
		}

		select {
		case <-ctx.Done():
			fmt.Printf("[WARN] Context cancelled, stopping processing\n")
			return totalRuns, totalPlayers, ctx.Err()
		default:
		}
	}

	if len(batch) > 0 {
		batchNumber++
		runs, players, err := ds.processBatch(batch)
		if err != nil {
			fmt.Printf("[ERROR] Final batch %d failed: %v\n", batchNumber, err)
		} else {
			totalRuns += runs
			totalPlayers += players
			if runs > 0 || players > 0 {
				fmt.Printf("[INFO] Final batch %d: +%d runs, +%d players\n", batchNumber, runs
, players)
			}
		}
	}

	fmt.Printf("\n[INFO] Final stats: %d requests processed, %d errors, %d runs, %d p
layers\n",
		processedCount, errorCount, totalRuns, totalPlayers)

	return totalRuns, totalPlayers, nil
}

// processBatch processes a batch of fetch results in a single transaction
func (ds *DatabaseService) processBatch(batch []blizzard.FetchResult) (int, int, e
rror) {
	if len(batch) == 0 {
		return 0, 0, nil
	}

	type batchItem struct {
		idx   int
		r     blizzard.RealmInfo
		d     blizzard.DungeonInfo
		board *blizzard.LeaderboardResponse
	}

	items := make([]batchItem, 0, len(batch))
	for i, res := range batch {
		if res.Leaderboard == nil || len(res.Leaderboard.LeadingGroups) == 0 {
			continue
		}
		items = append(items, batchItem{
			idx:   i + 1,
			r:     res.RealmInfo,
			d:     res.Dungeon,
			board: res.Leaderboard,
		})
	}

	if len(items) == 0 {
		return 0, 0, nil
	}

	startBatch := time.Now()
	tx, err := ds.db.Begin()
	if err != nil {
		return 0, 0, fmt.Errorf("failed to begin batch transaction: %w", err)
	}
	defer tx.Rollback()

	totalRuns := 0
	totalPlayers := 0

	for _, it := range items {
		itemStart := time.Now()
		runs, players, err := ds.insertLeaderboardDataTx(tx, it.board, it.r, it.d)
		if err != nil {
			return 0, 0, fmt.Errorf("failed to insert leaderboard data: %w", err)
		}
		totalRuns += runs
		totalPlayers += players
		if runs > 0 || players > 0 {
			fmt.Printf("    - Batch item %d: %s/%s -> +%d runs, +%d players in %dms\n",
				it.idx, it.r.Name, it.d.Name, runs, players, time.Since(itemStart).Millisecond
s())
		}
	}

	if err := tx.Commit(); err != nil {
		return 0, 0, fmt.Errorf("failed to commit batch transaction: %w", err)
	}
	fmt.Printf("    [OK] Batch committed in %dms (total +%d runs, +%d players)\n",
		time.Since(startBatch).Milliseconds(), totalRuns, totalPlayers)
	return totalRuns, totalPlayers, nil
}

// insertLeaderboardDataTx inserts leaderboard data within a transaction
func (ds *DatabaseService) insertLeaderboardDataTx(tx *sql.Tx, leaderboard *blizza
rd.LeaderboardResponse, realmInfo blizzard.RealmInfo, dungeon blizzard.DungeonInfo
) (int, int, error) {
	realmID, err := ds.getRealmIDTx(tx, realmInfo.Slug, realmInfo.Region)
	if err != nil {
		return 0, 0, fmt.Errorf("failed to get realm ID: %w", err)
	}
	dungeonID, err := ds.getDungeonIDTx(tx, dungeon.Slug)
	if err != nil {
		return 0, 0, fmt.Errorf("failed to get dungeon ID: %w", err)
	}

	// compute newest completed_timestamp in this leaderboard (for marker update)
	maxCT := int64(0)
	for _, run := range leaderboard.LeadingGroups {
		if run.CompletedTimestamp > maxCT {
			maxCT = run.CompletedTimestamp
		}
	}

	runsInserted := 0
	playersInserted := 0

	for _, run := range leaderboard.LeadingGroups {
		var playerIDs []int
		for _, member := range run.Members {
			if id, ok := member.GetPlayerID(); ok {
				playerIDs = append(playerIDs, id)
			}
		}

		if len(playerIDs) == 0 {
			continue
		}

		teamSignature := utils.ComputeTeamSignature(playerIDs)

		var runSeasonID *int
		if run.CompletedTimestamp > 0 {
			sid, err := ds.determineSeasonForRunTx(tx, realmInfo.Region, run.CompletedTimes
tamp)
			if err != nil {
				return 0, 0, fmt.Errorf("failed to determine season for run: %w", err)
			}
			if sid > 0 {
				runSeasonID = &sid
			}
		}

		runQuery := `
			INSERT OR IGNORE INTO challenge_runs
			(duration, completed_timestamp, keystone_level, dungeon_id, realm_id, period_id
, period_start_timestamp, period_end_timestamp, team_signature, season_id)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		`

		result, err := tx.Exec(runQuery,
			run.Duration,
			run.CompletedTimestamp,
			run.KeystoneLevel,
			dungeonID,
			realmID,
			leaderboard.Period,
			leaderboard.PeriodStartTimestamp,
			leaderboard.PeriodEndTimestamp,
			teamSignature,
			runSeasonID,
		)
		if err != nil {
			return 0, 0, fmt.Errorf("failed to insert run: %w", err)
		}

		rowsAffected, err := result.RowsAffected()
		if err != nil {
			return 0, 0, fmt.Errorf("failed to check rows affected: %w", err)
		}

		if rowsAffected == 0 {
			continue
		}

		runID, err := result.LastInsertId()
		if err != nil {
			return 0, 0, fmt.Errorf("failed to get run ID: %w", err)
		}

		runsInserted++

		for _, member := range run.Members {
			playerID, hasPlayerID := member.GetPlayerID()
			playerName, _ := member.GetPlayerName()
			playerRealmSlug, hasRealmSlug := member.GetRealmSlug()

			if !hasPlayerID {
				continue
			}

			var playerRealmID int
			if hasRealmSlug {
				playerRealmID, err = ds.getRealmIDTx(tx, playerRealmSlug, realmInfo.Region)
				if err != nil {
					return 0, 0, fmt.Errorf("failed to resolve player realm: %w", err)
				}
				if playerRealmID == 0 {
					if _, err := tx.Exec(`INSERT OR IGNORE INTO realms (slug, name, region, conne
cted_realm_id, parent_realm_slug) VALUES (?, ?, ?, NULL, ?)`,
						playerRealmSlug, playerRealmSlug, realmInfo.Region, ""); err != nil {
						return 0, 0, fmt.Errorf("failed to create placeholder realm: %w", err)
					}
					id2, err := ds.getRealmIDTx(tx, playerRealmSlug, realmInfo.Region)
					if err != nil || id2 == 0 {
						return 0, 0, fmt.Errorf("failed to resolve placeholder realm id: %w", err)
					}
					playerRealmID = id2
				}
			} else {
				playerRealmID = realmID
			}

			// check if player with same name+realm already exists (handles faction transfe
rs where Blizzard ID changes)
			existingPlayerID, err := getExistingPlayerIDTx(tx, playerName, playerRealmID)
			if err != nil {
				return 0, 0, fmt.Errorf("failed to check existing player: %w", err)
			}

			effectivePlayerID := playerID

			if existingPlayerID != 0 && existingPlayerID != int64(playerID) {
				// player exists with different ID (likely faction transfer)
				// migrate old runs to the new player ID and mark old player invalid
				_, err := tx.Exec(`UPDATE run_members SET player_id = ? WHERE player_id = ?`, 
playerID, existingPlayerID)
				if err != nil {
					return 0, 0, fmt.Errorf("failed to migrate runs to new player ID: %w", err)
				}
				_, err = tx.Exec(`UPDATE players SET is_valid = 0 WHERE id = ?`, existingPlaye
rID)
				if err != nil {
					return 0, 0, fmt.Errorf("failed to invalidate old player: %w", err)
				}
			}

			playerQuery := `
				INSERT INTO players (id, name, name_lower, realm_id)
				VALUES (?, ?, lower(?), ?)
				ON CONFLICT(id) DO UPDATE SET
				  name = excluded.name,
				  name_lower = lower(excluded.name),
				  realm_id = excluded.realm_id
				WHERE excluded.name != name OR excluded.realm_id != realm_id
			`
			playerResult, err := tx.Exec(playerQuery, playerID, playerName, playerName, pla
yerRealmID)
			if err != nil {
				return 0, 0, fmt.Errorf("failed to insert player: %w", err)
			}

			rowsAffected, err := playerResult.RowsAffected()
			if err != nil {
				return 0, 0, fmt.Errorf("failed to get rows affected: %w", err)
			}
			if rowsAffected > 0 {
				playersInserted++
			}

			specID, _ := member.GetSpecID()
			faction, _ := member.GetFaction()

			memberQuery := `INSERT INTO run_members (run_id, player_id, spec_id, faction) V
ALUES (?, ?, ?, ?)`

			var specPtr *int
			var factionPtr *string
			if specID > 0 {
				specPtr = &specID
			}
			if faction != "" {
				factionPtr = &faction
			}

			_, err = tx.Exec(memberQuery, runID, effectivePlayerID, specPtr, factionPtr)
			if err != nil {
				return 0, 0, fmt.Errorf("failed to insert run member: %w", err)
			}
		}
	}

	// update marker inside the transaction
	if maxCT > 0 {
		if _, err := tx.Exec(`INSERT INTO api_fetch_markers (realm_slug, dungeon_id, per
iod_id, last_completed_ts)
                               VALUES (?, ?, ?, ?)
                               ON CONFLICT(realm_slug, dungeon_id, period_id)
                               DO UPDATE SET last_completed_ts = MAX(last_complete
d_ts, excluded.last_completed_ts)`,
			realmInfo.Slug, dungeonID, leaderboard.Period, maxCT); err != nil {
			return 0, 0, fmt.Errorf("failed to update fetch marker: %w", err)
		}
	}

	return runsInserted, playersInserted, nil
}

package database

import (
	"context"
	"database/sql"
	"fmt"
	"ookstats/internal/blizzard"
	"ookstats/internal/utils"
	"strings"
	"time"
)

// InsertLeaderboardData inserts leaderboard
 data and returns the number of runs and pla
yers inserted
func (ds *DatabaseService) InsertLeaderboard
Data(leaderboard *blizzard.LeaderboardRespon
se, realmInfo blizzard.RealmInfo, dungeon bl
izzard.DungeonInfo) (int, int, error) {
	if leaderboard == nil || len(leaderboard.Le
adingGroups) == 0 {
		return 0, 0, nil
	}

	tx, err := ds.db.Begin()
	if err != nil {
		return 0, 0, fmt.Errorf("failed to begin t
ransaction: %w", err)
	}
	defer tx.Rollback()

	runs, players, err := ds.insertLeaderboardD
ataTx(tx, leaderboard, realmInfo, dungeon)
	if err != nil {
		return 0, 0, err
	}

	if err := tx.Commit(); err != nil {
		return 0, 0, fmt.Errorf("failed to commit 
transaction: %w", err)
	}
	return runs, players, nil
}

// BatchProcessFetchResults processes fetch 
results concurrently with transaction batchi
ng
func (ds *DatabaseService) BatchProcessFetch
Results(ctx context.Context, results <-chan 
blizzard.FetchResult) (int, int, error) {
	totalRuns := 0
	totalPlayers := 0
	processedCount := 0
	errorCount := 0

	batch := make([]blizzard.FetchResult, 0, 10
)
	batchNumber := 0

	fmt.Printf("[INFO] Starting to process API 
results...\n")
	for result := range results {
		processedCount++
		ds.recordFetchStatusResult(result)

		if result.Error != nil {
			errorCount++
			if verbose {
				fmt.Printf("[ERROR] API error [%d] %s/%s
: %v\n", processedCount, result.RealmInfo.Na
me, result.Dungeon.Name, result.Error)
			} else if !strings.Contains(strings.ToLow
er(result.Error.Error()), "404") {
				fmt.Printf("[ERROR] API error [%d] %s/%s
: %v\n", processedCount, result.RealmInfo.Na
me, result.Dungeon.Name, result.Error)
			}
			continue
		}

		batch = append(batch, result)

		if len(batch) >= 10 {
			batchNumber++
			runs, players, err := ds.processBatch(bat
ch)
			if err != nil {
				fmt.Printf("[ERROR] Batch %d failed: %v\
n", batchNumber, err)
			} else {
				totalRuns += runs
				totalPlayers += players
				if runs > 0 || players > 0 {
					fmt.Printf("[INFO] Batch %d: +%d runs, 
+%d players (total: %d runs, %d players)\n",
						batchNumber, runs, players, totalRuns,
 totalPlayers)
				}
			}
			batch = batch[:0]
		}

		if processedCount%10 == 0 {
			fmt.Printf("[INFO] Progress: %d requests 
processed, %d errors\n", processedCount, err
orCount)
		}

		select {
		case <-ctx.Done():
			fmt.Printf("[WARN] Context cancelled, sto
pping processing\n")
			return totalRuns, totalPlayers, ctx.Err()
		default:
		}
	}

	if len(batch) > 0 {
		batchNumber++
		runs, players, err := ds.processBatch(batc
h)
		if err != nil {
			fmt.Printf("[ERROR] Final batch %d failed
: %v\n", batchNumber, err)
		} else {
			totalRuns += runs
			totalPlayers += players
			if runs > 0 || players > 0 {
				fmt.Printf("[INFO] Final batch %d: +%d r
uns, +%d players\n", batchNumber, runs, play
ers)
			}
		}
	}

	fmt.Printf("\n[INFO] Final stats: %d reques
ts processed, %d errors, %d runs, %d players
\n",
		processedCount, errorCount, totalRuns, tot
alPlayers)

	return totalRuns, totalPlayers, nil
}

// processBatch processes a batch of fetch r
esults in a single transaction
func (ds *DatabaseService) processBatch(batc
h []blizzard.FetchResult) (int, int, error) 
{
	if len(batch) == 0 {
		return 0, 0, nil
	}

	type batchItem struct {
		idx   int
		r     blizzard.RealmInfo
		d     blizzard.DungeonInfo
		board *blizzard.LeaderboardResponse
	}

	items := make([]batchItem, 0, len(batch))
	for i, res := range batch {
		if res.Leaderboard == nil || len(res.Leade
rboard.LeadingGroups) == 0 {
			continue
		}
		items = append(items, batchItem{
			idx:   i + 1,
			r:     res.RealmInfo,
			d:     res.Dungeon,
			board: res.Leaderboard,
		})
	}

	if len(items) == 0 {
		return 0, 0, nil
	}

	startBatch := time.Now()
	tx, err := ds.db.Begin()
	if err != nil {
		return 0, 0, fmt.Errorf("failed to begin b
atch transaction: %w", err)
	}
	defer tx.Rollback()

	totalRuns := 0
	totalPlayers := 0

	for _, it := range items {
		itemStart := time.Now()
		runs, players, err := ds.insertLeaderboard
DataTx(tx, it.board, it.r, it.d)
		if err != nil {
			return 0, 0, fmt.Errorf("failed to insert
 leaderboard data: %w", err)
		}
		totalRuns += runs
		totalPlayers += players
		if runs > 0 || players > 0 {
			fmt.Printf("    - Batch item %d: %s/%s ->
 +%d runs, +%d players in %dms\n",
				it.idx, it.r.Name, it.d.Name, runs, play
ers, time.Since(itemStart).Milliseconds())
		}
	}

	if err := tx.Commit(); err != nil {
		return 0, 0, fmt.Errorf("failed to commit 
batch transaction: %w", err)
	}
	fmt.Printf("    [OK] Batch committed in %dm
s (total +%d runs, +%d players)\n",
		time.Since(startBatch).Milliseconds(), tot
alRuns, totalPlayers)
	return totalRuns, totalPlayers, nil
}

// insertLeaderboardDataTx inserts leaderboa
rd data within a transaction
func (ds *DatabaseService) insertLeaderboard
DataTx(tx *sql.Tx, leaderboard *blizzard.Lea
derboardResponse, realmInfo blizzard.RealmIn
fo, dungeon blizzard.DungeonInfo) (int, int,
 error) {
	realmID, err := ds.getRealmIDTx(tx, realmIn
fo.Slug, realmInfo.Region)
	if err != nil {
		return 0, 0, fmt.Errorf("failed to get rea
lm ID: %w", err)
	}
	dungeonID, err := ds.getDungeonIDTx(tx, dun
geon.Slug)
	if err != nil {
		return 0, 0, fmt.Errorf("failed to get dun
geon ID: %w", err)
	}

	// compute newest completed_timestamp in th
is leaderboard (for marker update)
	maxCT := int64(0)
	for _, run := range leaderboard.LeadingGrou
ps {
		if run.CompletedTimestamp > maxCT {
			maxCT = run.CompletedTimestamp
		}
	}

	runsInserted := 0
	playersInserted := 0

	for _, run := range leaderboard.LeadingGrou
ps {
		var playerIDs []int
		for _, member := range run.Members {
			if id, ok := member.GetPlayerID(); ok {
				playerIDs = append(playerIDs, id)
			}
		}

		if len(playerIDs) == 0 {
			continue
		}

		teamSignature := utils.ComputeTeamSignatur
e(playerIDs)

		var runSeasonID *int
		if run.CompletedTimestamp > 0 {
			sid, err := ds.determineSeasonForRunTx(tx
, realmInfo.Region, run.CompletedTimestamp)
			if err != nil {
				return 0, 0, fmt.Errorf("failed to deter
mine season for run: %w", err)
			}
			if sid > 0 {
				runSeasonID = &sid
			}
		}

		runQuery := `
			INSERT OR IGNORE INTO challenge_runs
			(duration, completed_timestamp, keystone_
level, dungeon_id, realm_id, period_id, peri
od_start_timestamp, period_end_timestamp, te
am_signature, season_id)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		`

		result, err := tx.Exec(runQuery,
			run.Duration,
			run.CompletedTimestamp,
			run.KeystoneLevel,
			dungeonID,
			realmID,
			leaderboard.Period,
			leaderboard.PeriodStartTimestamp,
			leaderboard.PeriodEndTimestamp,
			teamSignature,
			runSeasonID,
		)
		if err != nil {
			return 0, 0, fmt.Errorf("failed to insert
 run: %w", err)
		}

		rowsAffected, err := result.RowsAffected()
		if err != nil {
			return 0, 0, fmt.Errorf("failed to check 
rows affected: %w", err)
		}

		if rowsAffected == 0 {
			continue
		}

		runID, err := result.LastInsertId()
		if err != nil {
			return 0, 0, fmt.Errorf("failed to get ru
n ID: %w", err)
		}

		runsInserted++

		for _, member := range run.Members {
			playerID, hasPlayerID := member.GetPlayer
ID()
			playerName, _ := member.GetPlayerName()
			playerRealmSlug, hasRealmSlug := member.G
etRealmSlug()

			if !hasPlayerID {
				continue
			}

			var playerRealmID int
			if hasRealmSlug {
				playerRealmID, err = ds.getRealmIDTx(tx,
 playerRealmSlug, realmInfo.Region)
				if err != nil {
					return 0, 0, fmt.Errorf("failed to reso
lve player realm: %w", err)
				}
				if playerRealmID == 0 {
					if _, err := tx.Exec(`INSERT OR IGNORE 
INTO realms (slug, name, region, connected_r
ealm_id, parent_realm_slug) VALUES (?, ?, ?,
 NULL, ?)`,
						playerRealmSlug, playerRealmSlug, real
mInfo.Region, ""); err != nil {
						return 0, 0, fmt.Errorf("failed to cre
ate placeholder realm: %w", err)
					}
					id2, err := ds.getRealmIDTx(tx, playerR
ealmSlug, realmInfo.Region)
					if err != nil || id2 == 0 {
						return 0, 0, fmt.Errorf("failed to res
olve placeholder realm id: %w", err)
					}
					playerRealmID = id2
				}
			} else {
				playerRealmID = realmID
			}

			// check if player with same name+realm a
lready exists (handles faction transfers whe
re Blizzard ID changes)
			existingPlayerID, err := getExistingPlaye
rIDTx(tx, playerName, playerRealmID)
			if err != nil {
				return 0, 0, fmt.Errorf("failed to check
 existing player: %w", err)
			}

			effectivePlayerID := playerID

			if existingPlayerID != 0 && existingPlaye
rID != int64(playerID) {
				// player exists with different ID (like
ly faction transfer)
				// migrate old runs to the new player ID
 and mark old player invalid
				_, err := tx.Exec(`UPDATE run_members SE
T player_id = ? WHERE player_id = ?`, player
ID, existingPlayerID)
				if err != nil {
					return 0, 0, fmt.Errorf("failed to migr
ate runs to new player ID: %w", err)
				}
				_, err = tx.Exec(`UPDATE players SET is_
valid = 0 WHERE id = ?`, existingPlayerID)
				if err != nil {
					return 0, 0, fmt.Errorf("failed to inva
lidate old player: %w", err)
				}
			}

			playerQuery := `
				INSERT INTO players (id, name, name_lowe
r, realm_id)
				VALUES (?, ?, lower(?), ?)
				ON CONFLICT(id) DO UPDATE SET
				  name = excluded.name,
				  name_lower = lower(excluded.name),
				  realm_id = excluded.realm_id
				WHERE excluded.name != name OR excluded.
realm_id != realm_id
			`
			playerResult, err := tx.Exec(playerQuery,
 playerID, playerName, playerName, playerRea
lmID)
			if err != nil {
				return 0, 0, fmt.Errorf("failed to inser
t player: %w", err)
			}

			rowsAffected, err := playerResult.RowsAff
ected()
			if err != nil {
				return 0, 0, fmt.Errorf("failed to get r
ows affected: %w", err)
			}
			if rowsAffected > 0 {
				playersInserted++
			}

			specID, _ := member.GetSpecID()
			faction, _ := member.GetFaction()

			memberQuery := `INSERT INTO run_members (
run_id, player_id, spec_id, faction) VALUES 
(?, ?, ?, ?)`

			var specPtr *int
			var factionPtr *string
			if specID > 0 {
				specPtr = &specID
			}
			if faction != "" {
				factionPtr = &faction
			}

			_, err = tx.Exec(memberQuery, runID, effe
ctivePlayerID, specPtr, factionPtr)
			if err != nil {
				return 0, 0, fmt.Errorf("failed to inser
t run member: %w", err)
			}
		}
	}

	// update marker inside the transaction
	if maxCT > 0 {
		if _, err := tx.Exec(`INSERT INTO api_fetc
h_markers (realm_slug, dungeon_id, period_id
, last_completed_ts)
                               VALUES (?, ?,
 ?, ?)
                               ON CONFLICT(r
ealm_slug, dungeon_id, period_id)
                               DO UPDATE SET
 last_completed_ts = MAX(last_completed_ts, 
excluded.last_completed_ts)`,
			realmInfo.Slug, dungeonID, leaderboard.Pe
riod, maxCT); err != nil {
			return 0, 0, fmt.Errorf("failed to update
 fetch marker: %w", err)
		}
	}

	return runsInserted, playersInserted, nil
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET