In the time-range condition assembly the branch selection tested $end, but
the "end" attribute is stored in $finish ($finish = $v->GetAttribute("end")).
$end was never assigned, so isset($end) was always false: a bounded
time-range query (both start and end present) fell through to the "start
only" branch and the SQL upper-bound predicate
(first_instance_start <= :time_range_end, and the legacy
dtstart < :time_range_end) was never emitted.
The result stayed correct because a later PHP pass ($range_filter +
expand_event_instances) re-applies the true range -- which is why this was
invisible to the HTTP-level regression suite. But every bounded query
therefore fetched all events whose series starts after the requested end,
parsed each into a vComponent and ran recurrence expansion on it, only to
discard it.
Measured on a 6972-event database, a "today"/"this week" query fetched and
expanded ~99 rows (~15%) more than necessary -- worst on exactly the narrow
near-future windows clients poll most often.
Test $finish so the end predicate is emitted for bounded queries. Output is
unchanged (the existing regression suite still passes); this only lets
PostgreSQL prune events that start after the window instead of dropping them
in PHP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>