{"record":{"id":"6f209519d25c4eb7","repo":"phacility/phabricator","slug":"event-queries-which-generate-ghost-events-must-inc","errorCode":null,"errorMessage":"Event queries which generate ghost events must include either a result limit or an end date, because they may otherwise generate an infinite number of results. This query has neither.","messagePattern":"Event queries which generate ghost events must include either a result limit or an end date, because they may otherwise generate an infinite number of results\\. This query has neither\\.","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/calendar/query/PhabricatorCalendarEventQuery.php","lineNumber":175,"sourceCode":"\n    return true;\n  }\n\n  protected function loadPage() {\n    $events = $this->loadStandardPage($this->newResultObject());\n\n    $viewer = $this->getViewer();\n    foreach ($events as $event) {\n      $event->applyViewerTimezone($viewer);\n    }\n\n    if (!$this->generateGhosts) {\n      return $events;\n    }\n\n    $raw_limit = $this->getRawResultLimit();\n    if (!$raw_limit && !$this->rangeEnd) {\n      throw new Exception(\n        pht(\n          'Event queries which generate ghost events must include either a '.\n          'result limit or an end date, because they may otherwise generate '.\n          'an infinite number of results. This query has neither.'));\n    }\n\n    foreach ($events as $key => $event) {\n      $sequence_start = 0;\n      $sequence_end = null;\n      $end = null;\n\n      $instance_of = $event->getInstanceOfEventPHID();\n\n      if ($instance_of == null && $this->isCancelled !== null) {\n        if ($event->getIsCancelled() != $this->isCancelled) {\n          unset($events[$key]);\n          continue;\n        }","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/calendar/query/PhabricatorCalendarEventQuery.php#L157-L193","documentation":"PhabricatorCalendarEventQuery throws when setGenerateGhosts(true) is used without either a result limit (setLimit) or an end date (withDateRange). Ghost generation materializes every occurrence of every recurring event matching the query, which is unbounded without one of those caps, so the query refuses rather than loop forever.","triggerScenarios":"id(new PhabricatorCalendarEventQuery())->setViewer($viewer)->setGenerateGhosts(true)->execute() with no setLimit(N) and no withDateRange($start, $end). Even with a rangeBegin only, the query still throws because only rangeEnd counts as the bound.","commonSituations":"Custom calendar dashboards or export scripts that iterate 'all upcoming instances'; upgrading code that previously worked because a limit was set elsewhere and later removed; search-engine code that forgets to forward the user's rangeEnd filter into the query.","solutions":["Add an explicit result cap: ->setLimit(1000)","Add a range end: ->withDateRange($start_epoch, $end_epoch) (an end bound is required; a start-only range is not enough)","For full exports, loop with paging: keep a limit per page and advance rangeBegin until exhausted"],"exampleFix":"// before\n$events = id(new PhabricatorCalendarEventQuery())\n  ->setViewer($viewer)\n  ->setGenerateGhosts(true)\n  ->execute();\n\n// after\n$events = id(new PhabricatorCalendarEventQuery())\n  ->setViewer($viewer)\n  ->setGenerateGhosts(true)\n  ->withDateRange($start_epoch, $end_epoch)\n  ->setLimit(1000)\n  ->execute();","handlingStrategy":"validation","validationCode":"if ($generate_ghosts && !$limit && !$end_epoch) {\n  throw new Exception(\n    'Ghost generation requires a result limit or a range end date.');\n}\n$events = id(new PhabricatorCalendarEventQuery())\n  ->setViewer($viewer)\n  ->setGenerateGhosts($generate_ghosts)\n  ->withDateRange($start_epoch, $end_epoch)\n  ->setLimit($limit ?: 1000)\n  ->execute();","typeGuard":null,"tryCatchPattern":"try {\n  $events = $query->execute();\n} catch (Exception $ex) {\n  if (strpos($ex->getMessage(), 'infinite number of results') !== false) {\n    $query->setLimit(1000);\n    $events = $query->execute();\n  } else {\n    throw $ex;\n  }\n}","preventionTips":["Always pair setGenerateGhosts(true) with withDateRange() or setLimit() by convention","Remember a start-only range does not satisfy the guard — only rangeEnd or a limit does","For exports, page through time windows (e.g. month by month) instead of one unbounded query"],"tags":["phabricator","calendar","query","infinite-loop-guard","policy-query"],"backgroundTag":"unbounded-query-guard","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}