diff --git a/src/applications/differential/query/DifferentialChangesetQuery.php b/src/applications/differential/query/DifferentialChangesetQuery.php index 9f19dae847..6cb60e04e9 100644 --- a/src/applications/differential/query/DifferentialChangesetQuery.php +++ b/src/applications/differential/query/DifferentialChangesetQuery.php @@ -1,157 +1,153 @@ ids = $ids; return $this; } public function withDiffs(array $diffs) { assert_instances_of($diffs, 'DifferentialDiff'); $this->diffs = $diffs; return $this; } public function needAttachToDiffs($attach) { $this->needAttachToDiffs = $attach; return $this; } public function needHunks($need) { $this->needHunks = $need; return $this; } protected function willExecute() { // If we fail to load any changesets (which is possible in the case of an // empty commit) we'll never call didFilterPage(). Attach empty changeset // lists now so that we end up with the right result. if ($this->needAttachToDiffs) { foreach ($this->diffs as $diff) { $diff->attachChangesets(array()); } } } protected function loadPage() { $table = new DifferentialChangeset(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $changesets) { // First, attach all the diffs we already have. We can just do this // directly without worrying about querying for them. When we don't have // a diff, record that we need to load it. if ($this->diffs) { $have_diffs = mpull($this->diffs, null, 'getID'); } else { $have_diffs = array(); } $must_load = array(); foreach ($changesets as $key => $changeset) { $diff_id = $changeset->getDiffID(); if (isset($have_diffs[$diff_id])) { $changeset->attachDiff($have_diffs[$diff_id]); } else { $must_load[$key] = $changeset; } } // Load all the diffs we don't have. $need_diff_ids = mpull($must_load, 'getDiffID'); $more_diffs = array(); if ($need_diff_ids) { $more_diffs = id(new DifferentialDiffQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withIDs($need_diff_ids) ->execute(); $more_diffs = mpull($more_diffs, null, 'getID'); } // Attach the diffs we loaded. foreach ($must_load as $key => $changeset) { $diff_id = $changeset->getDiffID(); if (isset($more_diffs[$diff_id])) { $changeset->attachDiff($more_diffs[$diff_id]); } else { // We didn't have the diff, and could not load it (it does not exist, // or we can't see it), so filter this result out. unset($changesets[$key]); } } return $changesets; } protected function didFilterPage(array $changesets) { if ($this->needAttachToDiffs) { $changeset_groups = mgroup($changesets, 'getDiffID'); foreach ($this->diffs as $diff) { $diff_changesets = idx($changeset_groups, $diff->getID(), array()); $diff->attachChangesets($diff_changesets); } } if ($this->needHunks) { id(new DifferentialHunkQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withChangesets($changesets) ->needAttachToChangesets(true) ->execute(); } return $changesets; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->diffs !== null) { $where[] = qsprintf( $conn_r, 'diffID IN (%Ld)', mpull($this->diffs, 'getID')); } if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorDifferentialApplication'; } - protected function getReversePaging() { - return true; - } - } diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index 8f16299c97..df752ccfeb 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -1,1149 +1,1140 @@ authorPHIDs = $authors; return $this; } public function withIDs(array $ids) { $this->taskIDs = $ids; return $this; } public function withPHIDs(array $phids) { $this->taskPHIDs = $phids; return $this; } public function withOwners(array $owners) { $this->includeUnowned = false; foreach ($owners as $k => $phid) { if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS || $phid === null) { $this->includeUnowned = true; unset($owners[$k]); break; } } $this->ownerPHIDs = $owners; return $this; } public function withAllProjects(array $projects) { $this->includeNoProject = false; foreach ($projects as $k => $phid) { if ($phid == ManiphestTaskOwner::PROJECT_NO_PROJECT) { $this->includeNoProject = true; unset($projects[$k]); } } $this->projectPHIDs = $projects; return $this; } /** * Add an additional "all projects" constraint to existing filters. * * This is used by boards to supplement queries. * * @param list List of project PHIDs to add to any existing constraint. * @return this */ public function addWithAllProjects(array $projects) { if ($this->projectPHIDs === null) { $this->projectPHIDs = array(); } return $this->withAllProjects(array_merge($this->projectPHIDs, $projects)); } public function withoutProjects(array $projects) { $this->xprojectPHIDs = $projects; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withPriorities(array $priorities) { $this->priorities = $priorities; return $this; } public function withSubpriorities(array $subpriorities) { $this->subpriorities = $subpriorities; return $this; } public function withSubscribers(array $subscribers) { $this->subscriberPHIDs = $subscribers; return $this; } public function withFullTextSearch($fulltext_search) { $this->fullTextSearch = $fulltext_search; return $this; } public function setGroupBy($group) { $this->groupBy = $group; return $this; } public function setOrderBy($order) { $this->orderBy = $order; return $this; } public function withAnyProjects(array $projects) { $this->anyProjectPHIDs = $projects; return $this; } public function withAnyUserProjects(array $users) { $this->anyUserProjectPHIDs = $users; return $this; } /** * True returns tasks that are blocking other tasks only. * False returns tasks that are not blocking other tasks only. * Null returns tasks regardless of blocking status. */ public function withBlockingTasks($mode) { $this->blockingTasks = $mode; return $this; } public function shouldJoinBlockingTasks() { return $this->blockingTasks !== null; } /** * True returns tasks that are blocked by other tasks only. * False returns tasks that are not blocked by other tasks only. * Null returns tasks regardless of blocked by status. */ public function withBlockedTasks($mode) { $this->blockedTasks = $mode; return $this; } public function shouldJoinBlockedTasks() { return $this->blockedTasks !== null; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withDateModifiedBefore($date_modified_before) { $this->dateModifiedBefore = $date_modified_before; return $this; } public function withDateModifiedAfter($date_modified_after) { $this->dateModifiedAfter = $date_modified_after; return $this; } public function needSubscriberPHIDs($bool) { $this->needSubscriberPHIDs = $bool; return $this; } public function needProjectPHIDs($bool) { $this->needProjectPHIDs = $bool; return $this; } protected function willExecute() { // Make sure the user can see any projects specified in this // query FIRST. if ($this->projectPHIDs) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($this->projectPHIDs) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($this->projectPHIDs as $index => $phid) { $project = idx($projects, $phid); if (!$project) { unset($this->projectPHIDs[$index]); continue; } } if (!$this->projectPHIDs) { $this->projectPolicyCheckFailed = true; } $this->projectPHIDs = array_values($this->projectPHIDs); } } protected function loadPage() { if ($this->projectPolicyCheckFailed) { throw new PhabricatorEmptyQueryException(); } $task_dao = new ManiphestTask(); $conn = $task_dao->establishConnection('r'); $where = array(); $where[] = $this->buildTaskIDsWhereClause($conn); $where[] = $this->buildTaskPHIDsWhereClause($conn); $where[] = $this->buildStatusWhereClause($conn); $where[] = $this->buildStatusesWhereClause($conn); $where[] = $this->buildPrioritiesWhereClause($conn); $where[] = $this->buildDependenciesWhereClause($conn); $where[] = $this->buildAuthorWhereClause($conn); $where[] = $this->buildOwnerWhereClause($conn); $where[] = $this->buildProjectWhereClause($conn); $where[] = $this->buildAnyProjectWhereClause($conn); $where[] = $this->buildAnyUserProjectWhereClause($conn); $where[] = $this->buildXProjectWhereClause($conn); $where[] = $this->buildFullTextWhereClause($conn); if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn, 'task.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn, 'task.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->dateModifiedAfter) { $where[] = qsprintf( $conn, 'task.dateModified >= %d', $this->dateModifiedAfter); } if ($this->dateModifiedBefore) { $where[] = qsprintf( $conn, 'task.dateModified <= %d', $this->dateModifiedBefore); } $where[] = $this->buildPagingClause($conn); $where = $this->formatWhereClause($where); $having = ''; $count = ''; if (count($this->projectPHIDs) > 1) { // We want to treat the query as an intersection query, not a union // query. We sum the project count and require it be the same as the // number of projects we're searching for. $count = ', COUNT(project.dst) projectCount'; $having = qsprintf( $conn, 'HAVING projectCount = %d', count($this->projectPHIDs)); } $order = $this->buildCustomOrderClause($conn); // TODO: Clean up this nonstandardness. if (!$this->getLimit()) { $this->setLimit(self::DEFAULT_PAGE_SIZE); } $group_column = ''; switch ($this->groupBy) { case self::GROUP_PROJECT: $group_column = qsprintf( $conn, ', projectGroupName.indexedObjectPHID projectGroupPHID'); break; } $rows = queryfx_all( $conn, 'SELECT task.* %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', $count, $group_column, $task_dao->getTableName(), $this->buildJoinsClause($conn), $where, $this->buildGroupClause($conn), $having, $order, $this->buildLimitClause($conn)); switch ($this->groupBy) { case self::GROUP_PROJECT: $data = ipull($rows, null, 'id'); break; default: $data = $rows; break; } $tasks = $task_dao->loadAllFromArray($data); switch ($this->groupBy) { case self::GROUP_PROJECT: $results = array(); foreach ($rows as $row) { $task = clone $tasks[$row['id']]; $task->attachGroupByProjectPHID($row['projectGroupPHID']); $results[] = $task; } $tasks = $results; break; } return $tasks; } protected function willFilterPage(array $tasks) { if ($this->groupBy == self::GROUP_PROJECT) { // We should only return project groups which the user can actually see. $project_phids = mpull($tasks, 'getGroupByProjectPHID'); $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($tasks as $key => $task) { if (!$task->getGroupByProjectPHID()) { // This task is either not in any projects, or only in projects // which we're ignoring because they're being queried for explicitly. continue; } if (empty($projects[$task->getGroupByProjectPHID()])) { unset($tasks[$key]); } } } return $tasks; } protected function didFilterPage(array $tasks) { $phids = mpull($tasks, 'getPHID'); if ($this->needProjectPHIDs) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($tasks as $task) { $project_phids = $edge_query->getDestinationPHIDs( array($task->getPHID())); $task->attachProjectPHIDs($project_phids); } } if ($this->needSubscriberPHIDs) { $subscriber_sets = id(new PhabricatorSubscribersQuery()) ->withObjectPHIDs($phids) ->execute(); foreach ($tasks as $task) { $subscribers = idx($subscriber_sets, $task->getPHID(), array()); $task->attachSubscriberPHIDs($subscribers); } } return $tasks; } private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) { if (!$this->taskIDs) { return null; } return qsprintf( $conn, 'task.id in (%Ld)', $this->taskIDs); } private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) { if (!$this->taskPHIDs) { return null; } return qsprintf( $conn, 'task.phid in (%Ls)', $this->taskPHIDs); } private function buildStatusWhereClause(AphrontDatabaseConnection $conn) { static $map = array( self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID, self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE, self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE, ); switch ($this->status) { case self::STATUS_ANY: return null; case self::STATUS_OPEN: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); case self::STATUS_CLOSED: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getClosedStatusConstants()); default: $constant = idx($map, $this->status); if (!$constant) { throw new Exception("Unknown status query '{$this->status}'!"); } return qsprintf( $conn, 'task.status = %s', $constant); } } private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) { if ($this->statuses) { return qsprintf( $conn, 'task.status IN (%Ls)', $this->statuses); } return null; } private function buildPrioritiesWhereClause(AphrontDatabaseConnection $conn) { if ($this->priorities) { return qsprintf( $conn, 'task.priority IN (%Ld)', $this->priorities); } if ($this->subpriorities) { return qsprintf( $conn, 'task.subpriority IN (%Lf)', $this->subpriorities); } return null; } private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) { if (!$this->authorPHIDs) { return null; } return qsprintf( $conn, 'task.authorPHID in (%Ls)', $this->authorPHIDs); } private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) { if (!$this->ownerPHIDs) { if ($this->includeUnowned === null) { return null; } else if ($this->includeUnowned) { return qsprintf( $conn, 'task.ownerPHID IS NULL'); } else { return qsprintf( $conn, 'task.ownerPHID IS NOT NULL'); } } if ($this->includeUnowned) { return qsprintf( $conn, 'task.ownerPHID IN (%Ls) OR task.ownerPHID IS NULL', $this->ownerPHIDs); } else { return qsprintf( $conn, 'task.ownerPHID IN (%Ls)', $this->ownerPHIDs); } } private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) { if (!strlen($this->fullTextSearch)) { return null; } // In doing a fulltext search, we first find all the PHIDs that match the // fulltext search, and then use that to limit the rest of the search $fulltext_query = id(new PhabricatorSavedQuery()) ->setEngineClassName('PhabricatorSearchApplicationSearchEngine') ->setParameter('query', $this->fullTextSearch); // NOTE: Setting this to something larger than 2^53 will raise errors in // ElasticSearch, and billions of results won't fit in memory anyway. $fulltext_query->setParameter('limit', 100000); $fulltext_query->setParameter('type', ManiphestTaskPHIDType::TYPECONST); $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); $fulltext_results = $engine->executeSearch($fulltext_query); if (empty($fulltext_results)) { $fulltext_results = array(null); } return qsprintf( $conn, 'task.phid IN (%Ls)', $fulltext_results); } private function buildDependenciesWhereClause( AphrontDatabaseConnection $conn) { if (!$this->shouldJoinBlockedTasks() && !$this->shouldJoinBlockingTasks()) { return null; } $parts = array(); if ($this->blockingTasks === true) { $parts[] = qsprintf( $conn, 'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockingTasks === false) { $parts[] = qsprintf( $conn, 'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } if ($this->blockedTasks === true) { $parts[] = qsprintf( $conn, 'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockedTasks === false) { $parts[] = qsprintf( $conn, 'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } return '('.implode(') OR (', $parts).')'; } private function buildProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->projectPHIDs && !$this->includeNoProject) { return null; } $parts = array(); if ($this->projectPHIDs) { $parts[] = qsprintf( $conn, 'project.dst in (%Ls)', $this->projectPHIDs); } if ($this->includeNoProject) { $parts[] = qsprintf( $conn, 'project.dst IS NULL'); } return '('.implode(') OR (', $parts).')'; } private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->anyProjectPHIDs) { return null; } return qsprintf( $conn, 'anyproject.dst IN (%Ls)', $this->anyProjectPHIDs); } private function buildAnyUserProjectWhereClause( AphrontDatabaseConnection $conn) { if (!$this->anyUserProjectPHIDs) { return null; } $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($this->anyUserProjectPHIDs) ->execute(); $any_user_project_phids = mpull($projects, 'getPHID'); if (!$any_user_project_phids) { throw new PhabricatorEmptyQueryException(); } return qsprintf( $conn, 'anyproject.dst IN (%Ls)', $any_user_project_phids); } private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->xprojectPHIDs) { return null; } return qsprintf( $conn, 'xproject.dst IS NULL'); } private function buildCustomOrderClause(AphrontDatabaseConnection $conn) { $reverse = ($this->getBeforeID() xor $this->getReversePaging()); $order = array(); switch ($this->groupBy) { case self::GROUP_NONE: break; case self::GROUP_PRIORITY: $order[] = 'task.priority'; break; case self::GROUP_OWNER: $order[] = 'task.ownerOrdering'; break; case self::GROUP_STATUS: $order[] = 'task.status'; break; case self::GROUP_PROJECT: $order[] = ''; break; default: throw new Exception("Unknown group query '{$this->groupBy}'!"); } $app_order = $this->buildApplicationSearchOrders($conn, $reverse); if (!$app_order) { switch ($this->orderBy) { case self::ORDER_PRIORITY: $order[] = 'task.priority'; $order[] = 'task.subpriority'; $order[] = 'task.dateModified'; break; case self::ORDER_CREATED: $order[] = 'task.id'; break; case self::ORDER_MODIFIED: $order[] = 'task.dateModified'; break; case self::ORDER_TITLE: $order[] = 'task.title'; break; default: throw new Exception("Unknown order query '{$this->orderBy}'!"); } } $order = array_unique($order); if (empty($order) && empty($app_order)) { return null; } foreach ($order as $k => $column) { switch ($column) { case 'subpriority': case 'ownerOrdering': case 'title': if ($reverse) { $order[$k] = "{$column} DESC"; } else { $order[$k] = "{$column} ASC"; } break; case '': // Put "No Project" at the end of the list. if ($reverse) { $order[$k] = 'projectGroupName.indexedObjectName IS NULL DESC, '. 'projectGroupName.indexedObjectName DESC'; } else { $order[$k] = 'projectGroupName.indexedObjectName IS NULL ASC, '. 'projectGroupName.indexedObjectName ASC'; } break; default: if ($reverse) { $order[$k] = "{$column} ASC"; } else { $order[$k] = "{$column} DESC"; } break; } } if ($app_order) { foreach ($app_order as $order_by) { $order[] = $order_by; } } if ($reverse) { $order[] = 'task.id ASC'; } else { $order[] = 'task.id DESC'; } return 'ORDER BY '.implode(', ', $order); } private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; $joins = array(); if ($this->projectPHIDs || $this->includeNoProject) { $joins[] = qsprintf( $conn_r, '%Q JOIN %T project ON project.src = task.phid AND project.type = %d', ($this->includeNoProject ? 'LEFT' : ''), $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } if ($this->shouldJoinBlockingTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocking ON blocking.src = task.phid '. 'AND blocking.type = %d '. 'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid', $edge_table, ManiphestTaskDependedOnByTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->shouldJoinBlockedTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocked ON blocked.src = task.phid '. 'AND blocked.type = %d '. 'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid', $edge_table, ManiphestTaskDependsOnTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T anyproject ON anyproject.src = task.phid AND anyproject.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } if ($this->xprojectPHIDs) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T xproject ON xproject.src = task.phid AND xproject.type = %d AND xproject.dst IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $this->xprojectPHIDs); } if ($this->subscriberPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T e_ccs ON e_ccs.src = task.phid '. 'AND e_ccs.type = %s '. 'AND e_ccs.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasSubscriberEdgeType::EDGECONST, $this->subscriberPHIDs); } switch ($this->groupBy) { case self::GROUP_PROJECT: $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs(); if ($ignore_group_phids) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d AND projectGroup.dst NOT IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $ignore_group_phids); } else { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroupName ON projectGroup.dst = projectGroupName.indexedObjectPHID', id(new ManiphestNameIndex())->getTableName()); break; } $joins[] = $this->buildApplicationSearchJoinClause($conn_r); return implode(' ', $joins); } private function buildGroupClause(AphrontDatabaseConnection $conn_r) { $joined_multiple_rows = (count($this->projectPHIDs) > 1) || (count($this->anyProjectPHIDs) > 1) || $this->shouldJoinBlockingTasks() || $this->shouldJoinBlockedTasks() || ($this->getApplicationSearchMayJoinMultipleRows()); $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); // If we're joining multiple rows, we need to group the results by the // task IDs. if ($joined_multiple_rows) { if ($joined_project_name) { return 'GROUP BY task.phid, projectGroup.dst'; } else { return 'GROUP BY task.phid'; } } else { return ''; } } /** * Return project PHIDs which we should ignore when grouping tasks by * project. For example, if a user issues a query like: * * Tasks in all projects: Frontend, Bugs * * ...then we don't show "Frontend" or "Bugs" groups in the result set, since * they're meaningless as all results are in both groups. * * Similarly, for queries like: * * Tasks in any projects: Public Relations * * ...we ignore the single project, as every result is in that project. (In * the case that there are several "any" projects, we do not ignore them.) * * @return list Project PHIDs which should be ignored in query * construction. */ private function getIgnoreGroupedProjectPHIDs() { $phids = array(); if ($this->projectPHIDs) { $phids[] = $this->projectPHIDs; } if (count($this->anyProjectPHIDs) == 1) { $phids[] = $this->anyProjectPHIDs; } // Maybe we should also exclude the "excludeProjectPHIDs"? It won't // impact the results, but we might end up with a better query plan. // Investigate this on real data? This is likely very rare. return array_mergev($phids); } protected function getPagingValue($result) { $id = $result->getID(); switch ($this->groupBy) { case self::GROUP_NONE: return $id; case self::GROUP_PRIORITY: return $id.'.'.$result->getPriority(); case self::GROUP_OWNER: return rtrim($id.'.'.$result->getOwnerPHID(), '.'); case self::GROUP_STATUS: return $id.'.'.$result->getStatus(); case self::GROUP_PROJECT: return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.'); default: throw new Exception("Unknown group query '{$this->groupBy}'!"); } } protected function buildPagingClause(AphrontDatabaseConnection $conn_r) { - $default = parent::buildPagingClause($conn_r); - $before_id = $this->getBeforeID(); $after_id = $this->getAfterID(); if (!$before_id && !$after_id) { - return $default; + return ''; } $cursor_id = nonempty($before_id, $after_id); $cursor_parts = explode('.', $cursor_id, 2); $task_id = $cursor_parts[0]; $group_id = idx($cursor_parts, 1); $cursor = $this->loadCursorObject($task_id); if (!$cursor) { // We may loop if we have a cursor and don't build a paging clause; fail // instead. throw new PhabricatorEmptyQueryException(); } $columns = array(); switch ($this->groupBy) { case self::GROUP_NONE: break; case self::GROUP_PRIORITY: $columns[] = array( 'table' => 'task', 'column' => 'priority', 'value' => (int)$group_id, 'type' => 'int', ); break; case self::GROUP_OWNER: - $columns[] = array( - 'table' => 'task', - 'column' => 'ownerOrdering', - 'value' => strlen($group_id), - 'type' => 'null', - ); + $value = null; if ($group_id) { $paging_users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($group_id)) ->execute(); - if (!$paging_users) { - return null; + if ($paging_users) { + $value = head($paging_users)->getUsername(); } - $columns[] = array( - 'table' => 'task', - 'column' => 'ownerOrdering', - 'value' => head($paging_users)->getUsername(), - 'type' => 'string', - 'reverse' => true, - ); } + $columns[] = array( + 'table' => 'task', + 'column' => 'ownerOrdering', + 'value' => $value, + 'type' => 'string', + 'null' => 'head', + 'reverse' => true, + ); break; case self::GROUP_STATUS: $columns[] = array( 'table' => 'task', 'column' => 'status', 'value' => $group_id, 'type' => 'string', ); break; case self::GROUP_PROJECT: - $columns[] = array( - 'table' => 'projectGroupName', - 'column' => 'indexedObjectName', - 'value' => strlen($group_id), - 'type' => 'null', - ); + $value = null; if ($group_id) { $paging_projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($group_id)) ->execute(); - if (!$paging_projects) { - return null; + if ($paging_projects) { + $value = head($paging_projects)->getName(); } - $columns[] = array( - 'table' => 'projectGroupName', - 'column' => 'indexedObjectName', - 'value' => head($paging_projects)->getName(), - 'type' => 'string', - 'reverse' => true, - ); } + + $columns[] = array( + 'table' => 'projectGroupName', + 'column' => 'indexedObjectName', + 'value' => $value, + 'type' => 'string', + 'null' => 'head', + 'reverse' => true, + ); break; default: throw new Exception("Unknown group query '{$this->groupBy}'!"); } $app_columns = $this->buildApplicationSearchPagination($conn_r, $cursor); if ($app_columns) { $columns = array_merge($columns, $app_columns); } else { switch ($this->orderBy) { case self::ORDER_PRIORITY: if ($this->groupBy != self::GROUP_PRIORITY) { $columns[] = array( 'table' => 'task', 'column' => 'priority', 'value' => (int)$cursor->getPriority(), 'type' => 'int', ); } $columns[] = array( 'table' => 'task', 'column' => 'subpriority', 'value' => $cursor->getSubpriority(), 'type' => 'float', ); $columns[] = array( 'table' => 'task', 'column' => 'dateModified', 'value' => (int)$cursor->getDateModified(), 'type' => 'int', ); break; case self::ORDER_CREATED: // This just uses the ID column, below. break; case self::ORDER_MODIFIED: $columns[] = array( 'table' => 'task', 'column' => 'dateModified', 'value' => (int)$cursor->getDateModified(), 'type' => 'int', ); break; case self::ORDER_TITLE: $columns[] = array( 'table' => 'task', 'column' => 'title', 'value' => $cursor->getTitle(), 'type' => 'string', ); break; default: throw new Exception("Unknown order query '{$this->orderBy}'!"); } } $columns[] = array( 'table' => 'task', 'column' => 'id', 'value' => $cursor->getID(), 'type' => 'int', ); return $this->buildPagingClauseFromMultipleColumns( $conn_r, $columns, array( 'reversed' => (bool)($before_id xor $this->getReversePaging()), )); } protected function getPrimaryTableAlias() { return 'task'; } public function getQueryApplicationClass() { return 'PhabricatorManiphestApplication'; } public function setReversePaging($reverse_paging) { $this->reversePaging = $reverse_paging; return $this; } protected function getReversePaging() { return $this->reversePaging; } } diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php index ecf8656fe5..b6e0c85f48 100644 --- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php +++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php @@ -1,337 +1,333 @@ ids = $ids; return $this; } public function withUserPHIDs(array $user_phids) { $this->userPHIDs = $user_phids; return $this; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function withEnded($ended) { $this->ended = $ended; return $this; } public function setOrder($order) { switch ($order) { case self::ORDER_ID_ASC: $this->setOrderVector(array('-id')); break; case self::ORDER_ID_DESC: $this->setOrderVector(array('id')); break; case self::ORDER_STARTED_ASC: $this->setOrderVector(array('-start', '-id')); break; case self::ORDER_STARTED_DESC: $this->setOrderVector(array('start', 'id')); break; case self::ORDER_ENDED_ASC: - $this->setOrderVector(array('-ongoing', '-end', '-id')); + $this->setOrderVector(array('-end', '-id')); break; case self::ORDER_ENDED_DESC: - $this->setOrderVector(array('ongoing', 'end', 'id')); + $this->setOrderVector(array('end', 'id')); break; default: throw new Exception(pht('Unknown order "%s".', $order)); } return $this; } public function needPreemptingEvents($need_events) { $this->needPreemptingEvents = $need_events; return $this; } private function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->userPHIDs !== null) { $where[] = qsprintf( $conn, 'userPHID IN (%Ls)', $this->userPHIDs); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } switch ($this->ended) { case self::ENDED_ALL: break; case self::ENDED_YES: $where[] = qsprintf( $conn, 'dateEnded IS NOT NULL'); break; case self::ENDED_NO: $where[] = qsprintf( $conn, 'dateEnded IS NULL'); break; default: throw new Exception("Unknown ended '{$this->ended}'!"); } $where[] = $this->buildPagingClause($conn); return $this->formatWhereClause($where); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'start' => array( 'column' => 'dateStarted', 'type' => 'int', ), - 'ongoing' => array( - 'column' => 'dateEnded', - 'type' => 'null', - ), 'end' => array( 'column' => 'dateEnded', 'type' => 'int', + 'null' => 'head', ), ); } protected function getPagingValueMap($cursor, array $keys) { $usertime = $this->loadCursorObject($cursor); return array( 'id' => $usertime->getID(), 'start' => $usertime->getDateStarted(), - 'ongoing' => $usertime->getDateEnded(), 'end' => $usertime->getDateEnded(), ); } protected function loadPage() { $usertime = new PhrequentUserTime(); $conn = $usertime->establishConnection('r'); $data = queryfx_all( $conn, 'SELECT usertime.* FROM %T usertime %Q %Q %Q', $usertime->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $usertime->loadAllFromArray($data); } protected function didFilterPage(array $page) { if ($this->needPreemptingEvents) { $usertime = new PhrequentUserTime(); $conn_r = $usertime->establishConnection('r'); $preempt = array(); foreach ($page as $event) { $preempt[] = qsprintf( $conn_r, '(userPHID = %s AND (dateStarted BETWEEN %d AND %d) AND (dateEnded IS NULL OR dateEnded > %d))', $event->getUserPHID(), $event->getDateStarted(), nonempty($event->getDateEnded(), PhabricatorTime::getNow()), $event->getDateStarted()); } $preempting_events = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE %Q ORDER BY dateStarted ASC, id ASC', $usertime->getTableName(), implode(' OR ', $preempt)); $preempting_events = $usertime->loadAllFromArray($preempting_events); $preempting_events = mgroup($preempting_events, 'getUserPHID'); foreach ($page as $event) { $e_start = $event->getDateStarted(); $e_end = $event->getDateEnded(); $select = array(); $user_events = idx($preempting_events, $event->getUserPHID(), array()); foreach ($user_events as $u_event) { if ($u_event->getID() == $event->getID()) { // Don't allow an event to preempt itself. continue; } $u_start = $u_event->getDateStarted(); $u_end = $u_event->getDateEnded(); if ($u_start < $e_start) { // This event started before our event started, so it's not // preempting us. continue; } if ($u_start == $e_start) { if ($u_event->getID() < $event->getID()) { // This event started at the same time as our event started, // but has a lower ID, so it's not preempting us. continue; } } if (($e_end !== null) && ($u_start > $e_end)) { // Our event has ended, and this event started after it ended. continue; } if (($u_end !== null) && ($u_end < $e_start)) { // This event ended before our event began. continue; } $select[] = $u_event; } $event->attachPreemptingEvents($select); } } return $page; } /* -( Helper Functions ) --------------------------------------------------- */ public static function getEndedSearchOptions() { return array( self::ENDED_ALL => pht('All'), self::ENDED_NO => pht('No'), self::ENDED_YES => pht('Yes'), ); } public static function getOrderSearchOptions() { return array( self::ORDER_STARTED_ASC => pht('by furthest start date'), self::ORDER_STARTED_DESC => pht('by nearest start date'), self::ORDER_ENDED_ASC => pht('by furthest end date'), self::ORDER_ENDED_DESC => pht('by nearest end date'), ); } public static function getUserTotalObjectsTracked( PhabricatorUser $user, $limit = PHP_INT_MAX) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); $count = queryfx_one( $conn, 'SELECT COUNT(usertime.id) N FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.dateEnded IS NULL '. 'LIMIT %d', $usertime_dao->getTableName(), $user->getPHID(), $limit); return $count['N']; } public static function isUserTrackingObject( PhabricatorUser $user, $phid) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); $count = queryfx_one( $conn, 'SELECT COUNT(usertime.id) N FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); return $count['N'] > 0; } public static function getUserTimeSpentOnObject( PhabricatorUser $user, $phid) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); // First calculate all the time spent where the // usertime blocks have ended. $sum_ended = queryfx_one( $conn, 'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '. 'FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NOT NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); // Now calculate the time spent where the usertime // blocks have not yet ended. $sum_not_ended = queryfx_one( $conn, 'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '. 'FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); return $sum_ended['N'] + $sum_not_ended['N']; } public function getQueryApplicationClass() { return 'PhabricatorPhrequentApplication'; } } diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php index 4a10b5f7b6..7e74261cbf 100644 --- a/src/applications/repository/query/PhabricatorRepositoryQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php @@ -1,585 +1,539 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withCallsigns(array $callsigns) { $this->callsigns = $callsigns; return $this; } public function withIdentifiers(array $identifiers) { $ids = array(); $callsigns = array(); $phids = array(); foreach ($identifiers as $identifier) { if (ctype_digit($identifier)) { $ids[$identifier] = $identifier; } else { $repository_type = PhabricatorRepositoryRepositoryPHIDType::TYPECONST; if (phid_get_type($identifier) === $repository_type) { $phids[$identifier] = $identifier; } else { $callsigns[$identifier] = $identifier; } } } $this->numericIdentifiers = $ids; $this->callsignIdentifiers = $callsigns; $this->phidIdentifiers = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withHosted($hosted) { $this->hosted = $hosted; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withUUIDs(array $uuids) { $this->uuids = $uuids; return $this; } public function withNameContains($contains) { $this->nameContains = $contains; return $this; } public function withRemoteURIs(array $uris) { $this->remoteURIs = $uris; return $this; } public function withAnyProjects(array $projects) { $this->anyProjectPHIDs = $projects; return $this; } public function needCommitCounts($need_counts) { $this->needCommitCounts = $need_counts; return $this; } public function needMostRecentCommits($need_commits) { $this->needMostRecentCommits = $need_commits; return $this; } public function needProjectPHIDs($need_phids) { $this->needProjectPHIDs = $need_phids; return $this; } public function setOrder($order) { - $this->order = $order; + switch ($order) { + case self::ORDER_CREATED: + $this->setOrderVector(array('id')); + break; + case self::ORDER_COMMITTED: + $this->setOrderVector(array('committed', 'id')); + break; + case self::ORDER_CALLSIGN: + $this->setOrderVector(array('callsign')); + break; + case self::ORDER_NAME: + $this->setOrderVector(array('name', 'id')); + break; + case self::ORDER_SIZE: + $this->setOrderVector(array('size', 'id')); + break; + default: + throw new Exception(pht('Unknown order "%s".', $order)); + } return $this; } public function getIdentifierMap() { if ($this->identifierMap === null) { throw new Exception( 'You must execute() the query before accessing the identifier map.'); } return $this->identifierMap; } protected function willExecute() { $this->identifierMap = array(); } protected function loadPage() { $table = new PhabricatorRepository(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T r %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinsClause($conn_r), $this->buildWhereClause($conn_r), - $this->buildCustomOrderClause($conn_r), + $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $repositories = $table->loadAllFromArray($data); if ($this->needCommitCounts) { $sizes = ipull($data, 'size', 'id'); foreach ($repositories as $id => $repository) { $repository->attachCommitCount(nonempty($sizes[$id], 0)); } } if ($this->needMostRecentCommits) { $commit_ids = ipull($data, 'lastCommitID', 'id'); $commit_ids = array_filter($commit_ids); if ($commit_ids) { $commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withIDs($commit_ids) ->execute(); } else { $commits = array(); } foreach ($repositories as $id => $repository) { $commit = null; if (idx($commit_ids, $id)) { $commit = idx($commits, $commit_ids[$id]); } $repository->attachMostRecentCommit($commit); } } return $repositories; } protected function willFilterPage(array $repositories) { assert_instances_of($repositories, 'PhabricatorRepository'); // TODO: Denormalize repository status into the PhabricatorRepository // table so we can do this filtering in the database. foreach ($repositories as $key => $repo) { $status = $this->status; switch ($status) { case self::STATUS_OPEN: if (!$repo->isTracked()) { unset($repositories[$key]); } break; case self::STATUS_CLOSED: if ($repo->isTracked()) { unset($repositories[$key]); } break; case self::STATUS_ALL: break; default: throw new Exception("Unknown status '{$status}'!"); } // TODO: This should also be denormalized. $hosted = $this->hosted; switch ($hosted) { case self::HOSTED_PHABRICATOR: if (!$repo->isHosted()) { unset($repositories[$key]); } break; case self::HOSTED_REMOTE: if ($repo->isHosted()) { unset($repositories[$key]); } break; case self::HOSTED_ALL: break; default: throw new Exception("Uknown hosted failed '${hosted}'!"); } } // TODO: Denormalize this, too. if ($this->remoteURIs) { $try_uris = $this->getNormalizedPaths(); $try_uris = array_fuse($try_uris); foreach ($repositories as $key => $repository) { if (!isset($try_uris[$repository->getNormalizedPath()])) { unset($repositories[$key]); } } } // Build the identifierMap if ($this->numericIdentifiers) { foreach ($this->numericIdentifiers as $id) { if (isset($repositories[$id])) { $this->identifierMap[$id] = $repositories[$id]; } } } if ($this->callsignIdentifiers) { $repository_callsigns = mpull($repositories, null, 'getCallsign'); foreach ($this->callsignIdentifiers as $callsign) { if (isset($repository_callsigns[$callsign])) { $this->identifierMap[$callsign] = $repository_callsigns[$callsign]; } } } if ($this->phidIdentifiers) { $repository_phids = mpull($repositories, null, 'getPHID'); foreach ($this->phidIdentifiers as $phid) { if (isset($repository_phids[$phid])) { $this->identifierMap[$phid] = $repository_phids[$phid]; } } } return $repositories; } protected function didFilterPage(array $repositories) { if ($this->needProjectPHIDs) { $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($repositories, 'getPHID')) ->withEdgeTypes(array($type_project)); $edge_query->execute(); foreach ($repositories as $repository) { $project_phids = $edge_query->getDestinationPHIDs( array( $repository->getPHID(), )); $repository->attachProjectPHIDs($project_phids); } } return $repositories; } - protected function buildCustomOrderClause(AphrontDatabaseConnection $conn) { - $parts = array(); - - $order = $this->order; - switch ($order) { - case self::ORDER_CREATED: - break; - case self::ORDER_COMMITTED: - $parts[] = array( - 'table' => 's', - 'column' => 'epoch', - ); - break; - case self::ORDER_CALLSIGN: - $parts[] = array( - 'table' => 'r', - 'column' => 'callsign', - 'reverse' => true, - ); - break; - case self::ORDER_NAME: - $parts[] = array( - 'table' => 'r', - 'column' => 'name', - 'reverse' => true, - ); - break; - case self::ORDER_SIZE: - $parts[] = array( - 'table' => 's', - 'column' => 'size', - ); - break; - default: - throw new Exception("Unknown order '{$order}!'"); - } + public function getPrimaryTableAlias() { + return 'r'; + } - $parts[] = array( - 'table' => 'r', - 'column' => 'id', + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'committed' => array( + 'table' => 's', + 'column' => 'epoch', + 'type' => 'int', + 'null' => 'tail', + ), + 'callsign' => array( + 'table' => 'r', + 'column' => 'callsign', + 'type' => 'string', + 'unique' => true, + 'reverse' => true, + ), + 'name' => array( + 'table' => 'r', + 'column' => 'name', + 'type' => 'string', + 'reverse' => true, + ), + 'size' => array( + 'table' => 's', + 'column' => 'size', + 'type' => 'int', + 'null' => 'tail', + ), ); - - return $this->formatOrderClause($conn, $parts); } - protected function loadCursorObject($id) { - $query = id(new PhabricatorRepositoryQuery()) - ->setViewer($this->getPagingViewer()) - ->withIDs(array((int)$id)); + protected function willExecuteCursorQuery( + PhabricatorCursorPagedPolicyAwareQuery $query) { + $vector = $this->getOrderVector(); - if ($this->order == self::ORDER_COMMITTED) { + if ($vector->containsKey('committed')) { $query->needMostRecentCommits(true); } - if ($this->order == self::ORDER_SIZE) { + if ($vector->containsKey('size')) { $query->needCommitCounts(true); } - - $results = $query->execute(); - return head($results); } - protected function buildPagingClause(AphrontDatabaseConnection $conn_r) { - $default = parent::buildPagingClause($conn_r); + protected function getPagingValueMap($cursor, array $keys) { + $repository = $this->loadCursorObject($cursor); - $before_id = $this->getBeforeID(); - $after_id = $this->getAfterID(); - - if (!$before_id && !$after_id) { - return $default; - } - - $order = $this->order; - if ($order == self::ORDER_CREATED) { - return $default; - } - - if ($before_id) { - $cursor = $this->loadCursorObject($before_id); - } else { - $cursor = $this->loadCursorObject($after_id); - } - - if (!$cursor) { - return null; - } - - $id_column = array( - 'table' => 'r', - 'column' => 'id', - 'type' => 'int', - 'value' => $cursor->getID(), + $map = array( + 'id' => $repository->getID(), + 'callsign' => $repository->getCallsign(), + 'name' => $repository->getName(), ); - $columns = array(); - switch ($order) { - case self::ORDER_COMMITTED: - $commit = $cursor->getMostRecentCommit(); - if (!$commit) { - return null; - } - $columns[] = array( - 'table' => 's', - 'column' => 'epoch', - 'type' => 'int', - 'value' => $commit->getEpoch(), - ); - $columns[] = $id_column; - break; - case self::ORDER_CALLSIGN: - $columns[] = array( - 'table' => 'r', - 'column' => 'callsign', - 'type' => 'string', - 'value' => $cursor->getCallsign(), - 'reverse' => true, - ); - break; - case self::ORDER_NAME: - $columns[] = array( - 'table' => 'r', - 'column' => 'name', - 'type' => 'string', - 'value' => $cursor->getName(), - 'reverse' => true, - ); - $columns[] = $id_column; - break; - case self::ORDER_SIZE: - $columns[] = array( - 'table' => 's', - 'column' => 'size', - 'type' => 'int', - 'value' => $cursor->getCommitCount(), - ); - $columns[] = $id_column; - break; - default: - throw new Exception("Unknown order '{$order}'!"); + foreach ($keys as $key) { + switch ($key) { + case 'committed': + $commit = $repository->getMostRecentCommit(); + if ($commit) { + $map[$key] = $commit->getEpoch(); + } else { + $map[$key] = null; + } + break; + case 'size': + $count = $repository->getCommitCount(); + if ($count) { + $map[$key] = $count; + } else { + $map[$key] = null; + } + break; + } } - return $this->buildPagingClauseFromMultipleColumns( - $conn_r, - $columns, - array( - 'reversed' => ($this->getReversePaging() xor (bool)($before_id)), - )); + return $map; } private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { $joins = array(); $join_summary_table = $this->needCommitCounts || - $this->needMostRecentCommits || - ($this->order == self::ORDER_COMMITTED) || - ($this->order == self::ORDER_SIZE); + $this->needMostRecentCommits; + + $vector = $this->getOrderVector(); + if ($vector->containsKey('committed') || + $vector->containsKey('size')) { + $join_summary_table = true; + } if ($join_summary_table) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T s ON r.id = s.repositoryID', PhabricatorRepository::TABLE_SUMMARY); } if ($this->anyProjectPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN edge e ON e.src = r.phid'); } return implode(' ', $joins); } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'r.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'r.phid IN (%Ls)', $this->phids); } if ($this->callsigns) { $where[] = qsprintf( $conn_r, 'r.callsign IN (%Ls)', $this->callsigns); } if ($this->numericIdentifiers || $this->callsignIdentifiers || $this->phidIdentifiers) { $identifier_clause = array(); if ($this->numericIdentifiers) { $identifier_clause[] = qsprintf( $conn_r, 'r.id IN (%Ld)', $this->numericIdentifiers); } if ($this->callsignIdentifiers) { $identifier_clause[] = qsprintf( $conn_r, 'r.callsign IN (%Ls)', $this->callsignIdentifiers); } if ($this->phidIdentifiers) { $identifier_clause[] = qsprintf( $conn_r, 'r.phid IN (%Ls)', $this->phidIdentifiers); } $where = array('('.implode(' OR ', $identifier_clause).')'); } if ($this->types) { $where[] = qsprintf( $conn_r, 'r.versionControlSystem IN (%Ls)', $this->types); } if ($this->uuids) { $where[] = qsprintf( $conn_r, 'r.uuid IN (%Ls)', $this->uuids); } if (strlen($this->nameContains)) { $where[] = qsprintf( $conn_r, 'name LIKE %~', $this->nameContains); } if ($this->anyProjectPHIDs) { $where[] = qsprintf( $conn_r, 'e.dst IN (%Ls)', $this->anyProjectPHIDs); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } + + public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } private function getNormalizedPaths() { $normalized_uris = array(); // Since we don't know which type of repository this URI is in the general // case, just generate all the normalizations. We could refine this in some // cases: if the query specifies VCS types, or the URI is a git-style URI // or an `svn+ssh` URI, we could deduce how to normalize it. However, this // would be more complicated and it's not clear if it matters in practice. foreach ($this->remoteURIs as $uri) { $normalized_uris[] = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_GIT, $uri); $normalized_uris[] = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_SVN, $uri); $normalized_uris[] = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL, $uri); } return array_unique(mpull($normalized_uris, 'getNormalizedPath')); } } diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index dceaf6c331..848857b8cf 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -1,950 +1,1030 @@ getID(); } protected function getReversePaging() { return false; } protected function nextPage(array $page) { // See getPagingViewer() for a description of this flag. $this->internalPaging = true; if ($this->beforeID) { $this->beforeID = $this->getPagingValue(last($page)); } else { $this->afterID = $this->getPagingValue(last($page)); } } final public function setAfterID($object_id) { $this->afterID = $object_id; return $this; } final protected function getAfterID() { return $this->afterID; } final public function setBeforeID($object_id) { $this->beforeID = $object_id; return $this; } final protected function getBeforeID() { return $this->beforeID; } /** * Get the viewer for making cursor paging queries. * * NOTE: You should ONLY use this viewer to load cursor objects while * building paging queries. * * Cursor paging can happen in two ways. First, the user can request a page * like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we * can fall back to implicit paging if we filter some results out of a * result list because the user can't see them and need to go fetch some more * results to generate a large enough result list. * * In the first case, want to use the viewer's policies to load the object. * This prevents an attacker from figuring out information about an object * they can't see by executing queries like `/stuff/?after=33&order=name`, * which would otherwise give them a hint about the name of the object. * Generally, if a user can't see an object, they can't use it to page. * * In the second case, we need to load the object whether the user can see * it or not, because we need to examine new results. For example, if a user * loads `/stuff/` and we run a query for the first 100 items that they can * see, but the first 100 rows in the database aren't visible, we need to * be able to issue a query for the next 100 results. If we can't load the * cursor object, we'll fail or issue the same query over and over again. * So, generally, internal paging must bypass policy controls. * * This method returns the appropriate viewer, based on the context in which * the paging is occuring. * * @return PhabricatorUser Viewer for executing paging queries. */ final protected function getPagingViewer() { if ($this->internalPaging) { return PhabricatorUser::getOmnipotentUser(); } else { return $this->getViewer(); } } final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) { if ($this->getRawResultLimit()) { return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit()); } else { return ''; } } final protected function didLoadResults(array $results) { if ($this->beforeID) { $results = array_reverse($results, $preserve_keys = true); } return $results; } final public function executeWithCursorPager(AphrontCursorPagerView $pager) { $this->setLimit($pager->getPageSize() + 1); if ($pager->getAfterID()) { $this->setAfterID($pager->getAfterID()); } else if ($pager->getBeforeID()) { $this->setBeforeID($pager->getBeforeID()); } $results = $this->execute(); $sliced_results = $pager->sliceResults($results); if ($sliced_results) { if ($pager->getBeforeID() || (count($results) > $pager->getPageSize())) { $pager->setNextPageID($this->getPagingValue(last($sliced_results))); } if ($pager->getAfterID() || ($pager->getBeforeID() && (count($results) > $pager->getPageSize()))) { $pager->setPrevPageID($this->getPagingValue(head($sliced_results))); } } return $sliced_results; } /** * Return the alias this query uses to identify the primary table. * * Some automatic query constructions may need to be qualified with a table * alias if the query performs joins which make column names ambiguous. If * this is the case, return the alias for the primary table the query * uses; generally the object table which has `id` and `phid` columns. * * @return string Alias for the primary table. */ protected function getPrimaryTableAlias() { return null; } /* -( Paging )------------------------------------------------------------- */ protected function buildPagingClause(AphrontDatabaseConnection $conn) { $orderable = $this->getOrderableColumns(); // TODO: Remove this once subqueries modernize. if (!$orderable) { if ($this->beforeID) { return qsprintf( $conn, '%Q %Q %s', $this->getPagingColumn(), $this->getReversePaging() ? '<' : '>', $this->beforeID); } else if ($this->afterID) { return qsprintf( $conn, '%Q %Q %s', $this->getPagingColumn(), $this->getReversePaging() ? '>' : '<', $this->afterID); } else { return null; } } $vector = $this->getOrderVector(); if ($this->beforeID !== null) { $cursor = $this->beforeID; $reversed = true; } else if ($this->afterID !== null) { $cursor = $this->afterID; $reversed = false; } else { // No paging is being applied to this query so we do not need to // construct a paging clause. return ''; } $keys = array(); foreach ($vector as $order) { $keys[] = $order->getOrderKey(); } $value_map = $this->getPagingValueMap($cursor, $keys); $columns = array(); foreach ($vector as $order) { $key = $order->getOrderKey(); if (!array_key_exists($key, $value_map)) { throw new Exception( pht( 'Query "%s" failed to return a value from getPagingValueMap() '. 'for column "%s".', get_class($this), $key)); } $column = $orderable[$key]; $column['value'] = $value_map[$key]; $columns[] = $column; } return $this->buildPagingClauseFromMultipleColumns( $conn, $columns, array( 'reversed' => $reversed, )); } protected function getPagingValueMap($cursor, array $keys) { // TODO: This is a hack to make this work with existing classes for now. return array( 'id' => $cursor, ); } protected function loadCursorObject($cursor) { $query = newv(get_class($this), array()) ->setViewer($this->getPagingViewer()) ->withIDs(array((int)$cursor)); $this->willExecuteCursorQuery($query); $object = $query->executeOne(); if (!$object) { throw new Exception( pht( 'Cursor "%s" does not identify a valid object.', $cursor)); } return $object; } protected function willExecuteCursorQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { return; } /** * Simplifies the task of constructing a paging clause across multiple * columns. In the general case, this looks like: * * A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c) * * To build a clause, specify the name, type, and value of each column * to include: * * $this->buildPagingClauseFromMultipleColumns( * $conn_r, * array( * array( * 'table' => 't', * 'column' => 'title', * 'type' => 'string', * 'value' => $cursor->getTitle(), * 'reverse' => true, * ), * array( * 'table' => 't', * 'column' => 'id', * 'type' => 'int', * 'value' => $cursor->getID(), * ), * ), * array( * 'reversed' => $is_reversed, * )); * * This method will then return a composable clause for inclusion in WHERE. * * @param AphrontDatabaseConnection Connection query will execute on. * @param list Column description dictionaries. * @param map Additional constuction options. * @return string Query clause. */ final protected function buildPagingClauseFromMultipleColumns( AphrontDatabaseConnection $conn, array $columns, array $options) { foreach ($columns as $column) { PhutilTypeSpec::checkMap( $column, array( 'table' => 'optional string|null', 'column' => 'string', 'value' => 'wild', 'type' => 'string', 'reverse' => 'optional bool', 'unique' => 'optional bool', + 'null' => 'optional string|null', )); } PhutilTypeSpec::checkMap( $options, array( 'reversed' => 'optional bool', )); $is_query_reversed = idx($options, 'reversed', false); $clauses = array(); $accumulated = array(); $last_key = last_key($columns); foreach ($columns as $key => $column) { $type = $column['type']; - switch ($type) { - case 'null': - $value = qsprintf($conn, '%d', ($column['value'] ? 0 : 1)); - break; - case 'int': - $value = qsprintf($conn, '%d', $column['value']); - break; - case 'float': - $value = qsprintf($conn, '%f', $column['value']); - break; - case 'string': - $value = qsprintf($conn, '%s', $column['value']); - break; - default: - throw new Exception("Unknown column type '{$type}'!"); + + $null = idx($column, 'null'); + if ($column['value'] === null) { + if ($null) { + $value = null; + } else { + throw new Exception( + pht( + 'Column "%s" has null value, but does not specify a null '. + 'behavior.', + $key)); + } + } else { + switch ($type) { + case 'int': + $value = qsprintf($conn, '%d', $column['value']); + break; + case 'float': + $value = qsprintf($conn, '%f', $column['value']); + break; + case 'string': + $value = qsprintf($conn, '%s', $column['value']); + break; + default: + throw new Exception( + pht( + 'Column "%s" has unknown column type "%s".', + $column['column'], + $type)); + } } $is_column_reversed = idx($column, 'reverse', false); $reverse = ($is_query_reversed xor $is_column_reversed); $clause = $accumulated; $table_name = idx($column, 'table'); $column_name = $column['column']; if ($table_name !== null) { $field = qsprintf($conn, '%T.%T', $table_name, $column_name); } else { $field = qsprintf($conn, '%T', $column_name); } - if ($type == 'null') { - $field = qsprintf($conn, '(%Q IS NULL)', $field); + $parts = array(); + if ($null) { + $can_page_if_null = ($null === 'head'); + $can_page_if_nonnull = ($null === 'tail'); + + if ($reverse) { + $can_page_if_null = !$can_page_if_null; + $can_page_if_nonnull = !$can_page_if_nonnull; + } + + $subclause = null; + if ($can_page_if_null && $value === null) { + $parts[] = qsprintf( + $conn, + '(%Q IS NOT NULL)', + $field); + } else if ($can_page_if_nonnull && $value !== null) { + $parts[] = qsprintf( + $conn, + '(%Q IS NULL)', + $field); + } + } + + if ($value !== null) { + $parts[] = qsprintf( + $conn, + '%Q %Q %Q', + $field, + $reverse ? '>' : '<', + $value); + } + + if ($parts) { + if (count($parts) > 1) { + $clause[] = '('.implode(') OR (', $parts).')'; + } else { + $clause[] = head($parts); + } } - $clause[] = qsprintf( - $conn, - '%Q %Q %Q', - $field, - $reverse ? '>' : '<', - $value); - $clauses[] = '('.implode(') AND (', $clause).')'; + if ($clause) { + if (count($clause) > 1) { + $clauses[] = '('.implode(') AND (', $clause).')'; + } else { + $clauses[] = head($clause); + } + } - $accumulated[] = qsprintf( - $conn, - '%Q = %Q', - $field, - $value); + if ($value === null) { + $accumulated[] = qsprintf( + $conn, + '%Q IS NULL', + $field); + } else { + $accumulated[] = qsprintf( + $conn, + '%Q = %Q', + $field, + $value); + } } return '('.implode(') OR (', $clauses).')'; } /* -( Result Ordering )---------------------------------------------------- */ /** * @task order */ public function setOrderVector($vector) { $vector = PhabricatorQueryOrderVector::newFromVector($vector); $orderable = $this->getOrderableColumns(); // Make sure that all the components identify valid columns. $unique = array(); foreach ($vector as $order) { $key = $order->getOrderKey(); if (empty($orderable[$key])) { $valid = implode(', ', array_keys($orderable)); throw new Exception( pht( 'This query ("%s") does not support sorting by order key "%s". '. 'Supported orders are: %s.', get_class($this), $key, $valid)); } $unique[$key] = idx($orderable[$key], 'unique', false); } // Make sure that the last column is unique so that this is a strong // ordering which can be used for paging. $last = last($unique); if ($last !== true) { throw new Exception( pht( 'Order vector "%s" is invalid: the last column in an order must '. 'be a column with unique values, but "%s" is not unique.', $vector->getAsString(), last_key($unique))); } // Make sure that other columns are not unique; an ordering like "id, name" // does not make sense because only "id" can ever have an effect. array_pop($unique); foreach ($unique as $key => $is_unique) { if ($is_unique) { throw new Exception( pht( 'Order vector "%s" is invalid: only the last column in an order '. 'may be unique, but "%s" is a unique column and not the last '. 'column in the order.', $vector->getAsString(), $key)); } } $this->orderVector = $vector; return $this; } /** * @task order */ protected function getOrderVector() { if (!$this->orderVector) { $vector = $this->getDefaultOrderVector(); $vector = PhabricatorQueryOrderVector::newFromVector($vector); // We call setOrderVector() here to apply checks to the default vector. // This catches any errors in the implementation. $this->setOrderVector($vector); } return $this->orderVector; } /** * @task order */ protected function getDefaultOrderVector() { return array('id'); } /** * @task order */ public function getOrderableColumns() { // TODO: Remove this once all subclasses move off the old stuff. if ($this->getPagingColumn() !== 'id') { // This class has bad old custom logic around paging, so return nothing // here. This deactivates the new order code. return array(); } return array( 'id' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'id', 'reverse' => false, 'type' => 'int', 'unique' => true, ), ); } /** * @task order */ final protected function buildOrderClause(AphrontDatabaseConnection $conn) { $orderable = $this->getOrderableColumns(); // TODO: Remove this once all subclasses move off the old stuff. We'll // only enter this block for code using older ordering mechanisms. New // code should expose an orderable column list. if (!$orderable) { if ($this->beforeID) { return qsprintf( $conn, 'ORDER BY %Q %Q', $this->getPagingColumn(), $this->getReversePaging() ? 'DESC' : 'ASC'); } else { return qsprintf( $conn, 'ORDER BY %Q %Q', $this->getPagingColumn(), $this->getReversePaging() ? 'ASC' : 'DESC'); } } $vector = $this->getOrderVector(); $parts = array(); foreach ($vector as $order) { $part = $orderable[$order->getOrderKey()]; if ($order->getIsReversed()) { $part['reverse'] = !idx($part, 'reverse', false); } $parts[] = $part; } return $this->formatOrderClause($conn, $parts); } /** * @task order */ protected function formatOrderClause( AphrontDatabaseConnection $conn, array $parts) { $is_query_reversed = false; if ($this->getReversePaging()) { $is_query_reversed = !$is_query_reversed; } if ($this->getBeforeID()) { $is_query_reversed = !$is_query_reversed; } $sql = array(); foreach ($parts as $key => $part) { $is_column_reversed = !empty($part['reverse']); $descending = true; if ($is_query_reversed) { $descending = !$descending; } if ($is_column_reversed) { $descending = !$descending; } $table = idx($part, 'table'); $column = $part['column']; if ($table !== null) { $field = qsprintf($conn, '%T.%T', $table, $column); } else { $field = qsprintf($conn, '%T', $column); } - if (idx($part, 'type') === 'null') { - $field = qsprintf($conn, '(%Q IS NULL)', $field); + $null = idx($part, 'null'); + if ($null) { + switch ($null) { + case 'head': + $null_field = qsprintf($conn, '(%Q IS NULL)', $field); + break; + case 'tail': + $null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field); + break; + default: + throw new Exception( + pht( + 'NULL value "%s" is invalid. Valid values are "head" and '. + '"tail".', + $null)); + } + + if ($descending) { + $sql[] = qsprintf($conn, '%Q DESC', $null_field); + } else { + $sql[] = qsprintf($conn, '%Q ASC', $null_field); + } } if ($descending) { $sql[] = qsprintf($conn, '%Q DESC', $field); } else { $sql[] = qsprintf($conn, '%Q ASC', $field); } } return qsprintf($conn, 'ORDER BY %Q', implode(', ', $sql)); } /* -( Application Search )------------------------------------------------- */ /** * Constrain the query with an ApplicationSearch index, requiring field values * contain at least one of the values in a set. * * This constraint can build the most common types of queries, like: * * - Find users with shirt sizes "X" or "XL". * - Find shoes with size "13". * * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. * @param string|list One or more values to filter by. * @return this * @task appsearch */ public function withApplicationSearchContainsConstraint( PhabricatorCustomFieldIndexStorage $index, $value) { $this->applicationSearchConstraints[] = array( 'type' => $index->getIndexValueType(), 'cond' => '=', 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), 'value' => $value, ); return $this; } /** * Constrain the query with an ApplicationSearch index, requiring values * exist in a given range. * * This constraint is useful for expressing date ranges: * * - Find events between July 1st and July 7th. * * The ends of the range are inclusive, so a `$min` of `3` and a `$max` of * `5` will match fields with values `3`, `4`, or `5`. Providing `null` for * either end of the range will leave that end of the constraint open. * * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. * @param int|null Minimum permissible value, inclusive. * @param int|null Maximum permissible value, inclusive. * @return this * @task appsearch */ public function withApplicationSearchRangeConstraint( PhabricatorCustomFieldIndexStorage $index, $min, $max) { $index_type = $index->getIndexValueType(); if ($index_type != 'int') { throw new Exception( pht( 'Attempting to apply a range constraint to a field with index type '. '"%s", expected type "%s".', $index_type, 'int')); } $this->applicationSearchConstraints[] = array( 'type' => $index->getIndexValueType(), 'cond' => 'range', 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), 'value' => array($min, $max), ); return $this; } /** * Order the results by an ApplicationSearch index. * * @param PhabricatorCustomField Field to which the index belongs. * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. * @param bool True to sort ascending. * @return this * @task appsearch */ public function withApplicationSearchOrder( PhabricatorCustomField $field, PhabricatorCustomFieldIndexStorage $index, $ascending) { $this->applicationSearchOrders[] = array( 'key' => $field->getFieldKey(), 'type' => $index->getIndexValueType(), 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), 'ascending' => $ascending, ); return $this; } /** * Get the name of the query's primary object PHID column, for constructing * JOIN clauses. Normally (and by default) this is just `"phid"`, but it may * be something more exotic. * * See @{method:getPrimaryTableAlias} if the column needs to be qualified with * a table alias. * * @return string Column name. * @task appsearch */ protected function getApplicationSearchObjectPHIDColumn() { if ($this->getPrimaryTableAlias()) { $prefix = $this->getPrimaryTableAlias().'.'; } else { $prefix = ''; } return $prefix.'phid'; } /** * Determine if the JOINs built by ApplicationSearch might cause each primary * object to return multiple result rows. Generally, this means the query * needs an extra GROUP BY clause. * * @return bool True if the query may return multiple rows for each object. * @task appsearch */ protected function getApplicationSearchMayJoinMultipleRows() { foreach ($this->applicationSearchConstraints as $constraint) { $type = $constraint['type']; $value = $constraint['value']; $cond = $constraint['cond']; switch ($cond) { case '=': switch ($type) { case 'string': case 'int': if (count((array)$value) > 1) { return true; } break; default: throw new Exception(pht('Unknown index type "%s"!', $type)); } break; case 'range': // NOTE: It's possible to write a custom field where multiple rows // match a range constraint, but we don't currently ship any in the // upstream and I can't immediately come up with cases where this // would make sense. break; default: throw new Exception(pht('Unknown constraint condition "%s"!', $cond)); } } return false; } /** * Construct a GROUP BY clause appropriate for ApplicationSearch constraints. * * @param AphrontDatabaseConnection Connection executing the query. * @return string Group clause. * @task appsearch */ protected function buildApplicationSearchGroupClause( AphrontDatabaseConnection $conn_r) { if ($this->getApplicationSearchMayJoinMultipleRows()) { return qsprintf( $conn_r, 'GROUP BY %Q', $this->getApplicationSearchObjectPHIDColumn()); } else { return ''; } } /** * Construct a JOIN clause appropriate for applying ApplicationSearch * constraints. * * @param AphrontDatabaseConnection Connection executing the query. * @return string Join clause. * @task appsearch */ protected function buildApplicationSearchJoinClause( AphrontDatabaseConnection $conn_r) { $joins = array(); foreach ($this->applicationSearchConstraints as $key => $constraint) { $table = $constraint['table']; $alias = 'appsearch_'.$key; $index = $constraint['index']; $cond = $constraint['cond']; $phid_column = $this->getApplicationSearchObjectPHIDColumn(); switch ($cond) { case '=': $type = $constraint['type']; switch ($type) { case 'string': $constraint_clause = qsprintf( $conn_r, '%T.indexValue IN (%Ls)', $alias, (array)$constraint['value']); break; case 'int': $constraint_clause = qsprintf( $conn_r, '%T.indexValue IN (%Ld)', $alias, (array)$constraint['value']); break; default: throw new Exception(pht('Unknown index type "%s"!', $type)); } $joins[] = qsprintf( $conn_r, 'JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s AND (%Q)', $table, $alias, $alias, $phid_column, $alias, $index, $constraint_clause); break; case 'range': list($min, $max) = $constraint['value']; if (($min === null) && ($max === null)) { // If there's no actual range constraint, just move on. break; } if ($min === null) { $constraint_clause = qsprintf( $conn_r, '%T.indexValue <= %d', $alias, $max); } else if ($max === null) { $constraint_clause = qsprintf( $conn_r, '%T.indexValue >= %d', $alias, $min); } else { $constraint_clause = qsprintf( $conn_r, '%T.indexValue BETWEEN %d AND %d', $alias, $min, $max); } $joins[] = qsprintf( $conn_r, 'JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s AND (%Q)', $table, $alias, $alias, $phid_column, $alias, $index, $constraint_clause); break; default: throw new Exception(pht('Unknown constraint condition "%s"!', $cond)); } } foreach ($this->applicationSearchOrders as $key => $order) { $table = $order['table']; $alias = 'appsearch_order_'.$key; $index = $order['index']; $phid_column = $this->getApplicationSearchObjectPHIDColumn(); $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s', $table, $alias, $alias, $phid_column, $alias, $index); } return implode(' ', $joins); } protected function buildApplicationSearchOrders( AphrontDatabaseConnection $conn_r, $reverse) { $orders = array(); foreach ($this->applicationSearchOrders as $key => $order) { $alias = 'appsearch_order_'.$key; if ($order['ascending'] xor $reverse) { $orders[] = qsprintf($conn_r, '%T.indexValue ASC', $alias); } else { $orders[] = qsprintf($conn_r, '%T.indexValue DESC', $alias); } } return $orders; } protected function buildApplicationSearchPagination( AphrontDatabaseConnection $conn_r, $cursor) { // We have to get the current field values on the cursor object. $fields = PhabricatorCustomField::getObjectFields( $cursor, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->setViewer($this->getViewer()); $fields->readFieldsFromStorage($cursor); $fields = mpull($fields->getFields(), null, 'getFieldKey'); $columns = array(); foreach ($this->applicationSearchOrders as $key => $order) { $alias = 'appsearch_order_'.$key; $field = idx($fields, $order['key']); $columns[] = array( 'name' => $alias.'.indexValue', 'value' => $field->getValueForStorage(), 'type' => $order['type'], ); } return $columns; } }