mirror of
https://we.phorge.it/source/phorge.git
synced 2025-02-26 13:39:08 +01:00
Summary: Ref T9979. I picked this name long before the advent of modern "Engine" architecture and it ended up being pretty confusing. Rename "SearchEngine" (currently: mysql or elasticsearch, used to store and query fulltext indexes) to "FulltextStorageEngine" to make it more clear what it does and disambituate it from ApplicationSearch, which also has a bunch of stuff called "SearchEngine", "SearchEngineExtension", etc. Test Plan: Grepped for `phabricatorsearchengine`. Reviewers: chad Reviewed By: chad Maniphest Tasks: T9979 Differential Revision: https://secure.phabricator.com/D14843
838 lines
23 KiB
PHP
838 lines
23 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Query tasks by specific criteria. This class uses the higher-performance
|
|
* but less-general Maniphest indexes to satisfy queries.
|
|
*/
|
|
final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
|
|
|
|
private $taskIDs;
|
|
private $taskPHIDs;
|
|
private $authorPHIDs;
|
|
private $ownerPHIDs;
|
|
private $noOwner;
|
|
private $anyOwner;
|
|
private $subscriberPHIDs;
|
|
private $dateCreatedAfter;
|
|
private $dateCreatedBefore;
|
|
private $dateModifiedAfter;
|
|
private $dateModifiedBefore;
|
|
private $subpriorityMin;
|
|
private $subpriorityMax;
|
|
|
|
private $fullTextSearch = '';
|
|
|
|
private $status = 'status-any';
|
|
const STATUS_ANY = 'status-any';
|
|
const STATUS_OPEN = 'status-open';
|
|
const STATUS_CLOSED = 'status-closed';
|
|
const STATUS_RESOLVED = 'status-resolved';
|
|
const STATUS_WONTFIX = 'status-wontfix';
|
|
const STATUS_INVALID = 'status-invalid';
|
|
const STATUS_SPITE = 'status-spite';
|
|
const STATUS_DUPLICATE = 'status-duplicate';
|
|
|
|
private $statuses;
|
|
private $priorities;
|
|
private $subpriorities;
|
|
|
|
private $groupBy = 'group-none';
|
|
const GROUP_NONE = 'group-none';
|
|
const GROUP_PRIORITY = 'group-priority';
|
|
const GROUP_OWNER = 'group-owner';
|
|
const GROUP_STATUS = 'group-status';
|
|
const GROUP_PROJECT = 'group-project';
|
|
|
|
const ORDER_PRIORITY = 'order-priority';
|
|
const ORDER_CREATED = 'order-created';
|
|
const ORDER_MODIFIED = 'order-modified';
|
|
const ORDER_TITLE = 'order-title';
|
|
|
|
private $needSubscriberPHIDs;
|
|
private $needProjectPHIDs;
|
|
private $blockingTasks;
|
|
private $blockedTasks;
|
|
|
|
public function withAuthors(array $authors) {
|
|
$this->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) {
|
|
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
|
|
$any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;
|
|
|
|
foreach ($owners as $k => $phid) {
|
|
if ($phid === $no_owner || $phid === null) {
|
|
$this->noOwner = true;
|
|
unset($owners[$k]);
|
|
break;
|
|
}
|
|
if ($phid === $any_owner) {
|
|
$this->anyOwner = true;
|
|
unset($owners[$k]);
|
|
break;
|
|
}
|
|
}
|
|
$this->ownerPHIDs = $owners;
|
|
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 withSubpriorityBetween($min, $max) {
|
|
$this->subpriorityMin = $min;
|
|
$this->subpriorityMax = $max;
|
|
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;
|
|
|
|
switch ($this->groupBy) {
|
|
case self::GROUP_NONE:
|
|
$vector = array();
|
|
break;
|
|
case self::GROUP_PRIORITY:
|
|
$vector = array('priority');
|
|
break;
|
|
case self::GROUP_OWNER:
|
|
$vector = array('owner');
|
|
break;
|
|
case self::GROUP_STATUS:
|
|
$vector = array('status');
|
|
break;
|
|
case self::GROUP_PROJECT:
|
|
$vector = array('project');
|
|
break;
|
|
}
|
|
|
|
$this->setGroupVector($vector);
|
|
|
|
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;
|
|
}
|
|
|
|
public function newResultObject() {
|
|
return new ManiphestTask();
|
|
}
|
|
|
|
protected function loadPage() {
|
|
$task_dao = new ManiphestTask();
|
|
$conn = $task_dao->establishConnection('r');
|
|
|
|
$where = $this->buildWhereClause($conn);
|
|
|
|
$group_column = '';
|
|
switch ($this->groupBy) {
|
|
case self::GROUP_PROJECT:
|
|
$group_column = qsprintf(
|
|
$conn,
|
|
', projectGroupName.indexedObjectPHID projectGroupPHID');
|
|
break;
|
|
}
|
|
|
|
$rows = queryfx_all(
|
|
$conn,
|
|
'%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
|
|
$this->buildSelectClause($conn),
|
|
$group_column,
|
|
$task_dao->getTableName(),
|
|
$this->buildJoinClause($conn),
|
|
$where,
|
|
$this->buildGroupClause($conn),
|
|
$this->buildHavingClause($conn),
|
|
$this->buildOrderClause($conn),
|
|
$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;
|
|
}
|
|
|
|
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
|
$where = parent::buildWhereClauseParts($conn);
|
|
|
|
$where[] = $this->buildStatusWhereClause($conn);
|
|
$where[] = $this->buildDependenciesWhereClause($conn);
|
|
$where[] = $this->buildOwnerWhereClause($conn);
|
|
$where[] = $this->buildFullTextWhereClause($conn);
|
|
|
|
if ($this->taskIDs !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'task.id in (%Ld)',
|
|
$this->taskIDs);
|
|
}
|
|
|
|
if ($this->taskPHIDs !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'task.phid in (%Ls)',
|
|
$this->taskPHIDs);
|
|
}
|
|
|
|
if ($this->statuses !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'task.status IN (%Ls)',
|
|
$this->statuses);
|
|
}
|
|
|
|
if ($this->authorPHIDs !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'task.authorPHID in (%Ls)',
|
|
$this->authorPHIDs);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if ($this->priorities !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'task.priority IN (%Ld)',
|
|
$this->priorities);
|
|
}
|
|
|
|
if ($this->subpriorities !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'task.subpriority IN (%Lf)',
|
|
$this->subpriorities);
|
|
}
|
|
|
|
if ($this->subpriorityMin !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'task.subpriority >= %f',
|
|
$this->subpriorityMin);
|
|
}
|
|
|
|
if ($this->subpriorityMax !== null) {
|
|
$where[] = qsprintf(
|
|
$conn,
|
|
'task.subpriority <= %f',
|
|
$this->subpriorityMax);
|
|
}
|
|
|
|
return $where;
|
|
}
|
|
|
|
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(pht("Unknown status query '%s'!", $this->status));
|
|
}
|
|
return qsprintf(
|
|
$conn,
|
|
'task.status = %s',
|
|
$constant);
|
|
}
|
|
}
|
|
|
|
private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
|
|
$subclause = array();
|
|
|
|
if ($this->noOwner) {
|
|
$subclause[] = qsprintf(
|
|
$conn,
|
|
'task.ownerPHID IS NULL');
|
|
}
|
|
|
|
if ($this->anyOwner) {
|
|
$subclause[] = qsprintf(
|
|
$conn,
|
|
'task.ownerPHID IS NOT NULL');
|
|
}
|
|
|
|
if ($this->ownerPHIDs) {
|
|
$subclause[] = qsprintf(
|
|
$conn,
|
|
'task.ownerPHID IN (%Ls)',
|
|
$this->ownerPHIDs);
|
|
}
|
|
|
|
if (!$subclause) {
|
|
return '';
|
|
}
|
|
|
|
return '('.implode(') OR (', $subclause).')';
|
|
}
|
|
|
|
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('types',
|
|
array(ManiphestTaskPHIDType::TYPECONST));
|
|
|
|
$engine = PhabricatorFulltextStorageEngine::loadEngine();
|
|
$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).')';
|
|
}
|
|
|
|
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
|
|
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
|
|
|
|
$joins = array();
|
|
|
|
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->subscriberPHIDs !== null) {
|
|
$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[] = parent::buildJoinClauseParts($conn_r);
|
|
|
|
return $joins;
|
|
}
|
|
|
|
protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
|
|
$joined_multiple_rows = $this->shouldJoinBlockingTasks() ||
|
|
$this->shouldJoinBlockedTasks() ||
|
|
($this->shouldGroupQueryResultRows());
|
|
|
|
$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<phid> Project PHIDs which should be ignored in query
|
|
* construction.
|
|
*/
|
|
private function getIgnoreGroupedProjectPHIDs() {
|
|
// Maybe we should also exclude the "OPERATOR_NOT" PHIDs? 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.
|
|
|
|
$edge_types = array(
|
|
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
|
);
|
|
|
|
$phids = array();
|
|
|
|
$phids[] = $this->getEdgeLogicValues(
|
|
$edge_types,
|
|
array(
|
|
PhabricatorQueryConstraint::OPERATOR_AND,
|
|
));
|
|
|
|
$any = $this->getEdgeLogicValues(
|
|
$edge_types,
|
|
array(
|
|
PhabricatorQueryConstraint::OPERATOR_OR,
|
|
));
|
|
if (count($any) == 1) {
|
|
$phids[] = $any;
|
|
}
|
|
|
|
return array_mergev($phids);
|
|
}
|
|
|
|
protected function getResultCursor($result) {
|
|
$id = $result->getID();
|
|
|
|
if ($this->groupBy == self::GROUP_PROJECT) {
|
|
return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
|
|
}
|
|
|
|
return $id;
|
|
}
|
|
|
|
public function getBuiltinOrders() {
|
|
$orders = array(
|
|
'priority' => array(
|
|
'vector' => array('priority', 'subpriority', 'id'),
|
|
'name' => pht('Priority'),
|
|
'aliases' => array(self::ORDER_PRIORITY),
|
|
),
|
|
'updated' => array(
|
|
'vector' => array('updated', 'id'),
|
|
'name' => pht('Date Updated (Latest First)'),
|
|
'aliases' => array(self::ORDER_MODIFIED),
|
|
),
|
|
'outdated' => array(
|
|
'vector' => array('-updated', '-id'),
|
|
'name' => pht('Date Updated (Oldest First)'),
|
|
),
|
|
'title' => array(
|
|
'vector' => array('title', 'id'),
|
|
'name' => pht('Title'),
|
|
'aliases' => array(self::ORDER_TITLE),
|
|
),
|
|
) + parent::getBuiltinOrders();
|
|
|
|
// Alias the "newest" builtin to the historical key for it.
|
|
$orders['newest']['aliases'][] = self::ORDER_CREATED;
|
|
|
|
$orders = array_select_keys(
|
|
$orders,
|
|
array(
|
|
'priority',
|
|
'updated',
|
|
'outdated',
|
|
'newest',
|
|
'oldest',
|
|
'title',
|
|
)) + $orders;
|
|
|
|
return $orders;
|
|
}
|
|
|
|
public function getOrderableColumns() {
|
|
return parent::getOrderableColumns() + array(
|
|
'priority' => array(
|
|
'table' => 'task',
|
|
'column' => 'priority',
|
|
'type' => 'int',
|
|
),
|
|
'owner' => array(
|
|
'table' => 'task',
|
|
'column' => 'ownerOrdering',
|
|
'null' => 'head',
|
|
'reverse' => true,
|
|
'type' => 'string',
|
|
),
|
|
'status' => array(
|
|
'table' => 'task',
|
|
'column' => 'status',
|
|
'type' => 'string',
|
|
'reverse' => true,
|
|
),
|
|
'project' => array(
|
|
'table' => 'projectGroupName',
|
|
'column' => 'indexedObjectName',
|
|
'type' => 'string',
|
|
'null' => 'head',
|
|
'reverse' => true,
|
|
),
|
|
'title' => array(
|
|
'table' => 'task',
|
|
'column' => 'title',
|
|
'type' => 'string',
|
|
'reverse' => true,
|
|
),
|
|
'subpriority' => array(
|
|
'table' => 'task',
|
|
'column' => 'subpriority',
|
|
'type' => 'float',
|
|
),
|
|
'updated' => array(
|
|
'table' => 'task',
|
|
'column' => 'dateModified',
|
|
'type' => 'int',
|
|
),
|
|
);
|
|
}
|
|
|
|
protected function getPagingValueMap($cursor, array $keys) {
|
|
$cursor_parts = explode('.', $cursor, 2);
|
|
$task_id = $cursor_parts[0];
|
|
$group_id = idx($cursor_parts, 1);
|
|
|
|
$task = $this->loadCursorObject($task_id);
|
|
|
|
$map = array(
|
|
'id' => $task->getID(),
|
|
'priority' => $task->getPriority(),
|
|
'subpriority' => $task->getSubpriority(),
|
|
'owner' => $task->getOwnerOrdering(),
|
|
'status' => $task->getStatus(),
|
|
'title' => $task->getTitle(),
|
|
'updated' => $task->getDateModified(),
|
|
);
|
|
|
|
foreach ($keys as $key) {
|
|
switch ($key) {
|
|
case 'project':
|
|
$value = null;
|
|
if ($group_id) {
|
|
$paging_projects = id(new PhabricatorProjectQuery())
|
|
->setViewer($this->getViewer())
|
|
->withPHIDs(array($group_id))
|
|
->execute();
|
|
if ($paging_projects) {
|
|
$value = head($paging_projects)->getName();
|
|
}
|
|
}
|
|
$map[$key] = $value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach ($keys as $key) {
|
|
if ($this->isCustomFieldOrderKey($key)) {
|
|
$map += $this->getPagingValueMapForCustomFields($task);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
protected function getPrimaryTableAlias() {
|
|
return 'task';
|
|
}
|
|
|
|
public function getQueryApplicationClass() {
|
|
return 'PhabricatorManiphestApplication';
|
|
}
|
|
|
|
}
|