From 8c492273dce8614dcad20506e37761b98fff2291 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 25 Aug 2025 07:19:13 -0500 Subject: [PATCH] Postgres Query Updates Fixing query errors caused by Postgres vs SQLite --- Dockerfile.optimized | 3 +- app/main.py | 9 +++--- app/routers_v3/battingstats.py | 50 ++++++++++++++++++++--------- app/routers_v3/fieldingstats.py | 56 ++++++++++++++++++++++++--------- app/routers_v3/pitchingstats.py | 50 ++++++++++++++++++++--------- app/routers_v3/players.py | 2 +- app/routers_v3/stratplay.py | 53 ++++++++++++++++++++++++------- 7 files changed, 163 insertions(+), 60 deletions(-) diff --git a/Dockerfile.optimized b/Dockerfile.optimized index f43bdb4..ccf5419 100644 --- a/Dockerfile.optimized +++ b/Dockerfile.optimized @@ -18,6 +18,7 @@ WORKDIR /usr/src/app RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ libpq-dev \ + curl \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean @@ -38,7 +39,7 @@ RUN mkdir -p /usr/src/app/storage /usr/src/app/logs && \ # Health check for container monitoring HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:80/health || exit 1 + CMD curl -f http://localhost:80/api/v3/current || exit 1 # Switch to non-root user USER sba diff --git a/app/main.py b/app/main.py index b80094c..af87706 100644 --- a/app/main.py +++ b/app/main.py @@ -10,9 +10,7 @@ from fastapi.openapi.utils import get_openapi # from fastapi.openapi.docs import get_swagger_ui_html # from fastapi.openapi.utils import get_openapi -from .routers_v3 import current, players, results, schedules, standings, teams, transactions, battingstats, \ - pitchingstats, fieldingstats, draftpicks, draftlist, managers, awards, draftdata, keepers, stratgame, stratplay, \ - injuries, decisions, divisions, sbaplayers, custom_commands +from .routers_v3 import current, players, results, schedules, standings, teams, transactions, battingstats, pitchingstats, fieldingstats, draftpicks, draftlist, managers, awards, draftdata, keepers, stratgame, stratplay, injuries, decisions, divisions, sbaplayers, custom_commands, views # date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}' log_level = logging.INFO if os.environ.get('LOG_LEVEL') == 'INFO' else logging.WARNING @@ -25,9 +23,9 @@ logger = logging.getLogger('discord_app') logger.setLevel(log_level) handler = RotatingFileHandler( - filename='/tmp/sba-database.log', + filename='./logs/sba-database.log', # encoding='utf-8', - maxBytes=32 * 1024 * 1024, # 32 MiB + maxBytes=8 * 1024 * 1024, # 8 MiB backupCount=5, # Rotate through 5 files ) @@ -69,6 +67,7 @@ app.include_router(decisions.router) app.include_router(divisions.router) app.include_router(sbaplayers.router) app.include_router(custom_commands.router) +app.include_router(views.router) logger.info(f'Loaded all routers.') diff --git a/app/routers_v3/battingstats.py b/app/routers_v3/battingstats.py index d0396cb..7471cf9 100644 --- a/app/routers_v3/battingstats.py +++ b/app/routers_v3/battingstats.py @@ -142,9 +142,23 @@ async def get_totalstats( if sum(1 for x in [s_type, (week_start or week_end), week] if x is not None) > 1: raise HTTPException(status_code=400, detail=f'Only one of s_type, week_start/week_end, or week may be used.') + # Build SELECT fields conditionally based on group_by to match GROUP BY exactly + select_fields = [] + + if group_by == 'player': + select_fields = [BattingStat.player] + elif group_by == 'team': + select_fields = [BattingStat.team] + elif group_by == 'playerteam': + select_fields = [BattingStat.player, BattingStat.team] + else: + # Default case + select_fields = [BattingStat.player] + all_stats = ( BattingStat - .select(BattingStat.player, fn.SUM(BattingStat.pa).alias('sum_pa'), fn.SUM(BattingStat.ab).alias('sum_ab'), + .select(*select_fields, + fn.SUM(BattingStat.pa).alias('sum_pa'), fn.SUM(BattingStat.ab).alias('sum_ab'), fn.SUM(BattingStat.run).alias('sum_run'), fn.SUM(BattingStat.hit).alias('sum_hit'), fn.SUM(BattingStat.rbi).alias('sum_rbi'), fn.SUM(BattingStat.double).alias('sum_double'), fn.SUM(BattingStat.triple).alias('sum_triple'), fn.SUM(BattingStat.hr).alias('sum_hr'), @@ -159,8 +173,7 @@ async def get_totalstats( fn.SUM(BattingStat.error).alias('sum_error'), fn.SUM(BattingStat.pb).alias('sum_pb'), fn.SUM(BattingStat.sbc).alias('sum_sbc'), fn.SUM(BattingStat.csc).alias('sum_csc'), fn.SUM(BattingStat.roba).alias('sum_roba'), fn.SUM(BattingStat.robs).alias('sum_robs'), - fn.SUM(BattingStat.raa).alias('sum_raa'), fn.SUM(BattingStat.rto).alias('sum_rto'), - BattingStat.team) + fn.SUM(BattingStat.raa).alias('sum_raa'), fn.SUM(BattingStat.rto).alias('sum_rto')) .where(BattingStat.season == season) .having(fn.SUM(BattingStat.pa) >= min_pa) ) @@ -201,12 +214,8 @@ async def get_totalstats( elif sort == 'team': all_stats = all_stats.order_by(BattingStat.team) if group_by is not None: - if group_by == 'team': - all_stats = all_stats.group_by(BattingStat.team) - elif group_by == 'player': - all_stats = all_stats.group_by(BattingStat.player) - elif group_by == 'playerteam': - all_stats = all_stats.group_by(BattingStat.team, BattingStat.player) + # Use the same fields for GROUP BY as we used for SELECT + all_stats = all_stats.group_by(*select_fields) # if team_abbrev is None and team_id is None and player_name is None and player_id is None: # raise HTTPException( @@ -230,9 +239,23 @@ async def get_totalstats( return_stats = { 'count': all_stats.count(), - 'stats': [{ - 'player': x.player_id if short_output else model_to_dict(x.player, recurse=False), - 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False), + 'stats': [] + } + + for x in all_stats: + # Handle player field based on grouping with safe access + this_player = 'TOT' + if 'player' in group_by and hasattr(x, 'player'): + this_player = x.player_id if short_output else model_to_dict(x.player, recurse=False) + + # Handle team field based on grouping with safe access + this_team = 'TOT' + if 'team' in group_by and hasattr(x, 'team'): + this_team = x.team_id if short_output else model_to_dict(x.team, recurse=False) + + return_stats['stats'].append({ + 'player': this_player, + 'team': this_team, 'pa': x.sum_pa, 'ab': x.sum_ab, 'run': x.sum_run, @@ -253,8 +276,7 @@ async def get_totalstats( 'bpfo': x.sum_bpfo, 'bp1b': x.sum_bp1b, 'bplo': x.sum_bplo - } for x in all_stats] - } + }) db.close() return return_stats diff --git a/app/routers_v3/fieldingstats.py b/app/routers_v3/fieldingstats.py index 5ec0527..ade0239 100644 --- a/app/routers_v3/fieldingstats.py +++ b/app/routers_v3/fieldingstats.py @@ -110,12 +110,26 @@ async def get_totalstats( group_by: Literal['team', 'player', 'playerteam'] = 'player', short_output: Optional[bool] = False, min_ch: Optional[int] = 1, week: list = Query(default=None)): + # Build SELECT fields conditionally based on group_by to match GROUP BY exactly + select_fields = [] + + if group_by == 'player': + select_fields = [BattingStat.player, BattingStat.pos] + elif group_by == 'team': + select_fields = [BattingStat.team, BattingStat.pos] + elif group_by == 'playerteam': + select_fields = [BattingStat.player, BattingStat.team, BattingStat.pos] + else: + # Default case + select_fields = [BattingStat.player, BattingStat.pos] + all_stats = ( BattingStat - .select(BattingStat.player, BattingStat.pos, fn.SUM(BattingStat.xch).alias('sum_xch'), + .select(*select_fields, + fn.SUM(BattingStat.xch).alias('sum_xch'), fn.SUM(BattingStat.xhit).alias('sum_xhit'), fn.SUM(BattingStat.error).alias('sum_error'), fn.SUM(BattingStat.pb).alias('sum_pb'), fn.SUM(BattingStat.sbc).alias('sum_sbc'), - fn.SUM(BattingStat.csc).alias('sum_csc'), BattingStat.team) + fn.SUM(BattingStat.csc).alias('sum_csc')) .where(BattingStat.season == season) .having(fn.SUM(BattingStat.xch) >= min_ch) ) @@ -157,12 +171,8 @@ async def get_totalstats( elif sort == 'team': all_stats = all_stats.order_by(BattingStat.team) if group_by is not None: - if group_by == 'team': - all_stats = all_stats.group_by(BattingStat.pos, BattingStat.team) - elif group_by == 'player': - all_stats = all_stats.group_by(BattingStat.pos, BattingStat.player) - elif group_by == 'playerteam': - all_stats = all_stats.group_by(BattingStat.pos, BattingStat.team, BattingStat.player) + # Use the same fields for GROUP BY as we used for SELECT + all_stats = all_stats.group_by(*select_fields) if team_id is not None: all_teams = Team.select().where(Team.id << team_id) all_stats = all_stats.where(BattingStat.team << all_teams) @@ -178,10 +188,27 @@ async def get_totalstats( all_stats = all_stats.where(BattingStat.player << all_players) return_stats = { - 'count': sum(1 for i in all_stats if i.sum_xch + i.sum_sbc > 0), - 'stats': [{ - 'player': x.player_id if short_output else model_to_dict(x.player, recurse=False), - 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False), + 'count': 0, + 'stats': [] + } + + for x in all_stats: + if x.sum_xch + x.sum_sbc <= 0: + continue + + # Handle player field based on grouping with safe access + this_player = 'TOT' + if 'player' in group_by and hasattr(x, 'player'): + this_player = x.player_id if short_output else model_to_dict(x.player, recurse=False) + + # Handle team field based on grouping with safe access + this_team = 'TOT' + if 'team' in group_by and hasattr(x, 'team'): + this_team = x.team_id if short_output else model_to_dict(x.team, recurse=False) + + return_stats['stats'].append({ + 'player': this_player, + 'team': this_team, 'pos': x.pos, 'xch': x.sum_xch, 'xhit': x.sum_xhit, @@ -189,7 +216,8 @@ async def get_totalstats( 'pb': x.sum_pb, 'sbc': x.sum_sbc, 'csc': x.sum_csc - } for x in all_stats if x.sum_xch + x.sum_sbc > 0] - } + }) + + return_stats['count'] = len(return_stats['stats']) db.close() return return_stats diff --git a/app/routers_v3/pitchingstats.py b/app/routers_v3/pitchingstats.py index 669c1a3..54364ec 100644 --- a/app/routers_v3/pitchingstats.py +++ b/app/routers_v3/pitchingstats.py @@ -129,9 +129,23 @@ async def get_totalstats( if sum(1 for x in [s_type, (week_start or week_end), week] if x is not None) > 1: raise HTTPException(status_code=400, detail=f'Only one of s_type, week_start/week_end, or week may be used.') + # Build SELECT fields conditionally based on group_by to match GROUP BY exactly + select_fields = [] + + if group_by == 'player': + select_fields = [PitchingStat.player] + elif group_by == 'team': + select_fields = [PitchingStat.team] + elif group_by == 'playerteam': + select_fields = [PitchingStat.player, PitchingStat.team] + else: + # Default case + select_fields = [PitchingStat.player] + all_stats = ( PitchingStat - .select(PitchingStat.player, fn.SUM(PitchingStat.ip).alias('sum_ip'), + .select(*select_fields, + fn.SUM(PitchingStat.ip).alias('sum_ip'), fn.SUM(PitchingStat.hit).alias('sum_hit'), fn.SUM(PitchingStat.run).alias('sum_run'), fn.SUM(PitchingStat.erun).alias('sum_erun'), fn.SUM(PitchingStat.so).alias('sum_so'), fn.SUM(PitchingStat.bb).alias('sum_bb'), fn.SUM(PitchingStat.hbp).alias('sum_hbp'), @@ -140,8 +154,7 @@ async def get_totalstats( fn.SUM(PitchingStat.win).alias('sum_win'), fn.SUM(PitchingStat.loss).alias('sum_loss'), fn.SUM(PitchingStat.hold).alias('sum_hold'), fn.SUM(PitchingStat.sv).alias('sum_sv'), fn.SUM(PitchingStat.bsv).alias('sum_bsv'), fn.SUM(PitchingStat.irs).alias('sum_irs'), - fn.SUM(PitchingStat.gs).alias('sum_gs'), fn.COUNT(PitchingStat.game).alias('sum_games'), - PitchingStat.team) + fn.SUM(PitchingStat.gs).alias('sum_gs'), fn.COUNT(PitchingStat.game).alias('sum_games')) .where(PitchingStat.season == season) .having(fn.SUM(PitchingStat.ip) >= ip_min) ) @@ -181,12 +194,8 @@ async def get_totalstats( elif sort == 'team': all_stats = all_stats.order_by(PitchingStat.team) if group_by is not None: - if group_by == 'team': - all_stats = all_stats.group_by(PitchingStat.team) - elif group_by == 'player': - all_stats = all_stats.group_by(PitchingStat.player) - elif group_by == 'playerteam': - all_stats = all_stats.group_by(PitchingStat.team, PitchingStat.player) + # Use the same fields for GROUP BY as we used for SELECT + all_stats = all_stats.group_by(*select_fields) if team_id is not None: all_teams = Team.select().where(Team.id << team_id) all_stats = all_stats.where(PitchingStat.team << all_teams) @@ -202,9 +211,23 @@ async def get_totalstats( return_stats = { 'count': all_stats.count(), - 'stats': [{ - 'player': x.player_id if short_output else model_to_dict(x.player, recurse=False), - 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False), + 'stats': [] + } + + for x in all_stats: + # Handle player field based on grouping with safe access + this_player = 'TOT' + if 'player' in group_by and hasattr(x, 'player'): + this_player = x.player_id if short_output else model_to_dict(x.player, recurse=False) + + # Handle team field based on grouping with safe access + this_team = 'TOT' + if 'team' in group_by and hasattr(x, 'team'): + this_team = x.team_id if short_output else model_to_dict(x.team, recurse=False) + + return_stats['stats'].append({ + 'player': this_player, + 'team': this_team, 'ip': x.sum_ip, 'hit': x.sum_hit, 'run': x.sum_run, @@ -224,8 +247,7 @@ async def get_totalstats( 'hold': x.sum_hold, 'sv': x.sum_sv, 'bsv': x.sum_bsv - } for x in all_stats] - } + }) db.close() return return_stats diff --git a/app/routers_v3/players.py b/app/routers_v3/players.py index 941e5c9..a733df3 100644 --- a/app/routers_v3/players.py +++ b/app/routers_v3/players.py @@ -98,7 +98,7 @@ async def get_players( line.pos_1, line.pos_2, line.pos_3, line.pos_4, line.pos_5, line.pos_6, line.pos_7, line.pos_8, line.last_game, line.last_game2, line.il_return, line.demotion_week, line.headshot, line.vanity_card, line.strat_code.replace(",", "-_-") if line.strat_code is not None else "", - line.bbref_id, line.injury_rating, line.id, line.sbaplayer_id + line.bbref_id, line.injury_rating, line.id, line.sbaplayer ] ) return_players = { diff --git a/app/routers_v3/stratplay.py b/app/routers_v3/stratplay.py index fed44c0..8b751ba 100644 --- a/app/routers_v3/stratplay.py +++ b/app/routers_v3/stratplay.py @@ -416,17 +416,26 @@ async def get_batting_totals( if inning is not None: bat_plays = bat_plays.where(StratPlay.inning_num << inning) + # Initialize game_select_fields for use in GROUP BY + game_select_fields = [] + # Add StratPlay.game to SELECT clause for group_by scenarios that need it if group_by in ['playergame', 'teamgame']: - # Rebuild the query with StratPlay.game included + # For playergame/teamgame grouping, build appropriate SELECT fields + if group_by == 'playergame': + game_select_fields = [StratPlay.batter, StratPlay.game, StratPlay.batter_team] + else: # teamgame + game_select_fields = [StratPlay.batter_team, StratPlay.game] + game_bat_plays = ( StratPlay - .select(StratPlay.batter, StratPlay.game, fn.SUM(StratPlay.pa).alias('sum_pa'), + .select(*game_select_fields, + fn.SUM(StratPlay.pa).alias('sum_pa'), fn.SUM(StratPlay.ab).alias('sum_ab'), fn.SUM(StratPlay.run).alias('sum_run'), fn.SUM(StratPlay.hit).alias('sum_hit'), fn.SUM(StratPlay.rbi).alias('sum_rbi'), fn.SUM(StratPlay.double).alias('sum_double'), fn.SUM(StratPlay.triple).alias('sum_triple'), fn.SUM(StratPlay.homerun).alias('sum_hr'), fn.SUM(StratPlay.bb).alias('sum_bb'), - fn.SUM(StratPlay.so).alias('sum_so'), StratPlay.batter_team, + fn.SUM(StratPlay.so).alias('sum_so'), fn.SUM(StratPlay.hbp).alias('sum_hbp'), fn.SUM(StratPlay.sac).alias('sum_sac'), fn.SUM(StratPlay.ibb).alias('sum_ibb'), fn.SUM(StratPlay.gidp).alias('sum_gidp'), fn.SUM(StratPlay.sb).alias('sum_sb'), fn.SUM(StratPlay.cs).alias('sum_cs'), @@ -489,11 +498,17 @@ async def get_batting_totals( run_plays = run_plays.group_by(StratPlay.runner, StratPlay.runner_team) def_plays = def_plays.group_by(StratPlay.defender, StratPlay.defender_team) elif group_by == 'playergame': - bat_plays = bat_plays.group_by(StratPlay.batter, StratPlay.game) + if game_select_fields: + bat_plays = bat_plays.group_by(*game_select_fields) + else: + bat_plays = bat_plays.group_by(StratPlay.batter, StratPlay.game) run_plays = run_plays.group_by(StratPlay.runner, StratPlay.game) def_plays = def_plays.group_by(StratPlay.defender, StratPlay.game) elif group_by == 'teamgame': - bat_plays = bat_plays.group_by(StratPlay.batter_team, StratPlay.game) + if game_select_fields: + bat_plays = bat_plays.group_by(*game_select_fields) + else: + bat_plays = bat_plays.group_by(StratPlay.batter_team, StratPlay.game) run_plays = run_plays.group_by(StratPlay.runner_team, StratPlay.game) elif group_by == 'league': bat_plays = bat_plays.join(StratGame) @@ -532,11 +547,19 @@ async def get_batting_totals( elif sort == 'pa-asc': bat_plays = bat_plays.order_by(SQL('sum_pa').asc()) elif sort == 'newest': - bat_plays = bat_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) - run_plays = run_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) + # For grouped queries, only sort by fields in GROUP BY clause + if group_by in ['playergame', 'teamgame']: + # StratPlay.game is in GROUP BY for these cases + bat_plays = bat_plays.order_by(StratPlay.game.desc()) + run_plays = run_plays.order_by(StratPlay.game.desc()) + # For other group_by values, skip game_id/play_num sorting since they're not in GROUP BY elif sort == 'oldest': - bat_plays = bat_plays.order_by(StratPlay.game_id, StratPlay.play_num) - run_plays = run_plays.order_by(StratPlay.game_id, StratPlay.play_num) + # For grouped queries, only sort by fields in GROUP BY clause + if group_by in ['playergame', 'teamgame']: + # StratPlay.game is in GROUP BY for these cases + bat_plays = bat_plays.order_by(StratPlay.game.asc()) + run_plays = run_plays.order_by(StratPlay.game.asc()) + # For other group_by values, skip game_id/play_num sorting since they're not in GROUP BY if limit < 1: limit = 1 @@ -1078,9 +1101,17 @@ async def get_fielding_totals( elif sort == 'ch-asc': def_plays = def_plays.order_by(SQL('sum_chances').asc()) elif sort == 'newest': - def_plays = def_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc()) + # For grouped queries, only sort by fields in GROUP BY clause + if group_by in ['playergame', 'playerpositiongame', 'playerweek', 'teamweek']: + # StratPlay.game is in GROUP BY for these cases + def_plays = def_plays.order_by(StratPlay.game.desc()) + # For other group_by values, skip game_id/play_num sorting since they're not in GROUP BY elif sort == 'oldest': - def_plays = def_plays.order_by(StratPlay.game_id, StratPlay.play_num) + # For grouped queries, only sort by fields in GROUP BY clause + if group_by in ['playergame', 'playerpositiongame', 'playerweek', 'teamweek']: + # StratPlay.game is in GROUP BY for these cases + def_plays = def_plays.order_by(StratPlay.game.asc()) + # For other group_by values, skip game_id/play_num sorting since they're not in GROUP BY if limit < 1: limit = 1