Codex commited on
Commit ·
143a3d0
1
Parent(s): e2806c2
Speed up pitcher suite live pitch queries
Browse files- 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
|
| 3124 |
-
${buildPitchTypeSqlFilter('pitch_type', 'pitch_name',
|
| 3125 |
-
${buildSplitSqlFilter('stand',
|
| 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
|
| 3139 |
-
${buildPitchTypeSqlFilter('pitch_type', 'pitch_name',
|
| 3140 |
-
${buildSplitSqlFilter('COALESCE(batter_stand, stand)',
|
| 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
|
| 3208 |
-
${buildPitchTypeSqlFilter('pitch_type', 'pitch_name',
|
| 3209 |
-
${buildSplitSqlFilter(splitExpr,
|
| 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 $
|
| 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
|
| 3426 |
-
${buildPitchTypeSqlFilter('pitch_type', 'pitch_name',
|
| 3427 |
-
${buildSplitSqlFilter('stand',
|
| 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
|
| 3469 |
-
${buildPitchTypeSqlFilter('NULL', 'pitch_name',
|
| 3470 |
-
${buildSplitSqlFilter('stand',
|
| 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
|
| 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
|
| 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
|
| 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,
|