Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php
index 389852c000..2a0ae4a8e9 100644
--- a/src/applications/repository/query/PhabricatorRepositoryQuery.php
+++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php
@@ -1,548 +1,575 @@
<?php
final class PhabricatorRepositoryQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $callsigns;
private $types;
private $uuids;
private $nameContains;
private $remoteURIs;
private $anyProjectPHIDs;
private $numericIdentifiers;
private $callsignIdentifiers;
private $phidIdentifiers;
private $identifierMap;
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ALL = 'status-all';
private $status = self::STATUS_ALL;
const ORDER_CREATED = 'order-created';
const ORDER_COMMITTED = 'order-committed';
const ORDER_CALLSIGN = 'order-callsign';
const ORDER_NAME = 'order-name';
+ const ORDER_SIZE = 'order-size';
private $order = self::ORDER_CREATED;
const HOSTED_PHABRICATOR = 'hosted-phab';
const HOSTED_REMOTE = 'hosted-remote';
const HOSTED_ALL = 'hosted-all';
private $hosted = self::HOSTED_ALL;
private $needMostRecentCommits;
private $needCommitCounts;
private $needProjectPHIDs;
public function withIDs(array $ids) {
$this->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;
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 loadPage() {
$table = new PhabricatorRepository();
$conn_r = $table->establishConnection('r');
if ($this->identifierMap === null) {
$this->identifierMap = array();
}
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T r %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinsClause($conn_r),
$this->buildWhereClause($conn_r),
- $this->buildOrderClause($conn_r),
+ $this->buildCustomOrderClause($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 getReversePaging() {
- switch ($this->order) {
- case self::ORDER_CALLSIGN:
- case self::ORDER_NAME:
- return true;
- }
- return false;
- }
+ protected function buildCustomOrderClause(AphrontDatabaseConnection $conn) {
+ $parts = array();
- protected function getPagingColumn() {
$order = $this->order;
switch ($order) {
case self::ORDER_CREATED:
- return 'r.id';
+ break;
case self::ORDER_COMMITTED:
- return 's.epoch';
+ $parts[] = array(
+ 'name' => 's.epoch',
+ );
+ break;
case self::ORDER_CALLSIGN:
- return 'r.callsign';
+ $parts[] = array(
+ 'name' => 'r.callsign',
+ 'reverse' => true,
+ );
+ break;
case self::ORDER_NAME:
- return 'r.name';
+ $parts[] = array(
+ 'name' => 'r.name',
+ 'reverse' => true,
+ );
+ break;
+ case self::ORDER_SIZE:
+ $parts[] = array(
+ 'name' => 's.size',
+ );
+ break;
default:
throw new Exception("Unknown order '{$order}!'");
}
+
+ $parts[] = array(
+ 'name' => 'r.id',
+ );
+
+ return $this->formatOrderClause($conn, $parts);
}
private function loadCursorObject($id) {
$query = id(new PhabricatorRepositoryQuery())
->setViewer($this->getPagingViewer())
->withIDs(array((int)$id));
if ($this->order == self::ORDER_COMMITTED) {
$query->needMostRecentCommits(true);
}
+ if ($this->order == self::ORDER_SIZE) {
+ $query->needCommitCounts(true);
+ }
+
$results = $query->execute();
return head($results);
}
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;
}
$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(
'name' => 'r.id',
'type' => 'int',
'value' => $cursor->getID(),
);
$columns = array();
switch ($order) {
case self::ORDER_COMMITTED:
$commit = $cursor->getMostRecentCommit();
if (!$commit) {
return null;
}
$columns[] = array(
'name' => 's.epoch',
'type' => 'int',
'value' => $commit->getEpoch(),
);
$columns[] = $id_column;
break;
case self::ORDER_CALLSIGN:
$columns[] = array(
'name' => 'r.callsign',
'type' => 'string',
'value' => $cursor->getCallsign(),
'reverse' => true,
);
break;
case self::ORDER_NAME:
$columns[] = array(
'name' => 'r.name',
'type' => 'string',
'value' => $cursor->getName(),
'reverse' => true,
);
$columns[] = $id_column;
break;
+ case self::ORDER_SIZE:
+ $columns[] = array(
+ 'name' => 's.size',
+ 'type' => 'int',
+ 'value' => $cursor->getCommitCount(),
+ );
+ $columns[] = $id_column;
+ break;
default:
throw new Exception("Unknown order '{$order}'!");
}
return $this->buildPagingClauseFromMultipleColumns(
$conn_r,
$columns,
array(
- // TODO: Clean up the column ordering stuff and then make this
- // depend on getReversePaging().
- 'reversed' => (bool)($before_id),
+ 'reversed' => ($this->getReversePaging() xor (bool)($before_id)),
));
}
private function buildJoinsClause(AphrontDatabaseConnection $conn_r) {
$joins = array();
$join_summary_table = $this->needCommitCounts ||
$this->needMostRecentCommits ||
- ($this->order == self::ORDER_COMMITTED);
+ ($this->order == self::ORDER_COMMITTED) ||
+ ($this->order == self::ORDER_SIZE);
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/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
index 6a2a2458f2..fe7736fea2 100644
--- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
+++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php
@@ -1,305 +1,307 @@
<?php
final class PhabricatorRepositorySearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Repositories');
}
public function getApplicationClassName() {
return 'PhabricatorDiffusionApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter('callsigns', $request->getStrList('callsigns'));
$saved->setParameter('status', $request->getStr('status'));
$saved->setParameter('order', $request->getStr('order'));
$saved->setParameter('hosted', $request->getStr('hosted'));
$saved->setParameter('types', $request->getArr('types'));
$saved->setParameter('name', $request->getStr('name'));
$saved->setParameter('anyProjectPHIDs', $request->getArr('anyProjects'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new PhabricatorRepositoryQuery())
->needProjectPHIDs(true)
->needCommitCounts(true)
->needMostRecentCommits(true);
$callsigns = $saved->getParameter('callsigns');
if ($callsigns) {
$query->withCallsigns($callsigns);
}
$status = $saved->getParameter('status');
$status = idx($this->getStatusValues(), $status);
if ($status) {
$query->withStatus($status);
}
$order = $saved->getParameter('order');
$order = idx($this->getOrderValues(), $order);
if ($order) {
$query->setOrder($order);
} else {
$query->setOrder(head($this->getOrderValues()));
}
$hosted = $saved->getParameter('hosted');
$hosted = idx($this->getHostedValues(), $hosted);
if ($hosted) {
$query->withHosted($hosted);
}
$types = $saved->getParameter('types');
if ($types) {
$query->withTypes($types);
}
$name = $saved->getParameter('name');
if (strlen($name)) {
$query->withNameContains($name);
}
$any_project_phids = $saved->getParameter('anyProjectPHIDs');
if ($any_project_phids) {
$query->withAnyProjects($any_project_phids);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query) {
$callsigns = $saved_query->getParameter('callsigns', array());
$types = $saved_query->getParameter('types', array());
$types = array_fuse($types);
$name = $saved_query->getParameter('name');
$any_project_phids = $saved_query->getParameter('anyProjectPHIDs', array());
if ($any_project_phids) {
$any_project_handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->withPHIDs($any_project_phids)
->execute();
} else {
$any_project_handles = array();
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('callsigns')
->setLabel(pht('Callsigns'))
->setValue(implode(', ', $callsigns)))
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name Contains'))
->setValue($name))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectDatasource())
->setName('anyProjects')
->setLabel(pht('In Any Project'))
->setValue($any_project_handles))
->appendChild(
id(new AphrontFormSelectControl())
->setName('status')
->setLabel(pht('Status'))
->setValue($saved_query->getParameter('status'))
->setOptions($this->getStatusOptions()))
->appendChild(
id(new AphrontFormSelectControl())
->setName('hosted')
->setLabel(pht('Hosted'))
->setValue($saved_query->getParameter('hosted'))
->setOptions($this->getHostedOptions()));
$type_control = id(new AphrontFormCheckboxControl())
->setLabel(pht('Types'));
$all_types = PhabricatorRepositoryType::getAllRepositoryTypes();
foreach ($all_types as $key => $name) {
$type_control->addCheckbox(
'types[]',
$key,
$name,
isset($types[$key]));
}
$form
->appendChild($type_control)
->appendChild(
id(new AphrontFormSelectControl())
->setName('order')
->setLabel(pht('Order'))
->setValue($saved_query->getParameter('order'))
->setOptions($this->getOrderOptions()));
}
protected function getURI($path) {
return '/diffusion/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'active' => pht('Active Repositories'),
'all' => pht('All Repositories'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'active':
return $query->setParameter('status', 'open');
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getStatusOptions() {
return array(
'' => pht('Active and Inactive Repositories'),
'open' => pht('Active Repositories'),
'closed' => pht('Inactive Repositories'),
);
}
private function getStatusValues() {
return array(
'' => PhabricatorRepositoryQuery::STATUS_ALL,
'open' => PhabricatorRepositoryQuery::STATUS_OPEN,
'closed' => PhabricatorRepositoryQuery::STATUS_CLOSED,
);
}
private function getOrderOptions() {
return array(
'committed' => pht('Most Recent Commit'),
'name' => pht('Name'),
'callsign' => pht('Callsign'),
'created' => pht('Date Created'),
+ 'size' => pht('Commit Count'),
);
}
private function getOrderValues() {
return array(
'committed' => PhabricatorRepositoryQuery::ORDER_COMMITTED,
'name' => PhabricatorRepositoryQuery::ORDER_NAME,
'callsign' => PhabricatorRepositoryQuery::ORDER_CALLSIGN,
'created' => PhabricatorRepositoryQuery::ORDER_CREATED,
+ 'size' => PhabricatorRepositoryQuery::ORDER_SIZE,
);
}
private function getHostedOptions() {
return array(
'' => pht('Hosted and Remote Repositories'),
'phabricator' => pht('Hosted Repositories'),
'remote' => pht('Remote Repositories'),
);
}
private function getHostedValues() {
return array(
'' => PhabricatorRepositoryQuery::HOSTED_ALL,
'phabricator' => PhabricatorRepositoryQuery::HOSTED_PHABRICATOR,
'remote' => PhabricatorRepositoryQuery::HOSTED_REMOTE,
);
}
protected function getRequiredHandlePHIDsForResultList(
array $repositories,
PhabricatorSavedQuery $query) {
return array_mergev(mpull($repositories, 'getProjectPHIDs'));
}
protected function renderResultList(
array $repositories,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($repositories, 'PhabricatorRepository');
$viewer = $this->requireViewer();;
$list = new PHUIObjectItemListView();
foreach ($repositories as $repository) {
$id = $repository->getID();
$item = id(new PHUIObjectItemView())
->setUser($viewer)
->setHeader($repository->getName())
->setObjectName('r'.$repository->getCallsign())
->setHref($this->getApplicationURI($repository->getCallsign().'/'));
$commit = $repository->getMostRecentCommit();
if ($commit) {
$commit_link = DiffusionView::linkCommit(
$repository,
$commit->getCommitIdentifier(),
$commit->getSummary());
$item->setSubhead($commit_link);
$item->setEpoch($commit->getEpoch());
}
$item->addIcon(
'none',
PhabricatorRepositoryType::getNameForRepositoryType(
$repository->getVersionControlSystem()));
$size = $repository->getCommitCount();
if ($size) {
$history_uri = DiffusionRequest::generateDiffusionURI(
array(
'callsign' => $repository->getCallsign(),
'action' => 'history',
));
$item->addAttribute(
phutil_tag(
'a',
array(
'href' => $history_uri,
),
pht('%s Commit(s)', new PhutilNumber($size))));
} else {
$item->addAttribute(pht('No Commits'));
}
$project_handles = array_select_keys(
$handles,
$repository->getProjectPHIDs());
if ($project_handles) {
$item->addAttribute(
id(new PHUIHandleTagListView())
->setSlim(true)
->setHandles($project_handles));
}
if (!$repository->isTracked()) {
$item->setDisabled(true);
$item->addIcon('disable-grey', pht('Inactive'));
}
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index b314a01bce..18526123a3 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,636 +1,674 @@
<?php
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*
* @task appsearch Integration with ApplicationSearch
*/
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
private $afterID;
private $beforeID;
private $applicationSearchConstraints = array();
private $applicationSearchOrders = array();
private $internalPaging;
protected function getPagingColumn() {
return 'id';
}
protected function getPagingValue($result) {
if (!is_object($result)) {
// This interface can't be typehinted and PHP gets really angry if we
// call a method on a non-object, so add an explicit check here.
throw new Exception(pht('Expected object, got "%s"!', gettype($result)));
}
return $result->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 '';
}
}
protected function buildPagingClause(
AphrontDatabaseConnection $conn_r) {
if ($this->beforeID) {
return qsprintf(
$conn_r,
'%Q %Q %s',
$this->getPagingColumn(),
$this->getReversePaging() ? '<' : '>',
$this->beforeID);
} else if ($this->afterID) {
return qsprintf(
$conn_r,
'%Q %Q %s',
$this->getPagingColumn(),
$this->getReversePaging() ? '>' : '<',
$this->afterID);
}
return null;
}
final protected function buildOrderClause(AphrontDatabaseConnection $conn_r) {
if ($this->beforeID) {
return qsprintf(
$conn_r,
'ORDER BY %Q %Q',
$this->getPagingColumn(),
$this->getReversePaging() ? 'DESC' : 'ASC');
} else {
return qsprintf(
$conn_r,
'ORDER BY %Q %Q',
$this->getPagingColumn(),
$this->getReversePaging() ? 'ASC' : 'DESC');
}
}
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;
}
/**
* 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(
* 'name' => 'title',
* 'type' => 'string',
* 'value' => $cursor->getTitle(),
* 'reverse' => true,
* ),
* array(
* 'name' => '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<map> 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(
'name' => 'string',
'value' => 'wild',
'type' => 'string',
'reverse' => 'optional bool',
));
}
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) {
$name = $column['name'];
$type = $column['type'];
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("Unknown column type '{$type}'!");
}
$is_column_reversed = idx($column, 'reverse', false);
$reverse = ($is_query_reversed xor $is_column_reversed);
$clause = $accumulated;
$clause[] = qsprintf(
$conn,
'%Q %Q %Q',
$name,
$reverse ? '>' : '<',
$value);
$clauses[] = '('.implode(') AND (', $clause).')';
$accumulated[] = qsprintf(
$conn,
'%Q = %Q',
$name,
$value);
}
return '('.implode(') OR (', $clauses).')';
}
+ 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;
+ }
+
+ $name = $part['name'];
+
+ if ($descending) {
+ $sql[] = qsprintf($conn, '%Q DESC', $name);
+ } else {
+ $sql[] = qsprintf($conn, '%Q ASC', $name);
+ }
+ }
+
+ 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<string> 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 if the
* query construction requires a table alias it may be something like
* `"task.phid"`.
*
* @return string Column name.
* @task appsearch
*/
protected function getApplicationSearchObjectPHIDColumn() {
return '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;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 7:04 AM (1 w, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
186457
Default Alt Text
(45 KB)

Event Timeline