1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-02-02 18:08:26 +01:00

(stable) Promote 2019 Week 12

This commit is contained in:
epriestley 2019-03-23 07:41:44 -07:00
commit 7477da3d4f
36 changed files with 735 additions and 387 deletions

View file

@ -3115,7 +3115,7 @@ phutil_register_library_map(array(
'PhabricatorEmojiDatasource' => 'applications/macro/typeahead/PhabricatorEmojiDatasource.php',
'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php',
'PhabricatorEmojiTranslation' => 'infrastructure/internationalization/translation/PhabricatorEmojiTranslation.php',
'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php',
'PhabricatorEmptyQueryException' => 'infrastructure/query/exception/PhabricatorEmptyQueryException.php',
'PhabricatorEnumConfigType' => 'applications/config/type/PhabricatorEnumConfigType.php',
'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php',
'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php',
@ -3392,6 +3392,7 @@ phutil_register_library_map(array(
'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php',
'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php',
'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php',
'PhabricatorInvalidQueryCursorException' => 'infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php',
'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php',
'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php',
@ -4195,6 +4196,7 @@ phutil_register_library_map(array(
'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php',
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php',
'PhabricatorQueryCursor' => 'infrastructure/query/policy/PhabricatorQueryCursor.php',
'PhabricatorQueryIterator' => 'infrastructure/storage/lisk/PhabricatorQueryIterator.php',
'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php',
'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
@ -9354,6 +9356,7 @@ phutil_register_library_map(array(
'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow',
'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorInvalidQueryCursorException' => 'Exception',
'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource',
@ -10294,6 +10297,7 @@ phutil_register_library_map(array(
'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorQuery' => 'Phobject',
'PhabricatorQueryConstraint' => 'Phobject',
'PhabricatorQueryCursor' => 'Phobject',
'PhabricatorQueryIterator' => 'PhutilBufferedIterator',
'PhabricatorQueryOrderItem' => 'Phobject',
'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase',

View file

@ -122,11 +122,10 @@ final class AlmanacDeviceQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$device = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $device->getID(),
'name' => $device->getName(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
);
}

View file

@ -78,6 +78,16 @@ final class AlmanacInterfaceQuery
return $interfaces;
}
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = parent::buildSelectClauseParts($conn);
if ($this->shouldJoinDeviceTable()) {
$select[] = qsprintf($conn, 'device.name');
}
return $select;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
@ -186,15 +196,16 @@ final class AlmanacInterfaceQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$interface = $this->loadCursorObject($cursor);
protected function newPagingMapFromCursorObject(
PhabricatorQueryCursor $cursor,
array $keys) {
$map = array(
'id' => $interface->getID(),
'name' => $interface->getDevice()->getName(),
$interface = $cursor->getObject();
return array(
'id' => (int)$interface->getID(),
'name' => $cursor->getRawRowProperty('device.name'),
);
return $map;
}
}

View file

@ -79,11 +79,10 @@ final class AlmanacNamespaceQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$namespace = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $namespace->getID(),
'name' => $namespace->getName(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
);
}

View file

@ -206,11 +206,10 @@ final class AlmanacServiceQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$service = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $service->getID(),
'name' => $service->getName(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
);
}

View file

@ -108,11 +108,11 @@ final class PhabricatorBadgesQuery
) + parent::getOrderableColumns();
}
protected function getPagingValueMap($cursor, array $keys) {
$badge = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'quality' => $badge->getQuality(),
'id' => $badge->getID(),
'id' => (int)$object->getID(),
'quality' => $object->getQuality(),
);
}

View file

@ -140,11 +140,10 @@ final class PhabricatorCalendarEventQuery
) + parent::getOrderableColumns();
}
protected function getPagingValueMap($cursor, array $keys) {
$event = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'start' => $event->getStartDateTimeEpoch(),
'id' => $event->getID(),
'id' => (int)$object->getID(),
'start' => (int)$object->getStartDateTimeEpoch(),
);
}

View file

@ -97,11 +97,10 @@ final class PhabricatorCountdownQuery
) + parent::getOrderableColumns();
}
protected function getPagingValueMap($cursor, array $keys) {
$countdown = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'epoch' => $countdown->getEpoch(),
'id' => $countdown->getID(),
'id' => (int)$object->getID(),
'epoch' => (int)$object->getEpoch(),
);
}

View file

@ -800,11 +800,10 @@ final class DifferentialRevisionQuery
) + parent::getOrderableColumns();
}
protected function getPagingValueMap($cursor, array $keys) {
$revision = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $revision->getID(),
'updated' => $revision->getDateModified(),
'id' => (int)$object->getID(),
'updated' => (int)$object->getDateModified(),
);
}

View file

@ -924,11 +924,10 @@ final class DiffusionCommitQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$commit = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $commit->getID(),
'epoch' => $commit->getEpoch(),
'id' => (int)$object->getID(),
'epoch' => (int)$object->getEpoch(),
);
}

View file

@ -181,11 +181,10 @@ final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery {
);
}
protected function getPagingValueMap($cursor, array $keys) {
$book = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'name' => $book->getName(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
);
}

View file

@ -147,17 +147,21 @@ final class PhabricatorFeedQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
return array(
'key' => $cursor,
);
protected function applyExternalCursorConstraintsToQuery(
PhabricatorCursorPagedPolicyAwareQuery $subquery,
$cursor) {
$subquery->withChronologicalKeys(array($cursor));
}
protected function getResultCursor($item) {
if ($item instanceof PhabricatorFeedStory) {
return $item->getChronologicalKey();
protected function newExternalCursorStringForResult($object) {
return $object->getChronologicalKey();
}
return $item['chronologicalKey'];
protected function newPagingMapFromPartialObject($object) {
// This query is unusual, and the "object" is a raw result row.
return array(
'key' => $object['chronologicalKey'],
);
}
protected function getPrimaryTableAlias() {

View file

@ -133,11 +133,10 @@ final class HarbormasterBuildPlanQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$plan = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $plan->getID(),
'name' => $plan->getName(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
);
}

View file

@ -249,11 +249,10 @@ final class PhabricatorMacroQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$macro = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $macro->getID(),
'name' => $macro->getName(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
);
}

View file

@ -134,10 +134,7 @@ final class ManiphestTransactionEditor
$parent_xaction->setMetadataValue('blocker.new', true);
}
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource())
$this->newSubEditor()
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, array($parent_xaction));

View file

@ -27,6 +27,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $closedEpochMax;
private $closerPHIDs;
private $columnPHIDs;
private $specificGroupByProjectPHID;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
@ -227,6 +228,11 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this;
}
public function withSpecificGroupByProjectPHID($project_phid) {
$this->specificGroupByProjectPHID = $project_phid;
return $this;
}
public function newResultObject() {
return new ManiphestTask();
}
@ -534,6 +540,13 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
$select_phids);
}
if ($this->specificGroupByProjectPHID !== null) {
$where[] = qsprintf(
$conn,
'projectGroupName.indexedObjectPHID = %s',
$this->specificGroupByProjectPHID);
}
return $where;
}
@ -824,16 +837,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
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(
@ -926,39 +929,37 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
);
}
protected function getPagingValueMap($cursor, array $keys) {
$cursor_parts = explode('.', $cursor, 2);
$task_id = $cursor_parts[0];
$group_id = idx($cursor_parts, 1);
protected function newPagingMapFromCursorObject(
PhabricatorQueryCursor $cursor,
array $keys) {
$task = $this->loadCursorObject($task_id);
$task = $cursor->getObject();
$map = array(
'id' => $task->getID(),
'priority' => $task->getPriority(),
'id' => (int)$task->getID(),
'priority' => (int)$task->getPriority(),
'owner' => $task->getOwnerOrdering(),
'status' => $task->getStatus(),
'title' => $task->getTitle(),
'updated' => $task->getDateModified(),
'updated' => (int)$task->getDateModified(),
'closed' => $task->getClosedEpoch(),
);
foreach ($keys as $key) {
switch ($key) {
case 'project':
if (isset($keys['project'])) {
$value = null;
if ($group_id) {
$group_phid = $task->getGroupByProjectPHID();
if ($group_phid) {
$paging_projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs(array($group_id))
->withPHIDs(array($group_phid))
->execute();
if ($paging_projects) {
$value = head($paging_projects)->getName();
}
}
$map[$key] = $value;
break;
}
$map['project'] = $value;
}
foreach ($keys as $key) {
@ -971,6 +972,77 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $map;
}
protected function newExternalCursorStringForResult($object) {
$id = $object->getID();
if ($this->groupBy == self::GROUP_PROJECT) {
return rtrim($id.'.'.$object->getGroupByProjectPHID(), '.');
}
return $id;
}
protected function newInternalCursorFromExternalCursor($cursor) {
list($task_id, $group_phid) = $this->parseCursor($cursor);
$cursor_object = parent::newInternalCursorFromExternalCursor($cursor);
if ($group_phid !== null) {
$project = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs(array($group_phid))
->execute();
if (!$project) {
$this->throwCursorException(
pht(
'Group PHID ("%s") component of cursor ("%s") is not valid.',
$group_phid,
$cursor));
}
$cursor_object->getObject()->attachGroupByProjectPHID($group_phid);
}
return $cursor_object;
}
protected function applyExternalCursorConstraintsToQuery(
PhabricatorCursorPagedPolicyAwareQuery $subquery,
$cursor) {
list($task_id, $group_phid) = $this->parseCursor($cursor);
$subquery->withIDs(array($task_id));
if ($group_phid) {
$subquery->setGroupBy(self::GROUP_PROJECT);
// The subquery needs to return exactly one result. If a task is in
// several projects, the query may naturally return several results.
// Specify that we want only the particular instance of the task in
// the specified project.
$subquery->withSpecificGroupByProjectPHID($group_phid);
}
}
private function parseCursor($cursor) {
// Split a "123.PHID-PROJ-abcd" cursor into a "Task ID" part and a
// "Project PHID" part.
$parts = explode('.', $cursor, 2);
if (count($parts) < 2) {
$parts[] = null;
}
if (!strlen($parts[1])) {
$parts[1] = null;
}
return $parts;
}
protected function getPrimaryTableAlias() {
return 'task';
}

View file

@ -267,11 +267,10 @@ final class PhabricatorOwnersPackageQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$package = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $package->getID(),
'name' => $package->getName(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
);
}

View file

@ -379,11 +379,10 @@ final class PhabricatorPeopleQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$user = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $user->getID(),
'username' => $user->getUsername(),
'id' => (int)$object->getID(),
'username' => $object->getUsername(),
);
}

View file

@ -171,15 +171,11 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery {
);
}
protected function getPagingValueMap($cursor, array $keys) {
$post = $this->loadCursorObject($cursor);
$map = array(
'datePublished' => $post->getDatePublished(),
'id' => $post->getID(),
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => (int)$object->getID(),
'datePublished' => (int)$object->getDatePublished(),
);
return $map;
}
public function getQueryApplicationClass() {

View file

@ -81,9 +81,9 @@ final class PhluxVariableQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$object = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => (int)$object->getID(),
'key' => $object->getVariableKey(),
);
}

View file

@ -133,12 +133,11 @@ final class PhrequentUserTimeQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$usertime = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $usertime->getID(),
'start' => $usertime->getDateStarted(),
'end' => $usertime->getDateEnded(),
'id' => (int)$object->getID(),
'start' => (int)$object->getDateStarted(),
'end' => (int)$object->getDateEnded(),
);
}

View file

@ -168,10 +168,20 @@ final class PhrictionDocumentQuery
return $documents;
}
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = parent::buildSelectClauseParts($conn);
if ($this->shouldJoinContentTable()) {
$select[] = qsprintf($conn, 'c.title');
}
return $select;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->getOrderVector()->containsKey('updated')) {
if ($this->shouldJoinContentTable()) {
$content_dao = new PhrictionContent();
$joins[] = qsprintf(
$conn,
@ -182,6 +192,14 @@ final class PhrictionDocumentQuery
return $joins;
}
private function shouldJoinContentTable() {
if ($this->getOrderVector()->containsKey('title')) {
return true;
}
return false;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
@ -354,35 +372,25 @@ final class PhrictionDocumentQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$document = $this->loadCursorObject($cursor);
protected function newPagingMapFromCursorObject(
PhabricatorQueryCursor $cursor,
array $keys) {
$document = $cursor->getObject();
$map = array(
'id' => $document->getID(),
'id' => (int)$document->getID(),
'depth' => $document->getDepth(),
'updated' => $document->getEditedEpoch(),
'updated' => (int)$document->getEditedEpoch(),
);
foreach ($keys as $key) {
switch ($key) {
case 'title':
$map[$key] = $document->getContent()->getTitle();
break;
}
if (isset($keys['title'])) {
$map['title'] = $cursor->getRawRowProperty('title');
}
return $map;
}
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
$vector = $this->getOrderVector();
if ($vector->containsKey('title')) {
$query->needContent(true);
}
}
protected function getPrimaryTableAlias() {
return 'd';
}

View file

@ -50,13 +50,6 @@ final class PhabricatorPhurlURLQuery
return $this;
}
protected function getPagingValueMap($cursor, array $keys) {
$url = $this->loadCursorObject($cursor);
return array(
'id' => $url->getID(),
);
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}

View file

@ -201,12 +201,11 @@ final class PhabricatorProjectQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$project = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $project->getID(),
'name' => $project->getName(),
'status' => $project->getStatus(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
'status' => $object->getStatus(),
);
}

View file

@ -130,12 +130,10 @@ final class ReleephProductQuery
);
}
protected function getPagingValueMap($cursor, array $keys) {
$product = $this->loadCursorObject($cursor);
protected function newPagingMapFromPartialObject($object) {
return array(
'id' => $product->getID(),
'name' => $product->getName(),
'id' => (int)$object->getID(),
'name' => $object->getName(),
);
}

View file

@ -442,47 +442,24 @@ final class PhabricatorRepositoryQuery
);
}
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
$vector = $this->getOrderVector();
protected function newPagingMapFromCursorObject(
PhabricatorQueryCursor $cursor,
array $keys) {
if ($vector->containsKey('committed')) {
$query->needMostRecentCommits(true);
}
if ($vector->containsKey('size')) {
$query->needCommitCounts(true);
}
}
protected function getPagingValueMap($cursor, array $keys) {
$repository = $this->loadCursorObject($cursor);
$repository = $cursor->getObject();
$map = array(
'id' => $repository->getID(),
'id' => (int)$repository->getID(),
'callsign' => $repository->getCallsign(),
'name' => $repository->getName(),
);
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;
if (isset($keys['committed'])) {
$map['committed'] = $cursor->getRawRowProperty('epoch');
}
if (isset($keys['size'])) {
$map['size'] = $cursor->getRawRowProperty('size');
}
return $map;
@ -491,8 +468,6 @@ final class PhabricatorRepositoryQuery
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$parts = parent::buildSelectClauseParts($conn);
$parts[] = qsprintf($conn, 'r.*');
if ($this->shouldJoinSummaryTable()) {
$parts[] = qsprintf($conn, 's.*');
}

View file

@ -249,6 +249,8 @@ final class PhabricatorApplicationSearchController
$pager = $engine->newPagerForSavedQuery($saved_query);
$pager->readFromRequest($request);
$query->setReturnPartialResultsOnOverheat(true);
$objects = $engine->executeQuery($query, $pager);
$force_nux = $request->getBool('nux');
@ -349,6 +351,8 @@ final class PhabricatorApplicationSearchController
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorSearchConstraintException $ex) {
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorInvalidQueryCursorException $ex) {
$exec_errors[] = $ex->getMessage();
}
// The engine may have encountered additional errors during rendering;
@ -798,6 +802,7 @@ final class PhabricatorApplicationSearchController
$object = $query
->setViewer(PhabricatorUser::getOmnipotentUser())
->setLimit(1)
->setReturnPartialResultsOnOverheat(true)
->execute();
if ($object) {
return null;

View file

@ -72,7 +72,7 @@ abstract class PhabricatorApplicationTransactionEditor
private $mailShouldSend = false;
private $modularTypes;
private $silent;
private $mustEncrypt;
private $mustEncrypt = array();
private $stampTemplates = array();
private $mailStamps = array();
private $oldTo = array();
@ -90,6 +90,11 @@ abstract class PhabricatorApplicationTransactionEditor
private $cancelURI;
private $extensions;
private $parentEditor;
private $subEditors = array();
private $publishableObject;
private $publishableTransactions;
const STORAGE_ENCODING_BINARY = 'binary';
/**
@ -1272,10 +1277,9 @@ abstract class PhabricatorApplicationTransactionEditor
$herald_source = PhabricatorContentSource::newForSource(
PhabricatorHeraldContentSource::SOURCECONST);
$herald_editor = newv(get_class($this), array())
$herald_editor = $this->newEditorCopy()
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setParentMessageID($this->getParentMessageID())
->setIsHeraldEditor(true)
->setActor($herald_actor)
->setActingAsPHID($herald_phid)
@ -1330,6 +1334,38 @@ abstract class PhabricatorApplicationTransactionEditor
}
$this->heraldHeader = $herald_header;
// See PHI1134. If we're a subeditor, we don't publish information about
// the edit yet. Our parent editor still needs to finish applying
// transactions and execute Herald, which may change the information we
// publish.
// For example, Herald actions may change the parent object's title or
// visibility, or Herald may apply rules like "Must Encrypt" that affect
// email.
// Once the parent finishes work, it will queue its own publish step and
// then queue publish steps for its children.
$this->publishableObject = $object;
$this->publishableTransactions = $xactions;
if (!$this->parentEditor) {
$this->queuePublishing();
}
return $xactions;
}
final private function queuePublishing() {
$object = $this->publishableObject;
$xactions = $this->publishableTransactions;
if (!$object) {
throw new Exception(
pht(
'Editor method "queuePublishing()" was called, but no publishable '.
'object is present. This Editor is not ready to publish.'));
}
// We're going to compute some of the data we'll use to publish these
// transactions here, before queueing a worker.
//
@ -1392,9 +1428,11 @@ abstract class PhabricatorApplicationTransactionEditor
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
$this->flushTransactionQueue($object);
foreach ($this->subEditors as $sub_editor) {
$sub_editor->queuePublishing();
}
return $xactions;
$this->flushTransactionQueue($object);
}
protected function didCatchDuplicateKeyException(
@ -3818,6 +3856,11 @@ abstract class PhabricatorApplicationTransactionEditor
$this->mustEncrypt = $adapter->getMustEncryptReasons();
// See PHI1134. Propagate "Must Encrypt" state to sub-editors.
foreach ($this->subEditors as $sub_editor) {
$sub_editor->mustEncrypt = $this->mustEncrypt;
}
$apply_xactions = $this->didApplyHeraldRules($object, $adapter, $xscript);
assert_instances_of($apply_xactions, 'PhabricatorApplicationTransaction');
@ -4034,15 +4077,10 @@ abstract class PhabricatorApplicationTransactionEditor
->setOldValue($old_phids)
->setNewValue($new_phids);
$editor
$editor = $this->newSubEditor($editor)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setParentMessageID($this->getParentMessageID())
->setIsInverseEdgeEditor(true)
->setIsSilent($this->getIsSilent())
->setActor($this->requireActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource());
->setIsInverseEdgeEditor(true);
$editor->applyTransactions($node, array($template));
}
@ -4551,23 +4589,41 @@ abstract class PhabricatorApplicationTransactionEditor
$xactions = $this->transactionQueue;
$this->transactionQueue = array();
$editor = $this->newQueueEditor();
$editor = $this->newEditorCopy();
return $editor->applyTransactions($object, $xactions);
}
private function newQueueEditor() {
$editor = id(newv(get_class($this), array()))
final protected function newSubEditor(
PhabricatorApplicationTransactionEditor $template = null) {
$editor = $this->newEditorCopy($template);
$editor->parentEditor = $this;
$this->subEditors[] = $editor;
return $editor;
}
private function newEditorCopy(
PhabricatorApplicationTransactionEditor $template = null) {
if ($template === null) {
$template = newv(get_class($this), array());
}
$editor = id(clone $template)
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setContinueOnMissingFields($this->getContinueOnMissingFields())
->setParentMessageID($this->getParentMessageID())
->setIsSilent($this->getIsSilent());
if ($this->actingAsPHID !== null) {
$editor->setActingAsPHID($this->actingAsPHID);
}
$editor->mustEncrypt = $this->mustEncrypt;
return $editor;
}

View file

@ -8,14 +8,18 @@ final class PhabricatorEdgeObject
private $src;
private $dst;
private $type;
private $dateCreated;
private $sequence;
public static function newFromRow(array $row) {
$edge = new self();
$edge->id = $row['id'];
$edge->src = $row['src'];
$edge->dst = $row['dst'];
$edge->type = $row['type'];
$edge->id = idx($row, 'id');
$edge->src = idx($row, 'src');
$edge->dst = idx($row, 'dst');
$edge->type = idx($row, 'type');
$edge->dateCreated = idx($row, 'dateCreated');
$edge->sequence = idx($row, 'seq');
return $edge;
}
@ -40,6 +44,15 @@ final class PhabricatorEdgeObject
return null;
}
public function getDateCreated() {
return $this->dateCreated;
}
public function getSequence() {
return $this->sequence;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -12,7 +12,6 @@ final class PhabricatorEdgeObjectQuery
private $edgeTypes;
private $destinationPHIDs;
public function withSourcePHIDs(array $source_phids) {
$this->sourcePHIDs = $source_phids;
return $this;
@ -85,18 +84,6 @@ final class PhabricatorEdgeObjectQuery
return $result;
}
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$parts = parent::buildSelectClauseParts($conn);
// TODO: This is hacky, because we don't have real IDs on this table.
$parts[] = qsprintf(
$conn,
'CONCAT(dateCreated, %s, seq) AS id',
'_');
return $parts;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$parts = parent::buildWhereClauseParts($conn);
@ -151,13 +138,45 @@ final class PhabricatorEdgeObjectQuery
return array('dateCreated', 'sequence');
}
protected function getPagingValueMap($cursor, array $keys) {
$parts = explode('_', $cursor);
protected function newInternalCursorFromExternalCursor($cursor) {
list($epoch, $sequence) = $this->parseCursor($cursor);
// Instead of actually loading an edge, we're just making a fake edge
// with the properties the cursor describes.
$edge_object = PhabricatorEdgeObject::newFromRow(
array(
'dateCreated' => $epoch,
'seq' => $sequence,
));
return id(new PhabricatorQueryCursor())
->setObject($edge_object);
}
protected function newPagingMapFromPartialObject($object) {
return array(
'dateCreated' => $parts[0],
'sequence' => $parts[1],
'dateCreated' => $object->getDateCreated(),
'sequence' => $object->getSequence(),
);
}
protected function newExternalCursorStringForResult($object) {
return sprintf(
'%d_%d',
$object->getDateCreated(),
$object->getSequence());
}
private function parseCursor($cursor) {
if (!preg_match('/^\d+_\d+\z/', $cursor)) {
$this->throwCursorException(
pht(
'Expected edge cursor in the form "0123_6789", got "%s".',
$cursor));
}
return explode('_', $cursor);
}
}

View file

@ -0,0 +1,4 @@
<?php
final class PhabricatorInvalidQueryCursorException
extends Exception {}

View file

@ -4,6 +4,7 @@
* 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 cursors Query Cursors
* @task clauses Building Query Clauses
* @task appsearch Integration with ApplicationSearch
* @task customfield Integration with CustomField
@ -15,8 +16,11 @@
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
private $afterID;
private $beforeID;
private $externalCursorString;
private $internalCursorObject;
private $isQueryOrderReversed = false;
private $rawCursorRow;
private $applicationSearchConstraints = array();
private $internalPaging;
private $orderVector;
@ -33,54 +37,200 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
private $ferretQuery;
private $ferretMetadata = array();
protected function getPageCursors(array $page) {
const FULLTEXT_RANK = '_ft_rank';
const FULLTEXT_MODIFIED = '_ft_epochModified';
const FULLTEXT_CREATED = '_ft_epochCreated';
/* -( Cursors )------------------------------------------------------------ */
protected function newExternalCursorStringForResult($object) {
if (!($object instanceof LiskDAO)) {
throw new Exception(
pht(
'Expected to be passed a result object of class "LiskDAO" in '.
'"newExternalCursorStringForResult()", actually passed "%s". '.
'Return storage objects from "loadPage()" or override '.
'"newExternalCursorStringForResult()".',
phutil_describe_type($object)));
}
return (string)$object->getID();
}
protected function newInternalCursorFromExternalCursor($cursor) {
$viewer = $this->getViewer();
$query = newv(get_class($this), array());
$query
->setParentQuery($this)
->setViewer($viewer);
// We're copying our order vector to the subquery so that the subquery
// knows it should generate any supplemental information required by the
// ordering.
// For example, Phriction documents may be ordered by title, but the title
// isn't a column in the "document" table: the query must JOIN the
// "content" table to perform the ordering. Passing the ordering to the
// subquery tells it that we need it to do that JOIN and attach relevant
// paging information to the internal cursor object.
// We only expect to load a single result, so the actual result order does
// not matter. We only want the internal cursor for that result to look
// like a cursor this parent query would generate.
$query->setOrderVector($this->getOrderVector());
$this->applyExternalCursorConstraintsToQuery($query, $cursor);
// We're executing the subquery normally to make sure the viewer can
// actually see the object, and that it's a completely valid object which
// passes all filtering and policy checks. You aren't allowed to use an
// object you can't see as a cursor, since this can leak information.
$result = $query->executeOne();
if (!$result) {
$this->throwCursorException(
pht(
'Cursor "%s" does not identify a valid object in query "%s".',
$cursor,
get_class($this)));
}
// Now that we made sure the viewer can actually see the object the
// external cursor identifies, return the internal cursor the query
// generated as a side effect while loading the object.
return $query->getInternalCursorObject();
}
final protected function throwCursorException($message) {
throw new PhabricatorInvalidQueryCursorException($message);
}
protected function applyExternalCursorConstraintsToQuery(
PhabricatorCursorPagedPolicyAwareQuery $subquery,
$cursor) {
$subquery->withIDs(array($cursor));
}
protected function newPagingMapFromCursorObject(
PhabricatorQueryCursor $cursor,
array $keys) {
$object = $cursor->getObject();
return $this->newPagingMapFromPartialObject($object);
}
protected function newPagingMapFromPartialObject($object) {
return array(
$this->getResultCursor(head($page)),
$this->getResultCursor(last($page)),
'id' => (int)$object->getID(),
);
}
protected function getResultCursor($object) {
if (!is_object($object)) {
final private function getExternalCursorStringForResult($object) {
$cursor = $this->newExternalCursorStringForResult($object);
if (!is_string($cursor)) {
throw new Exception(
pht(
'Expected object, got "%s".',
gettype($object)));
'Expected "newExternalCursorStringForResult()" in class "%s" to '.
'return a string, but got "%s".',
get_class($this),
phutil_describe_type($cursor)));
}
return $object->getID();
return $cursor;
}
protected function nextPage(array $page) {
// See getPagingViewer() for a description of this flag.
$this->internalPaging = true;
if ($this->beforeID !== null) {
$page = array_reverse($page, $preserve_keys = true);
list($before, $after) = $this->getPageCursors($page);
$this->beforeID = $before;
} else {
list($before, $after) = $this->getPageCursors($page);
$this->afterID = $after;
}
final private function getExternalCursorString() {
return $this->externalCursorString;
}
final public function setAfterID($object_id) {
$this->afterID = $object_id;
final private function setExternalCursorString($external_cursor) {
$this->externalCursorString = $external_cursor;
return $this;
}
final protected function getAfterID() {
return $this->afterID;
final private function getIsQueryOrderReversed() {
return $this->isQueryOrderReversed;
}
final public function setBeforeID($object_id) {
$this->beforeID = $object_id;
final private function setIsQueryOrderReversed($is_reversed) {
$this->isQueryOrderReversed = $is_reversed;
return $this;
}
final protected function getBeforeID() {
return $this->beforeID;
final private function getInternalCursorObject() {
return $this->internalCursorObject;
}
final private function setInternalCursorObject(
PhabricatorQueryCursor $cursor) {
$this->internalCursorObject = $cursor;
return $this;
}
final private function getInternalCursorFromExternalCursor(
$cursor_string) {
$cursor_object = $this->newInternalCursorFromExternalCursor($cursor_string);
if (!($cursor_object instanceof PhabricatorQueryCursor)) {
throw new Exception(
pht(
'Expected "newInternalCursorFromExternalCursor()" to return an '.
'object of class "PhabricatorQueryCursor", but got "%s" (in '.
'class "%s").',
phutil_describe_type($cursor_object),
get_class($this)));
}
return $cursor_object;
}
final private function getPagingMapFromCursorObject(
PhabricatorQueryCursor $cursor,
array $keys) {
$map = $this->newPagingMapFromCursorObject($cursor, $keys);
if (!is_array($map)) {
throw new Exception(
pht(
'Expected "newPagingMapFromCursorObject()" to return a map of '.
'paging values, but got "%s" (in class "%s").',
phutil_describe_type($map),
get_class($this)));
}
foreach ($keys as $key) {
if (!array_key_exists($key, $map)) {
throw new Exception(
pht(
'Map returned by "newPagingMapFromCursorObject()" in class "%s" '.
'omits required key "%s".',
get_class($this),
$key));
}
}
return $map;
}
final protected function nextPage(array $page) {
if (!$page) {
return;
}
$cursor = id(new PhabricatorQueryCursor())
->setObject(last($page));
if ($this->rawCursorRow) {
$cursor->setRawRow($this->rawCursorRow);
}
$this->setInternalCursorObject($cursor);
}
final public function getFerretMetadata() {
@ -152,56 +302,21 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
$metadata = id(new PhabricatorFerretMetadata())
->setPHID($phid)
->setEngine($this->ferretEngine)
->setRelevance(idx($row, '_ft_rank'));
->setRelevance(idx($row, self::FULLTEXT_RANK));
$this->ferretMetadata[$phid] = $metadata;
unset($row['_ft_rank']);
unset($row[self::FULLTEXT_RANK]);
unset($row[self::FULLTEXT_MODIFIED]);
unset($row[self::FULLTEXT_CREATED]);
}
}
$this->rawCursorRow = last($rows);
return $rows;
}
/**
* 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 occurring.
*
* @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) {
if ($this->shouldLimitResults()) {
$limit = $this->getRawResultLimit();
@ -218,7 +333,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
}
final protected function didLoadResults(array $results) {
if ($this->beforeID) {
if ($this->getIsQueryOrderReversed()) {
$results = array_reverse($results, $preserve_keys = true);
}
@ -230,10 +345,11 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
$this->setLimit($limit + 1);
if ($pager->getAfterID()) {
$this->setAfterID($pager->getAfterID());
if (strlen($pager->getAfterID())) {
$this->setExternalCursorString($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setBeforeID($pager->getBeforeID());
$this->setExternalCursorString($pager->getBeforeID());
$this->setIsQueryOrderReversed(true);
}
$results = $this->execute();
@ -241,15 +357,22 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
$sliced_results = $pager->sliceResults($results);
if ($sliced_results) {
list($before, $after) = $this->getPageCursors($sliced_results);
// If we have results, generate external-facing cursors from the visible
// results. This stops us from leaking any internal details about objects
// which we loaded but which were not visible to the viewer.
if ($pager->getBeforeID() || ($count > $limit)) {
$pager->setNextPageID($after);
$last_object = last($sliced_results);
$cursor = $this->getExternalCursorStringForResult($last_object);
$pager->setNextPageID($cursor);
}
if ($pager->getAfterID() ||
($pager->getBeforeID() && ($count > $limit))) {
$pager->setPrevPageID($before);
$head_object = head($sliced_results);
$cursor = $this->getExternalCursorStringForResult($head_object);
$pager->setPrevPageID($cursor);
}
}
@ -421,40 +544,42 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
*/
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
$vector = $this->getQueryableOrderVector();
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.
// If we don't have a cursor object yet, it means we're trying to load
// the first result page. We may need to build a cursor object from the
// external string, or we may not need a paging clause yet.
$cursor_object = $this->getInternalCursorObject();
if (!$cursor_object) {
$external_cursor = $this->getExternalCursorString();
if ($external_cursor !== null) {
$cursor_object = $this->getInternalCursorFromExternalCursor(
$external_cursor);
}
}
// If we still don't have a cursor object, this is the first result page
// and we aren't paging it. We don't need to build a paging clause.
if (!$cursor_object) {
return qsprintf($conn, '');
}
$reversed = $this->getIsQueryOrderReversed();
$keys = array();
foreach ($vector as $order) {
$keys[] = $order->getOrderKey();
}
$keys = array_fuse($keys);
$value_map = $this->getPagingValueMap($cursor, $keys);
$value_map = $this->getPagingMapFromCursorObject(
$cursor_object,
$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];
@ -476,48 +601,6 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
}
/**
* @task paging
*/
protected function getPagingValueMap($cursor, array $keys) {
return array(
'id' => $cursor,
);
}
/**
* @task paging
*/
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 in query "%s".',
$cursor,
get_class($this)));
}
return $object;
}
/**
* @task paging
*/
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
return;
}
/**
* Simplifies the task of constructing a paging clause across multiple
* columns. In the general case, this looks like:
@ -1020,18 +1103,21 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
if ($this->supportsFerretEngine()) {
$columns['rank'] = array(
'table' => null,
'column' => '_ft_rank',
'column' => self::FULLTEXT_RANK,
'type' => 'int',
'requires-ferret' => true,
);
$columns['fulltext-created'] = array(
'table' => 'ft_doc',
'column' => 'epochCreated',
'table' => null,
'column' => self::FULLTEXT_CREATED,
'type' => 'int',
'requires-ferret' => true,
);
$columns['fulltext-modified'] = array(
'table' => 'ft_doc',
'column' => 'epochModified',
'table' => null,
'column' => self::FULLTEXT_MODIFIED,
'type' => 'int',
'requires-ferret' => true,
);
}
@ -1049,11 +1135,12 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
$for_union = false) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
$vector = $this->getQueryableOrderVector();
$parts = array();
foreach ($vector as $order) {
$part = $orderable[$order->getOrderKey()];
if ($order->getIsReversed()) {
$part['reverse'] = !idx($part, 'reverse', false);
}
@ -1063,6 +1150,31 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
return $this->formatOrderClause($conn, $parts, $for_union);
}
/**
* @task order
*/
private function getQueryableOrderVector() {
$vector = $this->getOrderVector();
$orderable = $this->getOrderableColumns();
$keep = array();
foreach ($vector as $order) {
$column = $orderable[$order->getOrderKey()];
// If this is a Ferret fulltext column but the query doesn't actually
// have a fulltext query, we'll skip most of the Ferret stuff and won't
// actually have the columns in the result set. Just skip them.
if (!empty($column['requires-ferret'])) {
if (!$this->getFerretTokens()) {
continue;
}
}
$keep[] = $order->getAsScalar();
}
return PhabricatorQueryOrderVector::newFromVector($keep);
}
/**
* @task order
@ -1072,10 +1184,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
array $parts,
$for_union = false) {
$is_query_reversed = false;
if ($this->getBeforeID()) {
$is_query_reversed = !$is_query_reversed;
}
$is_query_reversed = $this->getIsQueryOrderReversed();
$sql = array();
foreach ($parts as $key => $part) {
@ -1676,7 +1785,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
}
if (!$this->ferretEngine) {
$select[] = qsprintf($conn, '0 _ft_rank');
$select[] = qsprintf($conn, '0 AS %T', self::FULLTEXT_RANK);
return $select;
}
@ -1755,8 +1864,27 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
$select[] = qsprintf(
$conn,
'%Q _ft_rank',
$sum);
'%Q AS %T',
$sum,
self::FULLTEXT_RANK);
// See D20297. We select these as real columns in the result set so that
// constructions like this will work:
//
// ((SELECT ...) UNION (SELECT ...)) ORDER BY ...
//
// If the columns aren't part of the result set, the final "ORDER BY" can
// not act on them.
$select[] = qsprintf(
$conn,
'ft_doc.epochCreated AS %T',
self::FULLTEXT_CREATED);
$select[] = qsprintf(
$conn,
'ft_doc.epochModified AS %T',
self::FULLTEXT_MODIFIED);
return $select;
}

View file

@ -45,6 +45,8 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
*/
private $raisePolicyExceptions;
private $isOverheated;
private $returnPartialResultsOnOverheat;
private $disableOverheating;
/* -( Query Configuration )------------------------------------------------ */
@ -130,6 +132,16 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
return $this;
}
final public function setReturnPartialResultsOnOverheat($bool) {
$this->returnPartialResultsOnOverheat = $bool;
return $this;
}
final public function setDisableOverheating($disable_overheating) {
$this->disableOverheating = $disable_overheating;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
@ -282,6 +294,13 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
$this->didFilterResults($removed);
// NOTE: We call "nextPage()" before checking if we've found enough
// results because we want to build the internal cursor object even
// if we don't need to execute another query: the internal cursor may
// be used by a parent query that is using this query to translate an
// external cursor into an internal cursor.
$this->nextPage($page);
foreach ($visible as $key => $result) {
++$count;
@ -312,12 +331,23 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
break;
}
$this->nextPage($page);
if (!$this->disableOverheating) {
if ($overheat_limit && ($total_seen >= $overheat_limit)) {
$this->isOverheated = true;
if (!$this->returnPartialResultsOnOverheat) {
throw new Exception(
pht(
'Query (of class "%s") overheated: examined more than %s '.
'raw rows without finding %s visible objects.',
get_class($this),
new PhutilNumber($overheat_limit),
new PhutilNumber($need)));
}
break;
}
}
} while (true);
$results = $this->didLoadResults($results);

View file

@ -0,0 +1,47 @@
<?php
final class PhabricatorQueryCursor
extends Phobject {
private $object;
private $rawRow;
public function setObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function setRawRow(array $raw_row) {
$this->rawRow = $raw_row;
return $this;
}
public function getRawRow() {
return $this->rawRow;
}
public function getRawRowProperty($key) {
if (!is_array($this->rawRow)) {
throw new Exception(
pht(
'Caller is trying to "getRawRowProperty()" with key "%s", but this '.
'cursor has no raw row.',
$key));
}
if (!array_key_exists($key, $this->rawRow)) {
throw new Exception(
pht(
'Caller is trying to access raw row property "%s", but the row '.
'does not have this property.',
$key));
}
return $this->rawRow[$key];
}
}

View file

@ -25,6 +25,8 @@ final class PhabricatorQueryIterator extends PhutilBufferedIterator {
$pager = clone $this->pager;
$query = clone $this->query;
$query->setDisableOverheating(true);
$results = $query->executeWithCursorPager($pager);
// If we got less than a full page of results, this was the last set of