Codex commited on
Commit
143a3d0
·
1 Parent(s): e2806c2

Speed up pitcher suite live pitch queries

Browse files
Files changed (1) hide show
  1. src/matchups.js +35 -27
src/matchups.js CHANGED
@@ -1463,6 +1463,10 @@ function buildSplitSqlFilter(columnExpression, parameterIndex) {
1463
  return `AND ($${parameterIndex}::text IS NULL OR $${parameterIndex}::text = 'overall' OR LOWER(${columnExpression}) = CASE WHEN $${parameterIndex}::text = 'vs_lhb' THEN 'l' WHEN $${parameterIndex}::text = 'vs_rhb' THEN 'r' ELSE LOWER(${columnExpression}) END)`;
1464
  }
1465
 
 
 
 
 
1466
  function deriveCountBucket(row) {
1467
  const balls = numberOrNull(row.balls) ?? 0;
1468
  const strikes = numberOrNull(row.strikes) ?? 0;
@@ -3043,6 +3047,7 @@ export class MatchupService {
3043
  const window = String(options.window ?? 'last_5');
3044
  const split = String(options.split ?? 'overall');
3045
  const pitchType = options.pitchType ?? options.pitch_type ?? null;
 
3046
 
3047
  if (view === 'results') {
3048
  const rows = await this.queryCockroachRows(
@@ -3120,11 +3125,11 @@ export class MatchupService {
3120
  AVG(pfx_x) AS avg_pfx_x,
3121
  AVG(pfx_z) AS avg_pfx_z
3122
  FROM public.live_pitch_mix_2026
3123
- WHERE LOWER(pitcher_name) = LOWER($1)
3124
- ${buildPitchTypeSqlFilter('pitch_type', 'pitch_name', 2)}
3125
- ${buildSplitSqlFilter('stand', 3)}
3126
  `,
3127
- [context.pitcherName, pitchType, split]
3128
  );
3129
  const baselineRows = await this.queryCockroachRows(
3130
  `
@@ -3135,11 +3140,11 @@ export class MatchupService {
3135
  AVG(pfx_x) AS avg_pfx_x,
3136
  AVG(pfx_z) AS avg_pfx_z
3137
  FROM public.shared_pitcher_baseline_event_rows
3138
- WHERE LOWER(pitcher_name) = LOWER($1)
3139
- ${buildPitchTypeSqlFilter('pitch_type', 'pitch_name', 2)}
3140
- ${buildSplitSqlFilter('COALESCE(batter_stand, stand)', 3)}
3141
  `,
3142
- [context.pitcherName, pitchType, split]
3143
  );
3144
  const current = currentRows[0] ?? {};
3145
  const baseline = baselineRows[0] ?? {};
@@ -3204,17 +3209,17 @@ export class MatchupService {
3204
  ${overlayAExpr} AS overlay_a,
3205
  ${overlayBExpr} AS overlay_b
3206
  FROM ${tableName}
3207
- WHERE LOWER(pitcher_name) = LOWER($1)
3208
- ${buildPitchTypeSqlFilter('pitch_type', 'pitch_name', 2)}
3209
- ${buildSplitSqlFilter(splitExpr, 3)}
3210
  GROUP BY point_key
3211
  )
3212
  SELECT point_key, primary_value, overlay_a, overlay_b
3213
  FROM grouped
3214
  ORDER BY point_key DESC
3215
- LIMIT $4
3216
  `,
3217
- [context.pitcherName, pitchType, split, getPitcherWindowPointLimit(window)]
3218
  );
3219
  if (!rows.length) {
3220
  throw new Error(`No ${view} trend points were available for ${context.pitcherName}.`);
@@ -3418,17 +3423,18 @@ export class MatchupService {
3418
  const split = String(options.split ?? 'overall');
3419
  const pitchType = options.pitchType ?? options.pitch_type ?? null;
3420
  const countBucket = String(options.countBucket ?? options.count_bucket ?? 'all');
 
3421
  const rows = await this.queryCockroachRows(
3422
  `
3423
  SELECT plate_x, plate_z, zone, pitch_type, pitch_name, stand, balls, strikes, description, events, estimated_woba_using_speedangle
3424
  FROM public.live_pitch_mix_2026
3425
- WHERE LOWER(pitcher_name) = LOWER($1)
3426
- ${buildPitchTypeSqlFilter('pitch_type', 'pitch_name', 2)}
3427
- ${buildSplitSqlFilter('stand', 3)}
3428
  ORDER BY game_date::date DESC, pitch_number DESC
3429
  LIMIT 4000
3430
  `,
3431
- [context.pitcherName, pitchType, split]
3432
  );
3433
  if (!rows.length) {
3434
  throw new Error(`No location rows were available for ${context.pitcherName}.`);
@@ -3461,17 +3467,18 @@ export class MatchupService {
3461
  const view = String(options.view ?? 'count_usage');
3462
  const split = String(options.split ?? 'overall');
3463
  const pitchType = options.pitchType ?? options.pitch_type ?? null;
 
3464
  const rows = await this.queryCockroachRows(
3465
  `
3466
  SELECT pitch_name, balls, strikes, description, stand
3467
  FROM public.live_pitch_mix_2026
3468
- WHERE LOWER(pitcher_name) = LOWER($1)
3469
- ${buildPitchTypeSqlFilter('NULL', 'pitch_name', 2)}
3470
- ${buildSplitSqlFilter('stand', 3)}
3471
  ORDER BY game_date::date DESC, pitch_number DESC
3472
  LIMIT 4000
3473
  `,
3474
- [context.pitcherName, pitchType, split]
3475
  );
3476
  if (!rows.length) {
3477
  throw new Error(`No approach rows were available for ${context.pitcherName}.`);
@@ -3500,6 +3507,7 @@ export class MatchupService {
3500
  const context = await this.resolvePitcherSuiteContext(options.pitcher, options);
3501
  const view = String(options.view ?? 'current_vs_career');
3502
  const window = String(options.window ?? 'last_5');
 
3503
 
3504
  if (view === 'risk_reward') {
3505
  const rows = await this.queryCockroachRows(
@@ -3536,17 +3544,17 @@ export class MatchupService {
3536
  `
3537
  SELECT AVG(release_speed) AS avg_release_speed, AVG(release_spin_rate) AS avg_release_spin_rate, AVG(release_extension) AS avg_release_extension, AVG(pfx_x) AS avg_pfx_x, AVG(pfx_z) AS avg_pfx_z
3538
  FROM public.live_pitch_mix_2026
3539
- WHERE LOWER(pitcher_name) = LOWER($1)
3540
  `,
3541
- [context.pitcherName]
3542
  );
3543
  const baselineRows = await this.queryCockroachRows(
3544
  `
3545
  SELECT AVG(release_speed) AS avg_release_speed, AVG(release_spin_rate) AS avg_release_spin_rate, AVG(release_extension) AS avg_release_extension, AVG(pfx_x) AS avg_pfx_x, AVG(pfx_z) AS avg_pfx_z
3546
  FROM public.shared_pitcher_baseline_event_rows
3547
- WHERE LOWER(pitcher_name) = LOWER($1)
3548
  `,
3549
- [context.pitcherName]
3550
  );
3551
  const current = currentRows[0] ?? {};
3552
  const baseline = baselineRows[0] ?? {};
@@ -3556,12 +3564,12 @@ export class MatchupService {
3556
  `
3557
  SELECT source_season::text AS season_label, AVG(release_speed) AS avg_release_speed, AVG(release_spin_rate) AS avg_release_spin_rate, AVG(release_extension) AS avg_release_extension
3558
  FROM public.shared_pitcher_baseline_event_rows
3559
- WHERE LOWER(pitcher_name) = LOWER($1)
3560
  GROUP BY source_season
3561
  ORDER BY source_season DESC
3562
  LIMIT 6
3563
  `,
3564
- [context.pitcherName]
3565
  );
3566
  return {
3567
  ...context,
 
1463
  return `AND ($${parameterIndex}::text IS NULL OR $${parameterIndex}::text = 'overall' OR LOWER(${columnExpression}) = CASE WHEN $${parameterIndex}::text = 'vs_lhb' THEN 'l' WHEN $${parameterIndex}::text = 'vs_rhb' THEN 'r' ELSE LOWER(${columnExpression}) END)`;
1464
  }
1465
 
1466
+ function buildPitcherIdentitySql(nameColumn, idColumn, nameParameterIndex, idParameterIndex) {
1467
+ return `(($${idParameterIndex}::text IS NOT NULL AND ${idColumn}::text = $${idParameterIndex}) OR ($${idParameterIndex}::text IS NULL AND LOWER(${nameColumn}) = LOWER($${nameParameterIndex})))`;
1468
+ }
1469
+
1470
  function deriveCountBucket(row) {
1471
  const balls = numberOrNull(row.balls) ?? 0;
1472
  const strikes = numberOrNull(row.strikes) ?? 0;
 
3047
  const window = String(options.window ?? 'last_5');
3048
  const split = String(options.split ?? 'overall');
3049
  const pitchType = options.pitchType ?? options.pitch_type ?? null;
3050
+ const pitcherId = context.pitcherId ?? null;
3051
 
3052
  if (view === 'results') {
3053
  const rows = await this.queryCockroachRows(
 
3125
  AVG(pfx_x) AS avg_pfx_x,
3126
  AVG(pfx_z) AS avg_pfx_z
3127
  FROM public.live_pitch_mix_2026
3128
+ WHERE ${buildPitcherIdentitySql('pitcher_name', 'pitcher', 1, 2)}
3129
+ ${buildPitchTypeSqlFilter('pitch_type', 'pitch_name', 3)}
3130
+ ${buildSplitSqlFilter('stand', 4)}
3131
  `,
3132
+ [context.pitcherName, pitcherId, pitchType, split]
3133
  );
3134
  const baselineRows = await this.queryCockroachRows(
3135
  `
 
3140
  AVG(pfx_x) AS avg_pfx_x,
3141
  AVG(pfx_z) AS avg_pfx_z
3142
  FROM public.shared_pitcher_baseline_event_rows
3143
+ WHERE ${buildPitcherIdentitySql('pitcher_name', 'pitcher', 1, 2)}
3144
+ ${buildPitchTypeSqlFilter('pitch_type', 'pitch_name', 3)}
3145
+ ${buildSplitSqlFilter('COALESCE(batter_stand, stand)', 4)}
3146
  `,
3147
+ [context.pitcherName, pitcherId, pitchType, split]
3148
  );
3149
  const current = currentRows[0] ?? {};
3150
  const baseline = baselineRows[0] ?? {};
 
3209
  ${overlayAExpr} AS overlay_a,
3210
  ${overlayBExpr} AS overlay_b
3211
  FROM ${tableName}
3212
+ WHERE ${buildPitcherIdentitySql('pitcher_name', 'pitcher', 1, 2)}
3213
+ ${buildPitchTypeSqlFilter('pitch_type', 'pitch_name', 3)}
3214
+ ${buildSplitSqlFilter(splitExpr, 4)}
3215
  GROUP BY point_key
3216
  )
3217
  SELECT point_key, primary_value, overlay_a, overlay_b
3218
  FROM grouped
3219
  ORDER BY point_key DESC
3220
+ LIMIT $5
3221
  `,
3222
+ [context.pitcherName, pitcherId, pitchType, split, getPitcherWindowPointLimit(window)]
3223
  );
3224
  if (!rows.length) {
3225
  throw new Error(`No ${view} trend points were available for ${context.pitcherName}.`);
 
3423
  const split = String(options.split ?? 'overall');
3424
  const pitchType = options.pitchType ?? options.pitch_type ?? null;
3425
  const countBucket = String(options.countBucket ?? options.count_bucket ?? 'all');
3426
+ const pitcherId = context.pitcherId ?? null;
3427
  const rows = await this.queryCockroachRows(
3428
  `
3429
  SELECT plate_x, plate_z, zone, pitch_type, pitch_name, stand, balls, strikes, description, events, estimated_woba_using_speedangle
3430
  FROM public.live_pitch_mix_2026
3431
+ WHERE ${buildPitcherIdentitySql('pitcher_name', 'pitcher', 1, 2)}
3432
+ ${buildPitchTypeSqlFilter('pitch_type', 'pitch_name', 3)}
3433
+ ${buildSplitSqlFilter('stand', 4)}
3434
  ORDER BY game_date::date DESC, pitch_number DESC
3435
  LIMIT 4000
3436
  `,
3437
+ [context.pitcherName, pitcherId, pitchType, split]
3438
  );
3439
  if (!rows.length) {
3440
  throw new Error(`No location rows were available for ${context.pitcherName}.`);
 
3467
  const view = String(options.view ?? 'count_usage');
3468
  const split = String(options.split ?? 'overall');
3469
  const pitchType = options.pitchType ?? options.pitch_type ?? null;
3470
+ const pitcherId = context.pitcherId ?? null;
3471
  const rows = await this.queryCockroachRows(
3472
  `
3473
  SELECT pitch_name, balls, strikes, description, stand
3474
  FROM public.live_pitch_mix_2026
3475
+ WHERE ${buildPitcherIdentitySql('pitcher_name', 'pitcher', 1, 2)}
3476
+ ${buildPitchTypeSqlFilter('NULL', 'pitch_name', 3)}
3477
+ ${buildSplitSqlFilter('stand', 4)}
3478
  ORDER BY game_date::date DESC, pitch_number DESC
3479
  LIMIT 4000
3480
  `,
3481
+ [context.pitcherName, pitcherId, pitchType, split]
3482
  );
3483
  if (!rows.length) {
3484
  throw new Error(`No approach rows were available for ${context.pitcherName}.`);
 
3507
  const context = await this.resolvePitcherSuiteContext(options.pitcher, options);
3508
  const view = String(options.view ?? 'current_vs_career');
3509
  const window = String(options.window ?? 'last_5');
3510
+ const pitcherId = context.pitcherId ?? null;
3511
 
3512
  if (view === 'risk_reward') {
3513
  const rows = await this.queryCockroachRows(
 
3544
  `
3545
  SELECT AVG(release_speed) AS avg_release_speed, AVG(release_spin_rate) AS avg_release_spin_rate, AVG(release_extension) AS avg_release_extension, AVG(pfx_x) AS avg_pfx_x, AVG(pfx_z) AS avg_pfx_z
3546
  FROM public.live_pitch_mix_2026
3547
+ WHERE ${buildPitcherIdentitySql('pitcher_name', 'pitcher', 1, 2)}
3548
  `,
3549
+ [context.pitcherName, pitcherId]
3550
  );
3551
  const baselineRows = await this.queryCockroachRows(
3552
  `
3553
  SELECT AVG(release_speed) AS avg_release_speed, AVG(release_spin_rate) AS avg_release_spin_rate, AVG(release_extension) AS avg_release_extension, AVG(pfx_x) AS avg_pfx_x, AVG(pfx_z) AS avg_pfx_z
3554
  FROM public.shared_pitcher_baseline_event_rows
3555
+ WHERE ${buildPitcherIdentitySql('pitcher_name', 'pitcher', 1, 2)}
3556
  `,
3557
+ [context.pitcherName, pitcherId]
3558
  );
3559
  const current = currentRows[0] ?? {};
3560
  const baseline = baselineRows[0] ?? {};
 
3564
  `
3565
  SELECT source_season::text AS season_label, AVG(release_speed) AS avg_release_speed, AVG(release_spin_rate) AS avg_release_spin_rate, AVG(release_extension) AS avg_release_extension
3566
  FROM public.shared_pitcher_baseline_event_rows
3567
+ WHERE ${buildPitcherIdentitySql('pitcher_name', 'pitcher', 1, 2)}
3568
  GROUP BY source_season
3569
  ORDER BY source_season DESC
3570
  LIMIT 6
3571
  `,
3572
+ [context.pitcherName, pitcherId]
3573
  );
3574
  return {
3575
  ...context,