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:
commit
7477da3d4f
36 changed files with 735 additions and 387 deletions
|
@ -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',
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.*');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 )----------------------------------------- */
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorInvalidQueryCursorException
|
||||
extends Exception {}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
47
src/infrastructure/query/policy/PhabricatorQueryCursor.php
Normal file
47
src/infrastructure/query/policy/PhabricatorQueryCursor.php
Normal 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];
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue