┌─ GO ───────────────────────────────────────────────────────────────────────┐
│ package database │
│ │
│ import "fmt" │
│ │
│ type PlayerAccountFingerprint struct { │
│ PlayerID int64 │
│ Region string │
│ TuplesJSON string │
│ FetchedAt int64 │
│ } │
│ │
│ type AccountGroup struct { │
│ Region string │
│ PlayerIDs []int64 │
│ } │
│ │
│ func (ds *DatabaseService) UpsertPlayerAccountFingerprint(playerID int64, region, │
│ tuplesJSON string, fetchedAt int64) error { │
│ return retryOnBusy(func() error { │
│ _, err := ds.db.Exec(` │
│ INSERT INTO player_account_fingerprint (player_id, region, tuples_json, fetched │
│ _at) │
│ VALUES (?, ?, ?, ?) │
│ ON CONFLICT(player_id) DO UPDATE SET │
│ region = excluded.region, │
│ tuples_json = excluded.tuples_json, │
│ fetched_at = excluded.fetched_at │
│ `, playerID, region, tuplesJSON, fetchedAt) │
│ return err │
│ }) │
│ } │
│ │
│ func (ds *DatabaseService) LoadAllPlayerAccountFingerprints() ([]PlayerAccountFing │
│ erprint, error) { │
│ rows, err := ds.db.Query(` │
│ SELECT player_id, region, tuples_json, fetched_at │
│ FROM player_account_fingerprint │
│ `) │
│ if err != nil { │
│ return nil, err │
│ } │
│ defer rows.Close() │
│ │
│ var out []PlayerAccountFingerprint │
│ for rows.Next() { │
│ var f PlayerAccountFingerprint │
│ if err := rows.Scan(&f.PlayerID, &f.Region, &f.TuplesJSON, &f.FetchedAt); err != │
│ nil { │
│ return nil, err │
│ } │
│ out = append(out, f) │
│ } │
│ return out, rows.Err() │
│ } │
│ │
│ type AccountBackfillCandidate struct { │
│ PlayerID int64 │
│ Name string │
│ RealmSlug string │
│ Region string │
│ } │
│ │
│ func (ds *DatabaseService) PlayersMissingAccountFingerprint(retryBefore int64) ([] │
│ AccountBackfillCandidate, error) { │
│ rows, err := ds.db.Query(` │
│ SELECT pf.player_id, p.name, r.slug, r.region │
│ FROM player_fingerprints pf │
│ JOIN players p ON p.id = pf.player_id │
│ JOIN realms r ON r.id = p.realm_id │
│ LEFT JOIN player_account_fingerprint paf ON paf.player_id = pf.player_id │
│ WHERE paf.player_id IS NULL │
│ AND p.is_valid = 1 │
│ AND (p.account_fp_attempted_at IS NULL OR p.account_fp_attempted_at < ?) │
│ `, retryBefore) │
│ if err != nil { │
│ return nil, err │
│ } │
│ defer rows.Close() │
│ │
│ var out []AccountBackfillCandidate │
│ for rows.Next() { │
│ var c AccountBackfillCandidate │
│ if err := rows.Scan(&c.PlayerID, &c.Name, &c.RealmSlug, &c.Region); err != nil { │
│ return nil, err │
│ } │
│ out = append(out, c) │
│ } │
│ return out, rows.Err() │
│ } │
│ │
│ // single tx so a failure mid-way leaves the previous grouping intact │
│ func (ds *DatabaseService) RebuildAccounts(groups []AccountGroup, computedAt int64 │
│ ) (int, error) { │
│ var totalChars int │
│ err := retryOnBusy(func() error { │
│ tx, err := ds.db.Begin() │
│ if err != nil { │
│ return err │
│ } │
│ defer tx.Rollback() │
│ │
│ if _, err := tx.Exec(`UPDATE players SET account_id = NULL WHERE account_id IS N │
│ OT NULL`); err != nil { │
│ return fmt.Errorf("clear account_id: %w", err) │
│ } │
│ if _, err := tx.Exec(`DELETE FROM accounts`); err != nil { │
│ return fmt.Errorf("delete accounts: %w", err) │
│ } │
│ │
│ insAcct, err := tx.Prepare(`INSERT INTO accounts (region, character_count, compu │
│ ted_at) VALUES (?, ?, ?)`) │
│ if err != nil { │
│ return err │
│ } │
│ defer insAcct.Close() │
│ updPlayer, err := tx.Prepare(`UPDATE players SET account_id = ? WHERE id = ?`) │
│ if err != nil { │
│ return err │
│ } │
│ defer updPlayer.Close() │
│ │
│ totalChars = 0 │
│ for _, g := range groups { │
│ if len(g.PlayerIDs) == 0 { │
│ continue │
│ } │
│ res, err := insAcct.Exec(g.Region, len(g.PlayerIDs), computedAt) │
│ if err != nil { │
│ return fmt.Errorf("insert account: %w", err) │
│ } │
│ accountID, err := res.LastInsertId() │
│ if err != nil { │
│ return err │
│ } │
│ for _, pid := range g.PlayerIDs { │
│ if _, err := updPlayer.Exec(accountID, pid); err != nil { │
│ return fmt.Errorf("update players.account_id pid=%d: %w", pid, err) │
│ } │
│ } │
│ totalChars += len(g.PlayerIDs) │
│ } │
│ return tx.Commit() │
│ }) │
│ if err != nil { │
│ return 0, err │
│ } │
│ return totalChars, nil │
│ } │
│ │
│ // MarkAccountFingerprintAttempts stamps candidates so failed fetches wait out the │
│ retry cooldown │
│ func (ds *DatabaseService) MarkAccountFingerprintAttempts(playerIDs []int64, ts in │
│ t64) error { │
│ if len(playerIDs) == 0 { │
│ return nil │
│ } │
│ return retryOnBusy(func() error { │
│ tx, err := ds.db.Begin() │
│ if err != nil { │
│ return err │
│ } │
│ defer tx.Rollback() │
│ stmt, err := tx.Prepare(`UPDATE players SET account_fp_attempted_at = ? WHERE id │
│ = ?`) │
│ if err != nil { │
│ return err │
│ } │
│ defer stmt.Close() │
│ for _, id := range playerIDs { │
│ if _, err := stmt.Exec(ts, id); err != nil { │
│ return err │
│ } │
│ } │
│ return tx.Commit() │
│ }) │
│ } │
│ │
│ func (ds *DatabaseService) CountPlayerAccountFingerprints() (int, error) { │
│ var n int │
│ err := ds.db.QueryRow(`SELECT COUNT(*) FROM player_account_fingerprint`).Scan(&n) │
│ return n, err │
│ } │
│ │
│ func (ds *DatabaseService) CountPlayersMissingAccountFingerprint() (int, error) { │
│ var n int │
│ err := ds.db.QueryRow(` │
│ SELECT COUNT(*) FROM player_fingerprints pf │
│ LEFT JOIN player_account_fingerprint paf ON paf.player_id = pf.player_id │
│ JOIN players p ON p.id = pf.player_id │
│ WHERE paf.player_id IS NULL AND p.is_valid = 1 │
│ `).Scan(&n) │
│ return n, err │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ GO ─────────────────────────────────┐
│ package database │
│ │
│ import "fmt" │
│ │
│ type PlayerAccountFingerprint struct { │
│ PlayerID int64 │
│ Region string │
│ TuplesJSON string │
│ FetchedAt int64 │
│ } │
│ │
│ type AccountGroup struct { │
│ Region string │
│ PlayerIDs []int64 │
│ } │
│ │
│ func (ds *DatabaseService) UpsertPlayerAccou │
│ ntFingerprint(playerID int64, region, tuples │
│ JSON string, fetchedAt int64) error { │
│ return retryOnBusy(func() error { │
│ _, err := ds.db.Exec(` │
│ INSERT INTO player_account_fingerprint (p │
│ layer_id, region, tuples_json, fetched_at) │
│ VALUES (?, ?, ?, ?) │
│ ON CONFLICT(player_id) DO UPDATE SET │
│ region = excluded.region, │
│ tuples_json = excluded.tuples_json, │
│ fetched_at = excluded.fetched_at │
│ `, playerID, region, tuplesJSON, fetchedAt │
│ ) │
│ return err │
│ }) │
│ } │
│ │
│ func (ds *DatabaseService) LoadAllPlayerAcco │
│ untFingerprints() ([]PlayerAccountFingerprin │
│ t, error) { │
│ rows, err := ds.db.Query(` │
│ SELECT player_id, region, tuples_json, fet │
│ ched_at │
│ FROM player_account_fingerprint │
│ `) │
│ if err != nil { │
│ return nil, err │
│ } │
│ defer rows.Close() │
│ │
│ var out []PlayerAccountFingerprint │
│ for rows.Next() { │
│ var f PlayerAccountFingerprint │
│ if err := rows.Scan(&f.PlayerID, &f.Region │
│ , &f.TuplesJSON, &f.FetchedAt); err != nil { │
│ return nil, err │
│ } │
│ out = append(out, f) │
│ } │
│ return out, rows.Err() │
│ } │
│ │
│ type AccountBackfillCandidate struct { │
│ PlayerID int64 │
│ Name string │
│ RealmSlug string │
│ Region string │
│ } │
│ │
│ func (ds *DatabaseService) PlayersMissingAcc │
│ ountFingerprint(retryBefore int64) ([]Accoun │
│ tBackfillCandidate, error) { │
│ rows, err := ds.db.Query(` │
│ SELECT pf.player_id, p.name, r.slug, r.reg │
│ ion │
│ FROM player_fingerprints pf │
│ JOIN players p ON p.id = pf.player_id │
│ JOIN realms r ON r.id = p.realm_id │
│ LEFT JOIN player_account_fingerprint paf O │
│ N paf.player_id = pf.player_id │
│ WHERE paf.player_id IS NULL │
│ AND p.is_valid = 1 │
│ AND (p.account_fp_attempted_at IS NULL O │
│ R p.account_fp_attempted_at < ?) │
│ `, retryBefore) │
│ if err != nil { │
│ return nil, err │
│ } │
│ defer rows.Close() │
│ │
│ var out []AccountBackfillCandidate │
│ for rows.Next() { │
│ var c AccountBackfillCandidate │
│ if err := rows.Scan(&c.PlayerID, &c.Name, │
│ &c.RealmSlug, &c.Region); err != nil { │
│ return nil, err │
│ } │
│ out = append(out, c) │
│ } │
│ return out, rows.Err() │
│ } │
│ │
│ // single tx so a failure mid-way leaves the │
│ previous grouping intact │
│ func (ds *DatabaseService) RebuildAccounts(g │
│ roups []AccountGroup, computedAt int64) (int │
│ , error) { │
│ var totalChars int │
│ err := retryOnBusy(func() error { │
│ tx, err := ds.db.Begin() │
│ if err != nil { │
│ return err │
│ } │
│ defer tx.Rollback() │
│ │
│ if _, err := tx.Exec(`UPDATE players SET a │
│ ccount_id = NULL WHERE account_id IS NOT NUL │
│ L`); err != nil { │
│ return fmt.Errorf("clear account_id: %w", │
│ err) │
│ } │
│ if _, err := tx.Exec(`DELETE FROM accounts │
│ `); err != nil { │
│ return fmt.Errorf("delete accounts: %w", │
│ err) │
│ } │
│ │
│ insAcct, err := tx.Prepare(`INSERT INTO ac │
│ counts (region, character_count, computed_at │
│ ) VALUES (?, ?, ?)`) │
│ if err != nil { │
│ return err │
│ } │
│ defer insAcct.Close() │
│ updPlayer, err := tx.Prepare(`UPDATE playe │
│ rs SET account_id = ? WHERE id = ?`) │
│ if err != nil { │
│ return err │
│ } │
│ defer updPlayer.Close() │
│ │
│ totalChars = 0 │
│ for _, g := range groups { │
│ if len(g.PlayerIDs) == 0 { │
│ continue │
│ } │
│ res, err := insAcct.Exec(g.Region, len(g. │
│ PlayerIDs), computedAt) │
│ if err != nil { │
│ return fmt.Errorf("insert account: %w", │
│ err) │
│ } │
│ accountID, err := res.LastInsertId() │
│ if err != nil { │
│ return err │
│ } │
│ for _, pid := range g.PlayerIDs { │
│ if _, err := updPlayer.Exec(accountID, p │
│ id); err != nil { │
│ return fmt.Errorf("update players.accou │
│ nt_id pid=%d: %w", pid, err) │
│ } │
│ } │
│ totalChars += len(g.PlayerIDs) │
│ } │
│ return tx.Commit() │
│ }) │
│ if err != nil { │
│ return 0, err │
│ } │
│ return totalChars, nil │
│ } │
│ │
│ // MarkAccountFingerprintAttempts stamps can │
│ didates so failed fetches wait out the retry │
│ cooldown │
│ func (ds *DatabaseService) MarkAccountFinger │
│ printAttempts(playerIDs []int64, ts int64) e │
│ rror { │
│ if len(playerIDs) == 0 { │
│ return nil │
│ } │
│ return retryOnBusy(func() error { │
│ tx, err := ds.db.Begin() │
│ if err != nil { │
│ return err │
│ } │
│ defer tx.Rollback() │
│ stmt, err := tx.Prepare(`UPDATE players SE │
│ T account_fp_attempted_at = ? WHERE id = ?`) │
│ if err != nil { │
│ return err │
│ } │
│ defer stmt.Close() │
│ for _, id := range playerIDs { │
│ if _, err := stmt.Exec(ts, id); err != ni │
│ l { │
│ return err │
│ } │
│ } │
│ return tx.Commit() │
│ }) │
│ } │
│ │
│ func (ds *DatabaseService) CountPlayerAccoun │
│ tFingerprints() (int, error) { │
│ var n int │
│ err := ds.db.QueryRow(`SELECT COUNT(*) FROM │
│ player_account_fingerprint`).Scan(&n) │
│ return n, err │
│ } │
│ │
│ func (ds *DatabaseService) CountPlayersMissi │
│ ngAccountFingerprint() (int, error) { │
│ var n int │
│ err := ds.db.QueryRow(` │
│ SELECT COUNT(*) FROM player_fingerprints p │
│ f │
│ LEFT JOIN player_account_fingerprint paf O │
│ N paf.player_id = pf.player_id │
│ JOIN players p ON p.id = pf.player_id │
│ WHERE paf.player_id IS NULL AND p.is_valid │
│ = 1 │
│ `).Scan(&n) │
│ return n, err │
│ } │
└──────────────────────────────────────────────┘