from datetime import datetime, timezone import json import os import sqlite3 from database.maintenance import backup_storage_status from web.config import Config from web.services.team_context_service import TeamContextService class IntegrityService: DATABASES = { 'L2': Config.DB_L2_PATH, 'L3': Config.DB_L3_PATH, 'Web': Config.DB_WEB_PATH, } @staticmethod def _check(checks, name, status, detail, value=None): checks.append({ 'name': name, 'status': status, 'detail': detail, 'value': value, }) @staticmethod def _connect(path): db = sqlite3.connect(path, timeout=Config.SQLITE_TIMEOUT_SECONDS) db.row_factory = sqlite3.Row return db @staticmethod def build_report(): checks = [] counts = {} connections = {} try: for name, path in IntegrityService.DATABASES.items(): if not os.path.exists(path): IntegrityService._check( checks, f'{name} database', 'fail', f'Missing file: {path}', ) continue try: db = IntegrityService._connect(path) connections[name] = db result = db.execute('PRAGMA quick_check').fetchone()[0] IntegrityService._check( checks, f'{name} database', 'pass' if result == 'ok' else 'fail', f'quick_check: {result}', os.path.getsize(path), ) except sqlite3.Error as exc: IntegrityService._check( checks, f'{name} database', 'fail', str(exc), ) l2 = connections.get('L2') if l2: IntegrityService._check_l2(l2, checks, counts) l3 = connections.get('L3') roster_ids = TeamContextService.get_active_roster_ids() counts['active_roster'] = len(roster_ids) if l3: IntegrityService._check_l3(l3, roster_ids, checks, counts) web = connections.get('Web') if web: IntegrityService._check_web(web, checks, counts) backup_status = backup_storage_status() counts['backup_sets'] = backup_status['sets'] counts['backup_bytes'] = backup_status['total_bytes'] IntegrityService._check( checks, 'Backup retention', 'warn' if backup_status['sets'] > 3 else 'pass', ( f"{backup_status['sets']} backup sets, " f"{backup_status['total_bytes']:,} bytes" ), backup_status['sets'], ) finally: for db in connections.values(): db.close() status_order = {'pass': 0, 'warn': 1, 'fail': 2} overall_status = max( (check['status'] for check in checks), key=lambda status: status_order[status], default='fail', ) return { 'generated_at': datetime.now(timezone.utc).isoformat(), 'overall_status': overall_status, 'counts': counts, 'checks': checks, 'totals': { status: sum(1 for check in checks if check['status'] == status) for status in ('pass', 'warn', 'fail') }, } @staticmethod def _table_names(db): return { row[0] for row in db.execute( "SELECT name FROM sqlite_master WHERE type='table'" ) } @staticmethod def _check_l2(db, checks, counts): required_tables = { 'dim_players', 'fact_matches', 'fact_match_teams', 'fact_match_players', 'fact_rounds', 'fact_round_events', 'fact_round_player_economy', } missing = sorted(required_tables - IntegrityService._table_names(db)) IntegrityService._check( checks, 'L2 required tables', 'fail' if missing else 'pass', f"Missing: {', '.join(missing)}" if missing else 'All required tables exist', ) if missing: return table_count_map = { 'matches': 'fact_matches', 'players': 'dim_players', 'player_match_rows': 'fact_match_players', 'rounds': 'fact_rounds', 'events': 'fact_round_events', 'economy_rows': 'fact_round_player_economy', } for key, table in table_count_map.items(): counts[key] = db.execute(f'SELECT COUNT(*) FROM {table}').fetchone()[0] orphan_players = db.execute( """ SELECT COUNT(*) FROM fact_match_players mp LEFT JOIN fact_matches m ON m.match_id = mp.match_id WHERE m.match_id IS NULL """ ).fetchone()[0] IntegrityService._check( checks, 'Player-match referential integrity', 'fail' if orphan_players else 'pass', f'{orphan_players} player rows reference missing matches', orphan_players, ) orphan_events = db.execute( """ SELECT COUNT(*) FROM fact_round_events e LEFT JOIN fact_rounds r ON r.match_id = e.match_id AND r.round_num = e.round_num WHERE r.match_id IS NULL """ ).fetchone()[0] IntegrityService._check( checks, 'Round-event referential integrity', 'fail' if orphan_events else 'pass', f'{orphan_events} events reference missing rounds', orphan_events, ) unusual_rosters = db.execute( """ SELECT COUNT(*) FROM ( SELECT match_id, COUNT(*) AS player_count FROM fact_match_players GROUP BY match_id HAVING player_count != 10 ) """ ).fetchone()[0] IntegrityService._check( checks, 'Match player cardinality', 'warn' if unusual_rosters else 'pass', f'{unusual_rosters} matches do not contain exactly 10 players', unusual_rosters, ) missing_names = db.execute( "SELECT COUNT(*) FROM dim_players WHERE username IS NULL OR TRIM(username) = ''" ).fetchone()[0] IntegrityService._check( checks, 'Player identity coverage', 'warn' if missing_names else 'pass', f'{missing_names} players have no username', missing_names, ) required_indexes = { 'idx_match_players_player_match', 'idx_match_players_match_team', 'idx_match_players_party', 'idx_round_events_victim', 'idx_economy_player_match', 'idx_matches_map_time', } existing_indexes = { row[0] for row in db.execute( "SELECT name FROM sqlite_master WHERE type = 'index'" ) } missing_indexes = sorted(required_indexes - existing_indexes) IntegrityService._check( checks, 'L2 operational indexes', 'fail' if missing_indexes else 'pass', ( f"Missing: {', '.join(missing_indexes)}" if missing_indexes else 'All high-frequency query indexes exist' ), len(required_indexes) - len(missing_indexes), ) @staticmethod def _check_l3(db, roster_ids, checks, counts): required_tables = { 'dm_discovery_insights', 'dm_duo_stats', 'dm_match_player_reports', 'dm_match_reports', 'dm_player_features', 'dm_player_awards', 'dm_player_match_history', 'dm_player_map_stats', 'dm_player_period_stats', 'dm_player_record_events', 'dm_player_records', 'dm_player_weapon_stats', 'dm_performance_medals', 'dm_lineup_stats', 'dm_team_season_stats', } missing = sorted(required_tables - IntegrityService._table_names(db)) IntegrityService._check( checks, 'L3 required tables', 'fail' if missing else 'pass', f"Missing: {', '.join(missing)}" if missing else 'All required tables exist', ) if missing: return counts['l3_features'] = db.execute( 'SELECT COUNT(*) FROM dm_player_features' ).fetchone()[0] counts['l3_history'] = db.execute( 'SELECT COUNT(*) FROM dm_player_match_history' ).fetchone()[0] counts['l3_maps'] = db.execute( 'SELECT COUNT(*) FROM dm_player_map_stats' ).fetchone()[0] counts['l3_weapons'] = db.execute( 'SELECT COUNT(*) FROM dm_player_weapon_stats' ).fetchone()[0] counts['l3_periods'] = db.execute( 'SELECT COUNT(*) FROM dm_player_period_stats' ).fetchone()[0] counts['l3_records'] = db.execute( 'SELECT COUNT(*) FROM dm_player_records' ).fetchone()[0] counts['l3_duos'] = db.execute( 'SELECT COUNT(*) FROM dm_duo_stats' ).fetchone()[0] counts['l3_lineups'] = db.execute( 'SELECT COUNT(*) FROM dm_lineup_stats' ).fetchone()[0] counts['l3_match_reports'] = db.execute( 'SELECT COUNT(*) FROM dm_match_reports' ).fetchone()[0] counts['l3_player_reports'] = db.execute( 'SELECT COUNT(*) FROM dm_match_player_reports' ).fetchone()[0] counts['l3_record_events'] = db.execute( 'SELECT COUNT(*) FROM dm_player_record_events' ).fetchone()[0] counts['l3_seasons'] = db.execute( 'SELECT COUNT(*) FROM dm_team_season_stats' ).fetchone()[0] counts['l3_awards'] = db.execute( 'SELECT COUNT(*) FROM dm_player_awards' ).fetchone()[0] counts['l3_discoveries'] = db.execute( 'SELECT COUNT(*) FROM dm_discovery_insights' ).fetchone()[0] counts['l3_medals'] = db.execute( 'SELECT COUNT(*) FROM dm_performance_medals' ).fetchone()[0] expected_matches = counts.get('matches', 0) IntegrityService._check( checks, 'Post-match report coverage', ( 'pass' if counts['l3_match_reports'] == expected_matches else 'fail' ), ( f"{counts['l3_match_reports']}/" f"{expected_matches} matches reported" ), counts['l3_match_reports'], ) if roster_ids: placeholders = ','.join('?' for _ in roster_ids) covered = db.execute( f""" SELECT COUNT(DISTINCT steam_id_64) FROM dm_player_features WHERE steam_id_64 IN ({placeholders}) """, roster_ids, ).fetchone()[0] IntegrityService._check( checks, 'Active roster feature coverage', 'pass' if covered == len(roster_ids) else 'fail', f'{covered}/{len(roster_ids)} roster players have L3 features', covered, ) expected_history = db.execute( f""" SELECT COALESCE(SUM(total_matches), 0) FROM dm_player_features WHERE steam_id_64 IN ({placeholders}) """, roster_ids, ).fetchone()[0] actual_history = db.execute( f""" SELECT COUNT(*) FROM dm_player_match_history WHERE steam_id_64 IN ({placeholders}) """, roster_ids, ).fetchone()[0] IntegrityService._check( checks, 'Roster history completeness', 'pass' if actual_history == expected_history else 'fail', f'{actual_history}/{expected_history} player-match rows materialized', actual_history, ) player_report_count = db.execute( f""" SELECT COUNT(*) FROM dm_match_player_reports WHERE steam_id_64 IN ({placeholders}) """, roster_ids, ).fetchone()[0] IntegrityService._check( checks, 'Roster player-report completeness', 'pass' if player_report_count == expected_history else 'fail', f'{player_report_count}/{expected_history} player reports', player_report_count, ) score_rows = db.execute( f""" SELECT steam_id_64, score_overall, tier_percentile FROM dm_player_features WHERE steam_id_64 IN ({placeholders}) AND score_overall > 0 """, roster_ids, ).fetchall() scores = [float(row['score_overall']) for row in score_rows] invalid_percentiles = 0 for row in score_rows: expected = ( sum(value <= float(row['score_overall']) for value in scores) / len(scores) * 100 ) actual = row['tier_percentile'] if actual is None or abs(float(actual) - expected) > 0.011: invalid_percentiles += 1 IntegrityService._check( checks, 'Roster percentile correctness', 'pass' if invalid_percentiles == 0 else 'warn', f'{invalid_percentiles} eligible players have stale percentiles', invalid_percentiles, ) for table, label in ( ('dm_player_period_stats', 'Roster period-stat coverage'), ('dm_player_records', 'Roster record coverage'), ): covered = db.execute( f""" SELECT COUNT(DISTINCT steam_id_64) FROM {table} WHERE steam_id_64 IN ({placeholders}) """, roster_ids, ).fetchone()[0] IntegrityService._check( checks, label, 'pass' if covered == len(roster_ids) else 'fail', f'{covered}/{len(roster_ids)} roster players covered', covered, ) for key, label in ( ('l3_history', 'Player match history mart'), ('l3_maps', 'Player map stats mart'), ('l3_weapons', 'Player weapon stats mart'), ('l3_periods', 'Player period stats mart'), ('l3_records', 'Player records mart'), ('l3_duos', 'Team duo stats mart'), ('l3_lineups', 'Team lineup stats mart'), ('l3_match_reports', 'Post-match report mart'), ('l3_player_reports', 'Player post-match report mart'), ('l3_record_events', 'Record event mart'), ('l3_seasons', 'Team season mart'), ('l3_awards', 'Player award mart'), ('l3_discoveries', 'Discovery insight mart'), ('l3_medals', 'Map and ELO medal mart'), ): value = counts[key] IntegrityService._check( checks, label, 'pass' if value else 'warn', f'{value} rows', value, ) discovery_tones = { row[0] for row in db.execute( 'SELECT DISTINCT tone FROM dm_discovery_insights' ) } IntegrityService._check( checks, 'Discovery tone coverage', ( 'pass' if discovery_tones == {'positive', 'negative', 'fun'} else 'fail' ), f"tones: {', '.join(sorted(discovery_tones))}", len(discovery_tones), ) invalid_medals = db.execute( """ SELECT COUNT(*) FROM dm_performance_medals WHERE matches < 5 OR medal_rank NOT BETWEEN 1 AND 3 """ ).fetchone()[0] duplicate_medals = db.execute( """ SELECT COUNT(*) FROM ( SELECT dimension_type, dimension_key, medal_rank, COUNT(*) AS n FROM dm_performance_medals GROUP BY dimension_type, dimension_key, medal_rank HAVING n > 1 ) """ ).fetchone()[0] IntegrityService._check( checks, 'Performance medal validity', 'pass' if invalid_medals == 0 and duplicate_medals == 0 else 'fail', ( f'{invalid_medals} invalid samples, ' f'{duplicate_medals} duplicate ranks' ), invalid_medals + duplicate_medals, ) if roster_ids: placeholders = ','.join('?' for _ in roster_ids) scope_sql = f"AND steam_id_64 IN ({placeholders})" scope_args = roster_ids else: scope_sql = '' scope_args = [] placeholder_rows = db.execute( f""" SELECT COUNT(*) FROM dm_player_features WHERE int_pos_site_a_control_rate = 0.33 AND int_pos_site_b_control_rate = 0.33 AND int_pos_mid_control_rate = 0.34 {scope_sql} """, scope_args, ).fetchone()[0] IntegrityService._check( checks, 'Experimental spatial metrics', 'warn' if placeholder_rows else 'pass', f'{placeholder_rows} active-roster rows contain placeholder site-control values', placeholder_rows, ) @staticmethod def _check_web(db, checks, counts): required_tables = { 'comments', 'etl_jobs', 'match_imports', 'player_metadata', 'schema_migrations', 'strategy_boards', 'team_lineups', 'team_roster_members', 'team_roster_versions', 'wiki_pages', } missing = sorted(required_tables - IntegrityService._table_names(db)) IntegrityService._check( checks, 'Web required tables', 'fail' if missing else 'pass', f"Missing: {', '.join(missing)}" if missing else 'All required tables exist', ) if missing: return counts['etl_jobs'] = db.execute( 'SELECT COUNT(*) FROM etl_jobs' ).fetchone()[0] counts['match_imports'] = db.execute( 'SELECT COUNT(*) FROM match_imports' ).fetchone()[0] schema_version = db.execute( 'SELECT COALESCE(MAX(version), 0) FROM schema_migrations' ).fetchone()[0] counts['web_schema_version'] = schema_version IntegrityService._check( checks, 'Web schema version', 'pass' if schema_version == Config.WEB_SCHEMA_VERSION else 'fail', f'{schema_version}/{Config.WEB_SCHEMA_VERSION}', schema_version, ) foreign_key_errors = db.execute( 'PRAGMA foreign_key_check' ).fetchall() IntegrityService._check( checks, 'Web foreign key integrity', 'fail' if foreign_key_errors else 'pass', f'{len(foreign_key_errors)} foreign key violations', len(foreign_key_errors), ) running_jobs = db.execute( """ SELECT COUNT(*) FROM etl_jobs WHERE status = 'running' """ ).fetchone()[0] IntegrityService._check( checks, 'Pipeline concurrency', 'warn' if running_jobs > 1 else 'pass', f'{running_jobs} running pipeline jobs', running_jobs, ) lineups = db.execute( 'SELECT id, player_ids_json, is_active FROM team_lineups' ).fetchall() counts['lineups'] = len(lineups) invalid_lineups = 0 for lineup in lineups: try: player_ids = json.loads(lineup['player_ids_json'] or '[]') if not isinstance(player_ids, list): invalid_lineups += 1 except (TypeError, json.JSONDecodeError): invalid_lineups += 1 IntegrityService._check( checks, 'Lineup JSON validity', 'fail' if invalid_lineups else 'pass', f'{invalid_lineups} lineups contain invalid player ID JSON', invalid_lineups, ) active_count = sum(1 for lineup in lineups if lineup['is_active'] == 1) IntegrityService._check( checks, 'Active lineup', 'pass' if active_count == 1 else 'warn', f'{active_count} active lineups configured', active_count, ) current_versions = db.execute( """ SELECT id FROM team_roster_versions WHERE is_current = 1 """ ).fetchall() current_member_count = 0 if len(current_versions) == 1: current_member_count = db.execute( """ SELECT COUNT(*) FROM team_roster_members WHERE roster_version_id = ? """, [current_versions[0]['id']], ).fetchone()[0] counts['roster_versions'] = db.execute( 'SELECT COUNT(*) FROM team_roster_versions' ).fetchone()[0] counts['current_roster_members'] = current_member_count IntegrityService._check( checks, 'Current roster version', 'pass' if len(current_versions) == 1 else 'fail', f'{len(current_versions)} current roster versions', len(current_versions), ) IntegrityService._check( checks, 'Roster version membership', 'pass' if current_member_count == counts.get('active_roster', 0) else 'fail', ( f"{current_member_count}/" f"{counts.get('active_roster', 0)} active members versioned" ), current_member_count, )