diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php index f8f67f7a3e..ae1da3aba0 100644 --- a/src/applications/feed/query/PhabricatorFeedQuery.php +++ b/src/applications/feed/query/PhabricatorFeedQuery.php @@ -1,139 +1,165 @@ filterPHIDs = $phids; return $this; } public function withChronologicalKeys(array $keys) { $this->chronologicalKeys = $keys; return $this; } + public function withEpochInRange($range_min, $range_max) { + $this->rangeMin = $range_min; + $this->rangeMax = $range_max; + return $this; + } + public function newResultObject() { return new PhabricatorFeedStoryData(); } protected function loadPage() { // NOTE: We return raw rows from this method, which is a little unusual. return $this->loadStandardPageRows($this->newResultObject()); } protected function willFilterPage(array $data) { return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer()); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); // NOTE: We perform this join unconditionally (even if we have no filter // PHIDs) to omit rows which have no story references. These story data // rows are notifications or realtime alerts. $ref_table = new PhabricatorFeedStoryReference(); $joins[] = qsprintf( $conn, 'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey', $ref_table->getTableName()); return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->filterPHIDs !== null) { $where[] = qsprintf( $conn, 'ref.objectPHID IN (%Ls)', $this->filterPHIDs); } if ($this->chronologicalKeys !== null) { // NOTE: We want to use integers in the query so we can take advantage // of keys, but can't use %d on 32-bit systems. Make sure all the keys // are integers and then format them raw. $keys = $this->chronologicalKeys; foreach ($keys as $key) { if (!ctype_digit($key)) { throw new Exception( pht("Key '%s' is not a valid chronological key!", $key)); } } $where[] = qsprintf( $conn, 'ref.chronologicalKey IN (%Q)', implode(', ', $keys)); } + // NOTE: We may not have 64-bit PHP, so do the shifts in MySQL instead. + // From EXPLAIN, it appears like MySQL is smart enough to compute the + // result and make use of keys to execute the query. + + if ($this->rangeMin !== null) { + $where[] = qsprintf( + $conn, + 'ref.chronologicalKey >= (%d << 32)', + $this->rangeMin); + } + + if ($this->rangeMax !== null) { + $where[] = qsprintf( + $conn, + 'ref.chronologicalKey < (%d << 32)', + $this->rangeMax); + } + return $where; } protected function buildGroupClause(AphrontDatabaseConnection $conn) { if ($this->filterPHIDs !== null) { return qsprintf($conn, 'GROUP BY ref.chronologicalKey'); } else { return qsprintf($conn, 'GROUP BY story.chronologicalKey'); } } protected function getDefaultOrderVector() { return array('key'); } public function getBuiltinOrders() { return array( 'newest' => array( 'vector' => array('key'), 'name' => pht('Creation (Newest First)'), 'aliases' => array('created'), ), 'oldest' => array( 'vector' => array('-key'), 'name' => pht('Creation (Oldest First)'), ), ); } public function getOrderableColumns() { $table = ($this->filterPHIDs ? 'ref' : 'story'); return array( 'key' => array( 'table' => $table, 'column' => 'chronologicalKey', 'type' => 'string', 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { return array( 'key' => $cursor, ); } protected function getResultCursor($item) { if ($item instanceof PhabricatorFeedStory) { return $item->getChronologicalKey(); } return $item['chronologicalKey']; } protected function getPrimaryTableAlias() { return 'story'; } public function getQueryApplicationClass() { return 'PhabricatorFeedApplication'; } } diff --git a/src/applications/feed/query/PhabricatorFeedSearchEngine.php b/src/applications/feed/query/PhabricatorFeedSearchEngine.php index 6dae9f9c37..d17c756524 100644 --- a/src/applications/feed/query/PhabricatorFeedSearchEngine.php +++ b/src/applications/feed/query/PhabricatorFeedSearchEngine.php @@ -1,132 +1,162 @@ setLabel(pht('Include Users')) ->setKey('userPHIDs'), // NOTE: This query is not executed with EdgeLogic, so we can't use // a fancy logical datasource. id(new PhabricatorSearchDatasourceField()) ->setDatasource(new PhabricatorProjectDatasource()) ->setLabel(pht('Include Projects')) ->setKey('projectPHIDs'), + id(new PhabricatorSearchDateControlField()) + ->setLabel(pht('Occurs After')) + ->setKey('rangeStart'), + id(new PhabricatorSearchDateControlField()) + ->setLabel(pht('Occurs Before')) + ->setKey('rangeEnd'), // NOTE: This is a legacy field retained only for backward // compatibility. If the projects field used EdgeLogic, we could use // `viewerprojects()` to execute an equivalent query. id(new PhabricatorSearchCheckboxesField()) ->setKey('viewerProjects') ->setOptions( array( 'self' => pht('Include stories about projects I am a member of.'), )), ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); $phids = array(); if ($map['userPHIDs']) { $phids += array_fuse($map['userPHIDs']); } if ($map['projectPHIDs']) { $phids += array_fuse($map['projectPHIDs']); } // NOTE: This value may be `true` for older saved queries, or // `array('self')` for newer ones. $viewer_projects = $map['viewerProjects']; if ($viewer_projects) { $viewer = $this->requireViewer(); $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withMemberPHIDs(array($viewer->getPHID())) ->execute(); $phids += array_fuse(mpull($projects, 'getPHID')); } if ($phids) { $query->withFilterPHIDs($phids); } + $range_min = $map['rangeStart']; + if ($range_min) { + $range_min = $range_min->getEpoch(); + } + + $range_max = $map['rangeEnd']; + if ($range_max) { + $range_max = $range_max->getEpoch(); + } + + if ($range_min && $range_max) { + if ($range_min > $range_max) { + throw new PhabricatorSearchConstraintException( + pht( + 'The specified "Occurs Before" date is earlier in time than the '. + 'specified "Occurs After" date, so this query can never match '. + 'any results.')); + } + } + + if ($range_min || $range_max) { + $query->withEpochInRange($range_min, $range_max); + } + return $query; } protected function getURI($path) { return '/feed/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'all' => pht('All Stories'), ); if ($this->requireViewer()->isLoggedIn()) { $names['projects'] = pht('Tags'); } return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; case 'projects': return $query->setParameter('viewerProjects', array('self')); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles) { $builder = new PhabricatorFeedBuilder($objects); if ($this->isPanelContext()) { $builder->setShowHovercards(false); } else { $builder->setShowHovercards(true); } $builder->setUser($this->requireViewer()); $view = $builder->buildView(); $list = phutil_tag_div('phabricator-feed-frame', $view); $result = new PhabricatorApplicationSearchResultView(); $result->setContent($list); return $result; } }