@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Make limits and ranges work better with Calendar event queries

Summary:
Fixes T8911. This corrects several issues which could crop up if a calendar event query matched more results than the query limit:

- The desired order was not applied by the SearchEngine -- it applies the first builtin order instead. Provide a proper builtin order.
- When we generate ghosts, we can't do limiting in the database because we may select and then immediately discard a large number of parent events which are outside of the query range.
- For now, just don't limit results to get the behavior correct.
- This may need to be refined eventually to improve performance.
- When trimming events, we could trim parents and fail to generate ghosts from them. Separate parent events out first.
- Try to simplify some logic.

Test Plan: An "Upcoming" dashboard panel with limit 10 and the main Calendar "Upcoming Events" UI now show the same results.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T8911

Differential Revision: https://secure.phabricator.com/D16289

+136 -79
+122 -73
src/applications/calendar/query/PhabricatorCalendarEventQuery.php
··· 75 75 return array('start', 'id'); 76 76 } 77 77 78 + public function getBuiltinOrders() { 79 + return array( 80 + 'start' => array( 81 + 'vector' => array('start', 'id'), 82 + 'name' => pht('Event Start'), 83 + ), 84 + ) + parent::getBuiltinOrders(); 85 + } 86 + 78 87 public function getOrderableColumns() { 79 88 return array( 80 89 'start' => array( ··· 95 104 ); 96 105 } 97 106 107 + protected function shouldLimitResults() { 108 + // When generating ghosts, we can't rely on database ordering because 109 + // MySQL can't predict the ghost start times. We'll just load all matching 110 + // events, then generate results from there. 111 + if ($this->generateGhosts) { 112 + return false; 113 + } 114 + 115 + return true; 116 + } 117 + 98 118 protected function loadPage() { 99 119 $events = $this->loadStandardPage($this->newResultObject()); 100 120 ··· 107 127 return $events; 108 128 } 109 129 110 - $enforced_end = null; 111 130 $raw_limit = $this->getRawResultLimit(); 112 131 113 132 if (!$raw_limit && !$this->rangeEnd) { ··· 121 140 foreach ($events as $key => $event) { 122 141 $sequence_start = 0; 123 142 $sequence_end = null; 124 - $duration = $event->getDuration(); 125 143 $end = null; 126 144 127 145 $instance_of = $event->getInstanceOfEventPHID(); ··· 132 150 continue; 133 151 } 134 152 } 153 + } 135 154 136 - if ($event->getIsRecurring() && $instance_of == null) { 137 - $frequency = $event->getFrequencyUnit(); 138 - $modify_key = '+1 '.$frequency; 155 + // Pull out all of the parents first. We may discard them as we begin 156 + // generating ghost events, but we still want to process all of them. 157 + $parents = array(); 158 + foreach ($events as $key => $event) { 159 + if ($event->isParentEvent()) { 160 + $parents[$key] = $event; 161 + } 162 + } 139 163 140 - if (($this->rangeBegin !== null) && 141 - ($this->rangeBegin > $event->getViewerDateFrom())) { 142 - $max_date = $this->rangeBegin - $duration; 143 - $date = $event->getViewerDateFrom(); 144 - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 164 + // Now that we've picked out all the parent events, we can immediately 165 + // discard anything outside of the time window. 166 + $events = $this->getEventsInRange($events); 145 167 146 - while ($date < $max_date) { 147 - // TODO: optimize this to not loop through all off-screen events 148 - $sequence_start++; 149 - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 150 - $date = $datetime->modify($modify_key)->format('U'); 151 - } 168 + $enforced_end = null; 169 + foreach ($parents as $key => $event) { 170 + $sequence_start = 0; 171 + $sequence_end = null; 172 + $start = null; 152 173 153 - $start = $this->rangeBegin; 154 - } else { 155 - $start = $event->getViewerDateFrom() - $duration; 156 - } 174 + $duration = $event->getDuration(); 175 + 176 + $frequency = $event->getFrequencyUnit(); 177 + $modify_key = '+1 '.$frequency; 157 178 158 - $date = $start; 179 + if (($this->rangeBegin !== null) && 180 + ($this->rangeBegin > $event->getViewerDateFrom())) { 181 + $max_date = $this->rangeBegin - $duration; 182 + $date = $event->getViewerDateFrom(); 159 183 $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 160 184 161 - if (($this->rangeEnd && $event->getRecurrenceEndDate()) && 162 - $this->rangeEnd < $event->getRecurrenceEndDate()) { 163 - $end = $this->rangeEnd; 164 - } else if ($event->getRecurrenceEndDate()) { 165 - $end = $event->getRecurrenceEndDate(); 166 - } else if ($this->rangeEnd) { 167 - $end = $this->rangeEnd; 168 - } else if ($enforced_end) { 169 - if ($end) { 170 - $end = min($end, $enforced_end); 171 - } else { 172 - $end = $enforced_end; 173 - } 185 + while ($date < $max_date) { 186 + // TODO: optimize this to not loop through all off-screen events 187 + $sequence_start++; 188 + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 189 + $date = $datetime->modify($modify_key)->format('U'); 174 190 } 175 191 176 - if ($end) { 177 - $sequence_end = $sequence_start; 178 - while ($date < $end) { 179 - $sequence_end++; 180 - $datetime->modify($modify_key); 181 - $date = $datetime->format('U'); 182 - if ($sequence_end > $raw_limit + $sequence_start) { 183 - break; 184 - } 192 + $start = $this->rangeBegin; 193 + } else { 194 + $start = $event->getViewerDateFrom() - $duration; 195 + } 196 + 197 + $date = $start; 198 + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 199 + 200 + // Select the minimum end time we need to generate events until. 201 + $end_times = array(); 202 + if ($this->rangeEnd) { 203 + $end_times[] = $this->rangeEnd; 204 + } 205 + 206 + if ($event->getRecurrenceEndDate()) { 207 + $end_times[] = $event->getRecurrenceEndDate(); 208 + } 209 + 210 + if ($enforced_end) { 211 + $end_times[] = $enforced_end; 212 + } 213 + 214 + if ($end_times) { 215 + $end = min($end_times); 216 + $sequence_end = $sequence_start; 217 + while ($date < $end) { 218 + $sequence_end++; 219 + $datetime->modify($modify_key); 220 + $date = $datetime->format('U'); 221 + if ($sequence_end > $raw_limit + $sequence_start) { 222 + break; 185 223 } 186 - } else { 187 - $sequence_end = $raw_limit + $sequence_start; 188 224 } 225 + } else { 226 + $sequence_end = $raw_limit + $sequence_start; 227 + } 189 228 190 - $sequence_start = max(1, $sequence_start); 229 + $sequence_start = max(1, $sequence_start); 230 + for ($index = $sequence_start; $index < $sequence_end; $index++) { 231 + $events[] = $event->newGhost($viewer, $index); 232 + } 191 233 192 - for ($index = $sequence_start; $index < $sequence_end; $index++) { 193 - $events[] = $event->newGhost($viewer, $index); 194 - } 234 + // NOTE: We're slicing results every time because this makes it cheaper 235 + // to generate future ghosts. If we already have 100 events that occur 236 + // before July 1, we know we never need to generate ghosts after that 237 + // because they couldn't possibly ever appear in the result set. 195 238 196 - // NOTE: We're slicing results every time because this makes it cheaper 197 - // to generate future ghosts. If we already have 100 events that occur 198 - // before July 1, we know we never need to generate ghosts after that 199 - // because they couldn't possibly ever appear in the result set. 200 - 201 - if ($raw_limit) { 202 - if (count($events) >= $raw_limit) { 203 - $events = msort($events, 'getViewerDateFrom'); 204 - $events = array_slice($events, 0, $raw_limit, true); 205 - $enforced_end = last($events)->getViewerDateFrom(); 206 - } 239 + if ($raw_limit) { 240 + if (count($events) > $raw_limit) { 241 + $events = msort($events, 'getViewerDateFrom'); 242 + $events = array_slice($events, 0, $raw_limit, true); 243 + $enforced_end = last($events)->getViewerDateFrom(); 207 244 } 208 245 } 209 246 } ··· 271 308 } 272 309 } 273 310 311 + $events = msort($events, 'getViewerDateFrom'); 312 + 274 313 return $events; 275 314 } 276 315 277 316 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { 278 317 $parts = parent::buildJoinClauseParts($conn_r); 318 + 279 319 if ($this->inviteePHIDs !== null) { 280 320 $parts[] = qsprintf( 281 321 $conn_r, ··· 284 324 id(new PhabricatorCalendarEventInvitee())->getTableName(), 285 325 PhabricatorCalendarEventInvitee::STATUS_UNINVITED); 286 326 } 327 + 287 328 return $parts; 288 329 } 289 330 ··· 397 438 398 439 399 440 protected function willFilterPage(array $events) { 400 - $range_start = $this->rangeBegin; 401 - $range_end = $this->rangeEnd; 402 441 $instance_of_event_phids = array(); 403 442 $recurring_events = array(); 404 443 $viewer = $this->getViewer(); 405 444 406 - foreach ($events as $key => $event) { 407 - $event_start = $event->getViewerDateFrom(); 408 - $event_end = $event->getViewerDateTo(); 409 - 410 - if ($range_start && $event_end < $range_start) { 411 - unset($events[$key]); 412 - } 413 - if ($range_end && $event_start > $range_end) { 414 - unset($events[$key]); 415 - } 416 - } 445 + $events = $this->getEventsInRange($events); 417 446 418 447 $phids = array(); 419 448 ··· 472 501 } 473 502 474 503 $events = msort($events, 'getViewerDateFrom'); 504 + 505 + return $events; 506 + } 507 + 508 + private function getEventsInRange(array $events) { 509 + $range_start = $this->rangeBegin; 510 + $range_end = $this->rangeEnd; 511 + 512 + foreach ($events as $key => $event) { 513 + $event_start = $event->getViewerDateFrom(); 514 + $event_end = $event->getViewerDateTo(); 515 + 516 + if ($range_start && $event_end < $range_start) { 517 + unset($events[$key]); 518 + } 519 + 520 + if ($range_end && $event_start > $range_end) { 521 + unset($events[$key]); 522 + } 523 + } 475 524 476 525 return $events; 477 526 }
+3 -2
src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
··· 192 192 } 193 193 194 194 if ($upcoming) { 195 + $now = PhabricatorTime::getNow(); 195 196 if ($min_range) { 196 - $min_range = max(time(), $min_range); 197 + $min_range = max($now, $min_range); 197 198 } else { 198 - $min_range = time(); 199 + $min_range = $now; 199 200 } 200 201 } 201 202
+11 -4
src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
··· 142 142 } 143 143 144 144 final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) { 145 - if ($this->getRawResultLimit()) { 146 - return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit()); 147 - } else { 148 - return ''; 145 + if ($this->shouldLimitResults()) { 146 + $limit = $this->getRawResultLimit(); 147 + if ($limit) { 148 + return qsprintf($conn_r, 'LIMIT %d', $limit); 149 + } 149 150 } 151 + 152 + return ''; 153 + } 154 + 155 + protected function shouldLimitResults() { 156 + return true; 150 157 } 151 158 152 159 final protected function didLoadResults(array $results) {