Skip to content

Fix PirepStats(): scope PAX & Cargo totals to the selected airline/aircraft#31

Open
MANFahrer-GF wants to merge 1 commit into
FatihKoz:mainfrom
MANFahrer-GF:fix/pirep-stats-scoping
Open

Fix PirepStats(): scope PAX & Cargo totals to the selected airline/aircraft#31
MANFahrer-GF wants to merge 1 commit into
FatihKoz:mainfrom
MANFahrer-GF:fix/pirep-stats-scoping

Conversation

@MANFahrer-GF
Copy link
Copy Markdown

@MANFahrer-GF MANFahrer-GF commented May 31, 2026

What this fixes

DB_StatServices::PirepStats($airline_id, $aircraft_id) reported VA-wide PAX and Cargo figures on every airline- and aircraft-detail page. All other stats (flights, time, distance, fuel, landing rate) were correctly scoped — only Total/Avg Passengers and Total/Avg Freight were wrong.

On a small operator the effect is dramatic: a business-jet airline with 6 accepted PIREPs (~31 pax / ~993 kg total) showed 439,756 passengers and 33,932,376 kg freight — the whole VA's totals.

Root cause

$allpireps = Pirep::where('state', '!=', PirepState::ACCEPTED)->when(...airline/aircraft...)->pluck('id')->toArray();
$pax_amount = PirepFare::where('type', FareType::PASSENGER)->whereNotIn('pirep_id', $allpireps)->sum('count');

$allpireps is the list of non-accepted pireps of the current scope only. Used as a whereNotIn blacklist it excludes just those few rows and still sums every other airline's/aircraft's fares, so the scoping intent is lost for PAX/CGO.

The constraint you raised (thank you!)

You pointed out the count($allpireps) < 65500 guard exists because a VA with ~200k pireps hit MySQL's hard limit on the number of items in a WHERE ... IN (...) clause. So any fix must not materialise a large id list in PHP — otherwise it just moves the wall.

You also asked to prefer plain Eloquent over selectRaw() with prefixes where possible.

What this PR does

A single JOIN grouped by fare type. The airline/aircraft scope lives in the JOIN's WHERE, so nothing is loaded into PHP — no IN (...) list, no 65k limit, at any VA size — and both PAX and CGO (sum + avg) come back in one query:

$fareRows = PirepFare::query()
    ->join('pireps', 'pireps.id', '=', 'pirep_fares.pirep_id')
    ->whereNull('pireps.deleted_at')
    ->where('pireps.state', PirepState::ACCEPTED)
    ->when(isset($airline_id),  fn ($q) => $q->where('pireps.airline_id', $airline_id))
    ->when(isset($aircraft_id), fn ($q) => $q->where('pireps.aircraft_id', $aircraft_id))
    ->groupBy('pirep_fares.type')
    ->get(['pirep_fares.type', DB::raw('SUM(count) as total'), DB::raw('AVG(count) as average')]);
// then map rows: type === PASSENGER -> pax, type === CARGO -> cgo

Notes on style / safety:

  • No selectRaw with a prefixed identifier. Only DB::raw('SUM(count)') / AVG(count) aggregates remain, and the bare columns count/type are unambiguous because the pireps table has neither (it uses flight_type) — so no table prefix is needed and it works regardless of the configured DB prefix.
  • join() bypasses the Pirep SoftDeletes global scope, so soft-deleted pireps are excluded explicitly via whereNull('pireps.deleted_at'). (As a side effect this is slightly more correct than the old code, which let fares of soft-deleted pireps through.)
  • The old count() < 65500 guard is removed because no id list is ever built.

Verification

Tested on a live install (prefix phpvms, ≈3,094 accepted pireps, ≈8,800 fare rows) by calling the real PirepStats() method:

scope pax (sum/avg) cgo (sum/avg)
per-airline 31 / 5 993 / 166 kg
per-aircraft 31 / 5 993 / 166 kg
whole VA (no args) 442,236 / 76 32,770,422 / 12,039 kg

All identical to the old method's intended results; fare aggregation confirmed at exactly 1 query (via the query log). Empty scope (airline with no pireps) returns sum 0 / avg null and the existing if ($pax_amount > 0) guards simply omit the rows — no crash.

Alternative considered (smaller diff, but slower)

If you'd rather keep the change as close as possible to the current code, a subquery whereIn also fixes it without a 65k limit (pass a builder, not ->pluck(), so Laravel emits IN (SELECT id FROM pireps WHERE ...)):

$scoped = Pirep::where('state', PirepState::ACCEPTED)
    ->when(isset($airline_id),  fn ($q) => $q->where('airline_id', $airline_id))
    ->when(isset($aircraft_id), fn ($q) => $q->where('aircraft_id', $aircraft_id))
    ->select('id'); // builder, NOT ->pluck()

$pax_amount = PirepFare::where('type', FareType::PASSENGER)->whereIn('pirep_id', $scoped)->sum('count');
$pax_avg    = PirepFare::where('type', FareType::PASSENGER)->whereIn('pirep_id', $scoped)->avg('count');
$cgo_amount = PirepFare::where('type', FareType::CARGO)->whereIn('pirep_id', $scoped)->sum('count');
$cgo_avg    = PirepFare::where('type', FareType::CARGO)->whereIn('pirep_id', $scoped)->avg('count');

Trade-offs (both correct, both 65k-safe, soft-deletes handled):

This PR — JOIN + groupBy Alternative — subquery whereIn
Queries 1 4 (sum/avg × pax/cgo)
Measured (live) global ~12 ms · per-airline ~0.75 ms global ~36 ms · per-airline ~2.7 ms
Idiomatic Eloquent uses DB::raw() aggregates pure builder, smallest diff
Soft-deletes explicit whereNull(deleted_at) automatic (Pirep scope)

I went with the JOIN for the single-query performance, but I'm happy to switch this PR to the subquery version if you prefer the smaller, prefix-free diff — your call.


🤖 Generated with Claude Code

Closes #30

PirepStats() reported VA-wide PAX and Cargo figures on every airline- and
aircraft-detail page. The fare totals used a whereNotIn() blacklist built only
from the *non-accepted* pireps of the current scope, so it excluded just those
few rows and still summed every other airline's/aircraft's fares.

Replace it with a single JOIN grouped by fare type. The airline/aircraft scope
lives in the JOIN's WHERE, so the figures are correctly scoped and nothing is
materialised into a PHP id array — this also removes the MySQL ~65k WHERE IN
limit that the original count() < 65500 guard worked around, so the guard is
dropped. Soft-deleted pireps are excluded explicitly since join() bypasses the
Pirep SoftDeletes scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown
Owner

@FatihKoz FatihKoz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am ok with the DB::raw(), it is better than using the selectRaw() after all.

Thanks, 1 query version look good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PirepStats(): airline/aircraft PAX & Cargo totals show VA-wide figures (whereNotIn scoping bug)

2 participants