Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/src/applications/almanac/query/AlmanacDeviceQuery.php b/src/applications/almanac/query/AlmanacDeviceQuery.php
index 0d38070e0b..21f8c952ef 100644
--- a/src/applications/almanac/query/AlmanacDeviceQuery.php
+++ b/src/applications/almanac/query/AlmanacDeviceQuery.php
@@ -1,146 +1,145 @@
<?php
final class AlmanacDeviceQuery
extends AlmanacQuery {
private $ids;
private $phids;
private $names;
private $namePrefix;
private $nameSuffix;
private $isClusterDevice;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withNameSuffix($suffix) {
$this->nameSuffix = $suffix;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new AlmanacDeviceNameNgrams(),
$ngrams);
}
public function withIsClusterDevice($is_cluster_device) {
$this->isClusterDevice = $is_cluster_device;
return $this;
}
public function newResultObject() {
return new AlmanacDevice();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'device.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'device.phid IN (%Ls)',
$this->phids);
}
if ($this->names !== null) {
$hashes = array();
foreach ($this->names as $name) {
$hashes[] = PhabricatorHash::digestForIndex($name);
}
$where[] = qsprintf(
$conn,
'device.nameIndex IN (%Ls)',
$hashes);
}
if ($this->namePrefix !== null) {
$where[] = qsprintf(
$conn,
'device.name LIKE %>',
$this->namePrefix);
}
if ($this->nameSuffix !== null) {
$where[] = qsprintf(
$conn,
'device.name LIKE %<',
$this->nameSuffix);
}
if ($this->isClusterDevice !== null) {
$where[] = qsprintf(
$conn,
'device.isBoundToClusterService = %d',
(int)$this->isClusterDevice);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'device';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
- 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(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Device Name'),
),
) + parent::getBuiltinOrders();
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
}
diff --git a/src/applications/almanac/query/AlmanacNamespaceQuery.php b/src/applications/almanac/query/AlmanacNamespaceQuery.php
index 81332cf03b..d4378e17c7 100644
--- a/src/applications/almanac/query/AlmanacNamespaceQuery.php
+++ b/src/applications/almanac/query/AlmanacNamespaceQuery.php
@@ -1,103 +1,102 @@
<?php
final class AlmanacNamespaceQuery
extends AlmanacQuery {
private $ids;
private $phids;
private $names;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new AlmanacNamespaceNameNgrams(),
$ngrams);
}
public function newResultObject() {
return new AlmanacNamespace();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'namespace.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'namespace.phid IN (%Ls)',
$this->phids);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'namespace.name IN (%Ls)',
$this->names);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'namespace';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
- 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(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Namespace Name'),
),
) + parent::getBuiltinOrders();
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
}
diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php
index 3374413e5b..edc55276a9 100644
--- a/src/applications/almanac/query/AlmanacServiceQuery.php
+++ b/src/applications/almanac/query/AlmanacServiceQuery.php
@@ -1,226 +1,225 @@
<?php
final class AlmanacServiceQuery
extends AlmanacQuery {
private $ids;
private $phids;
private $names;
private $serviceTypes;
private $devicePHIDs;
private $namePrefix;
private $nameSuffix;
private $needBindings;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withServiceTypes(array $types) {
$this->serviceTypes = $types;
return $this;
}
public function withDevicePHIDs(array $phids) {
$this->devicePHIDs = $phids;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withNameSuffix($suffix) {
$this->nameSuffix = $suffix;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new AlmanacServiceNameNgrams(),
$ngrams);
}
public function needBindings($need_bindings) {
$this->needBindings = $need_bindings;
return $this;
}
public function newResultObject() {
return new AlmanacService();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinBindingTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T binding ON service.phid = binding.servicePHID',
id(new AlmanacBinding())->getTableName());
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'service.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'service.phid IN (%Ls)',
$this->phids);
}
if ($this->names !== null) {
$hashes = array();
foreach ($this->names as $name) {
$hashes[] = PhabricatorHash::digestForIndex($name);
}
$where[] = qsprintf(
$conn,
'service.nameIndex IN (%Ls)',
$hashes);
}
if ($this->serviceTypes !== null) {
$where[] = qsprintf(
$conn,
'service.serviceType IN (%Ls)',
$this->serviceTypes);
}
if ($this->devicePHIDs !== null) {
$where[] = qsprintf(
$conn,
'binding.devicePHID IN (%Ls)',
$this->devicePHIDs);
}
if ($this->namePrefix !== null) {
$where[] = qsprintf(
$conn,
'service.name LIKE %>',
$this->namePrefix);
}
if ($this->nameSuffix !== null) {
$where[] = qsprintf(
$conn,
'service.name LIKE %<',
$this->nameSuffix);
}
return $where;
}
protected function willFilterPage(array $services) {
$service_map = AlmanacServiceType::getAllServiceTypes();
foreach ($services as $key => $service) {
$implementation = idx($service_map, $service->getServiceType());
if (!$implementation) {
$this->didRejectResult($service);
unset($services[$key]);
continue;
}
$implementation = clone $implementation;
$service->attachServiceImplementation($implementation);
}
return $services;
}
protected function didFilterPage(array $services) {
if ($this->needBindings) {
$service_phids = mpull($services, 'getPHID');
$bindings = id(new AlmanacBindingQuery())
->setViewer($this->getViewer())
->withServicePHIDs($service_phids)
->needProperties($this->getNeedProperties())
->execute();
$bindings = mgroup($bindings, 'getServicePHID');
foreach ($services as $service) {
$service_bindings = idx($bindings, $service->getPHID(), array());
$service->attachBindings($service_bindings);
}
}
return parent::didFilterPage($services);
}
private function shouldJoinBindingTable() {
return ($this->devicePHIDs !== null);
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinBindingTable()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function getPrimaryTableAlias() {
return 'service';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
- 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(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Service Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/badges/query/PhabricatorBadgesQuery.php b/src/applications/badges/query/PhabricatorBadgesQuery.php
index c977e3f826..dcadf881fe 100644
--- a/src/applications/badges/query/PhabricatorBadgesQuery.php
+++ b/src/applications/badges/query/PhabricatorBadgesQuery.php
@@ -1,119 +1,119 @@
<?php
final class PhabricatorBadgesQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $qualities;
private $statuses;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withQualities(array $qualities) {
$this->qualities = $qualities;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
id(new PhabricatorBadgesBadgeNameNgrams()),
$ngrams);
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function getPrimaryTableAlias() {
return 'badges';
}
public function newResultObject() {
return new PhabricatorBadgesBadge();
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'badges.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'badges.phid IN (%Ls)',
$this->phids);
}
if ($this->qualities !== null) {
$where[] = qsprintf(
$conn,
'badges.quality IN (%Ls)',
$this->qualities);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'badges.status IN (%Ls)',
$this->statuses);
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorBadgesApplication';
}
public function getBuiltinOrders() {
return array(
'quality' => array(
'vector' => array('quality', 'id'),
'name' => pht('Rarity (Rarest First)'),
),
'shoddiness' => array(
'vector' => array('-quality', '-id'),
'name' => pht('Rarity (Most Common First)'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return array(
'quality' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'quality',
'reverse' => true,
'type' => 'int',
),
) + 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(),
);
}
}
diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
index fc1399fdb3..db50bb4d77 100644
--- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
@@ -1,748 +1,747 @@
<?php
final class PhabricatorCalendarEventQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $rangeBegin;
private $rangeEnd;
private $inviteePHIDs;
private $hostPHIDs;
private $isCancelled;
private $eventsWithNoParent;
private $instanceSequencePairs;
private $isStub;
private $parentEventPHIDs;
private $importSourcePHIDs;
private $importAuthorPHIDs;
private $importUIDs;
private $utcInitialEpochMin;
private $utcInitialEpochMax;
private $isImported;
private $needRSVPs;
private $generateGhosts = false;
public function newResultObject() {
return new PhabricatorCalendarEvent();
}
public function setGenerateGhosts($generate_ghosts) {
$this->generateGhosts = $generate_ghosts;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withDateRange($begin, $end) {
$this->rangeBegin = $begin;
$this->rangeEnd = $end;
return $this;
}
public function withUTCInitialEpochBetween($min, $max) {
$this->utcInitialEpochMin = $min;
$this->utcInitialEpochMax = $max;
return $this;
}
public function withInvitedPHIDs(array $phids) {
$this->inviteePHIDs = $phids;
return $this;
}
public function withHostPHIDs(array $phids) {
$this->hostPHIDs = $phids;
return $this;
}
public function withIsCancelled($is_cancelled) {
$this->isCancelled = $is_cancelled;
return $this;
}
public function withIsStub($is_stub) {
$this->isStub = $is_stub;
return $this;
}
public function withEventsWithNoParent($events_with_no_parent) {
$this->eventsWithNoParent = $events_with_no_parent;
return $this;
}
public function withInstanceSequencePairs(array $pairs) {
$this->instanceSequencePairs = $pairs;
return $this;
}
public function withParentEventPHIDs(array $parent_phids) {
$this->parentEventPHIDs = $parent_phids;
return $this;
}
public function withImportSourcePHIDs(array $import_phids) {
$this->importSourcePHIDs = $import_phids;
return $this;
}
public function withImportAuthorPHIDs(array $author_phids) {
$this->importAuthorPHIDs = $author_phids;
return $this;
}
public function withImportUIDs(array $uids) {
$this->importUIDs = $uids;
return $this;
}
public function withIsImported($is_imported) {
$this->isImported = $is_imported;
return $this;
}
public function needRSVPs(array $phids) {
$this->needRSVPs = $phids;
return $this;
}
protected function getDefaultOrderVector() {
return array('start', 'id');
}
public function getBuiltinOrders() {
return array(
'start' => array(
'vector' => array('start', 'id'),
'name' => pht('Event Start'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return array(
'start' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'utcInitialEpoch',
'reverse' => true,
'type' => 'int',
'unique' => false,
),
) + 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(),
);
}
protected function shouldLimitResults() {
// When generating ghosts, we can't rely on database ordering because
// MySQL can't predict the ghost start times. We'll just load all matching
// events, then generate results from there.
if ($this->generateGhosts) {
return false;
}
return true;
}
protected function loadPage() {
$events = $this->loadStandardPage($this->newResultObject());
$viewer = $this->getViewer();
foreach ($events as $event) {
$event->applyViewerTimezone($viewer);
}
if (!$this->generateGhosts) {
return $events;
}
$raw_limit = $this->getRawResultLimit();
if (!$raw_limit && !$this->rangeEnd) {
throw new Exception(
pht(
'Event queries which generate ghost events must include either a '.
'result limit or an end date, because they may otherwise generate '.
'an infinite number of results. This query has neither.'));
}
foreach ($events as $key => $event) {
$sequence_start = 0;
$sequence_end = null;
$end = null;
$instance_of = $event->getInstanceOfEventPHID();
if ($instance_of == null && $this->isCancelled !== null) {
if ($event->getIsCancelled() != $this->isCancelled) {
unset($events[$key]);
continue;
}
}
}
// Pull out all of the parents first. We may discard them as we begin
// generating ghost events, but we still want to process all of them.
$parents = array();
foreach ($events as $key => $event) {
if ($event->isParentEvent()) {
$parents[$key] = $event;
}
}
// Now that we've picked out all the parent events, we can immediately
// discard anything outside of the time window.
$events = $this->getEventsInRange($events);
$generate_from = $this->rangeBegin;
$generate_until = $this->rangeEnd;
foreach ($parents as $key => $event) {
$duration = $event->getDuration();
$start_date = $this->getRecurrenceWindowStart(
$event,
$generate_from - $duration);
$end_date = $this->getRecurrenceWindowEnd(
$event,
$generate_until);
$limit = $this->getRecurrenceLimit($event, $raw_limit);
$set = $event->newRecurrenceSet();
$recurrences = $set->getEventsBetween(
$start_date,
$end_date,
$limit + 1);
// We're generating events from the beginning and then filtering them
// here (instead of only generating events starting at the start date)
// because we need to know the proper sequence indexes to generate ghost
// events. This may change after RDATE support.
if ($start_date) {
$start_epoch = $start_date->getEpoch();
} else {
$start_epoch = null;
}
foreach ($recurrences as $sequence_index => $sequence_datetime) {
if (!$sequence_index) {
// This is the parent event, which we already have.
continue;
}
if ($start_epoch) {
if ($sequence_datetime->getEpoch() < $start_epoch) {
continue;
}
}
$events[] = $event->newGhost(
$viewer,
$sequence_index,
$sequence_datetime);
}
// NOTE: We're slicing results every time because this makes it cheaper
// to generate future ghosts. If we already have 100 events that occur
// before July 1, we know we never need to generate ghosts after that
// because they couldn't possibly ever appear in the result set.
if ($raw_limit) {
if (count($events) > $raw_limit) {
$events = msort($events, 'getStartDateTimeEpoch');
$events = array_slice($events, 0, $raw_limit, true);
$generate_until = last($events)->getEndDateTimeEpoch();
}
}
}
// Now that we're done generating ghost events, we're going to remove any
// ghosts that we have concrete events for (or which we can load the
// concrete events for). These concrete events are generated when users
// edit a ghost, and replace the ghost events.
// First, generate a map of all concrete <parentPHID, sequence> events we
// already loaded. We don't need to load these again.
$have_pairs = array();
foreach ($events as $event) {
if ($event->getIsGhostEvent()) {
continue;
}
$parent_phid = $event->getInstanceOfEventPHID();
$sequence = $event->getSequenceIndex();
$have_pairs[$parent_phid][$sequence] = true;
}
// Now, generate a map of all <parentPHID, sequence> events we generated
// ghosts for. We need to try to load these if we don't already have them.
$map = array();
$parent_pairs = array();
foreach ($events as $key => $event) {
if (!$event->getIsGhostEvent()) {
continue;
}
$parent_phid = $event->getInstanceOfEventPHID();
$sequence = $event->getSequenceIndex();
// We already loaded the concrete version of this event, so we can just
// throw out the ghost and move on.
if (isset($have_pairs[$parent_phid][$sequence])) {
unset($events[$key]);
continue;
}
// We didn't load the concrete version of this event, so we need to
// try to load it if it exists.
$parent_pairs[] = array($parent_phid, $sequence);
$map[$parent_phid][$sequence] = $key;
}
if ($parent_pairs) {
$instances = id(new self())
->setViewer($viewer)
->setParentQuery($this)
->withInstanceSequencePairs($parent_pairs)
->execute();
foreach ($instances as $instance) {
$parent_phid = $instance->getInstanceOfEventPHID();
$sequence = $instance->getSequenceIndex();
$indexes = idx($map, $parent_phid);
$key = idx($indexes, $sequence);
// Replace the ghost with the corresponding concrete event.
$events[$key] = $instance;
}
}
$events = msort($events, 'getStartDateTimeEpoch');
return $events;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
$parts = parent::buildJoinClauseParts($conn_r);
if ($this->inviteePHIDs !== null) {
$parts[] = qsprintf(
$conn_r,
'JOIN %T invitee ON invitee.eventPHID = event.phid
AND invitee.status != %s',
id(new PhabricatorCalendarEventInvitee())->getTableName(),
PhabricatorCalendarEventInvitee::STATUS_UNINVITED);
}
return $parts;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'event.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'event.phid IN (%Ls)',
$this->phids);
}
// NOTE: The date ranges we query for are larger than the requested ranges
// because we need to catch all-day events. We'll refine this range later
// after adjusting the visible range of events we load.
if ($this->rangeBegin) {
$where[] = qsprintf(
$conn,
'(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)',
$this->rangeBegin - phutil_units('16 hours in seconds'));
}
if ($this->rangeEnd) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch <= %d',
$this->rangeEnd + phutil_units('16 hours in seconds'));
}
if ($this->utcInitialEpochMin !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch >= %d',
$this->utcInitialEpochMin);
}
if ($this->utcInitialEpochMax !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch <= %d',
$this->utcInitialEpochMax);
}
if ($this->inviteePHIDs !== null) {
$where[] = qsprintf(
$conn,
'invitee.inviteePHID IN (%Ls)',
$this->inviteePHIDs);
}
if ($this->hostPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.hostPHID IN (%Ls)',
$this->hostPHIDs);
}
if ($this->isCancelled !== null) {
$where[] = qsprintf(
$conn,
'event.isCancelled = %d',
(int)$this->isCancelled);
}
if ($this->eventsWithNoParent == true) {
$where[] = qsprintf(
$conn,
'event.instanceOfEventPHID IS NULL');
}
if ($this->instanceSequencePairs !== null) {
$sql = array();
foreach ($this->instanceSequencePairs as $pair) {
$sql[] = qsprintf(
$conn,
'(event.instanceOfEventPHID = %s AND event.sequenceIndex = %d)',
$pair[0],
$pair[1]);
}
$where[] = qsprintf(
$conn,
'%LO',
$sql);
}
if ($this->isStub !== null) {
$where[] = qsprintf(
$conn,
'event.isStub = %d',
(int)$this->isStub);
}
if ($this->parentEventPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.instanceOfEventPHID IN (%Ls)',
$this->parentEventPHIDs);
}
if ($this->importSourcePHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IN (%Ls)',
$this->importSourcePHIDs);
}
if ($this->importAuthorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importAuthorPHID IN (%Ls)',
$this->importAuthorPHIDs);
}
if ($this->importUIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importUID IN (%Ls)',
$this->importUIDs);
}
if ($this->isImported !== null) {
if ($this->isImported) {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IS NULL');
}
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'event';
}
protected function shouldGroupQueryResultRows() {
if ($this->inviteePHIDs !== null) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getQueryApplicationClass() {
return 'PhabricatorCalendarApplication';
}
protected function willFilterPage(array $events) {
$instance_of_event_phids = array();
$recurring_events = array();
$viewer = $this->getViewer();
$events = $this->getEventsInRange($events);
$import_phids = array();
foreach ($events as $event) {
$import_phid = $event->getImportSourcePHID();
if ($import_phid !== null) {
$import_phids[$import_phid] = $import_phid;
}
}
if ($import_phids) {
$imports = id(new PhabricatorCalendarImportQuery())
->setParentQuery($this)
->setViewer($viewer)
->withPHIDs($import_phids)
->execute();
$imports = mpull($imports, null, 'getPHID');
} else {
$imports = array();
}
foreach ($events as $key => $event) {
$import_phid = $event->getImportSourcePHID();
if ($import_phid === null) {
$event->attachImportSource(null);
continue;
}
$import = idx($imports, $import_phid);
if (!$import) {
unset($events[$key]);
$this->didRejectResult($event);
continue;
}
$event->attachImportSource($import);
}
$phids = array();
foreach ($events as $event) {
$phids[] = $event->getPHID();
$instance_of = $event->getInstanceOfEventPHID();
if ($instance_of) {
$instance_of_event_phids[] = $instance_of;
}
}
if (count($instance_of_event_phids) > 0) {
$recurring_events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withPHIDs($instance_of_event_phids)
->withEventsWithNoParent(true)
->execute();
$recurring_events = mpull($recurring_events, null, 'getPHID');
}
if ($events) {
$invitees = id(new PhabricatorCalendarEventInviteeQuery())
->setViewer($viewer)
->withEventPHIDs($phids)
->execute();
$invitees = mgroup($invitees, 'getEventPHID');
} else {
$invitees = array();
}
foreach ($events as $key => $event) {
$event_invitees = idx($invitees, $event->getPHID(), array());
$event->attachInvitees($event_invitees);
$instance_of = $event->getInstanceOfEventPHID();
if (!$instance_of) {
continue;
}
$parent = idx($recurring_events, $instance_of);
// should never get here
if (!$parent) {
unset($events[$key]);
continue;
}
$event->attachParentEvent($parent);
if ($this->isCancelled !== null) {
if ($event->getIsCancelled() != $this->isCancelled) {
unset($events[$key]);
continue;
}
}
}
$events = msort($events, 'getStartDateTimeEpoch');
if ($this->needRSVPs) {
$rsvp_phids = $this->needRSVPs;
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
$project_phids = array();
foreach ($events as $event) {
foreach ($event->getInvitees() as $invitee) {
$invitee_phid = $invitee->getInviteePHID();
if (phid_get_type($invitee_phid) == $project_type) {
$project_phids[] = $invitee_phid;
}
}
}
if ($project_phids) {
$member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($project_phids)
->withEdgeTypes(array($member_type))
->withDestinationPHIDs($rsvp_phids);
$edges = $query->execute();
$project_map = array();
foreach ($edges as $src => $types) {
foreach ($types as $type => $dsts) {
foreach ($dsts as $dst => $edge) {
$project_map[$dst][] = $src;
}
}
}
} else {
$project_map = array();
}
$membership_map = array();
foreach ($rsvp_phids as $rsvp_phid) {
$membership_map[$rsvp_phid] = array();
$membership_map[$rsvp_phid][] = $rsvp_phid;
$project_phids = idx($project_map, $rsvp_phid);
if ($project_phids) {
foreach ($project_phids as $project_phid) {
$membership_map[$rsvp_phid][] = $project_phid;
}
}
}
foreach ($events as $event) {
$invitees = $event->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
$rsvp_map = array();
foreach ($rsvp_phids as $rsvp_phid) {
$membership_phids = $membership_map[$rsvp_phid];
$rsvps = array_select_keys($invitees, $membership_phids);
$rsvp_map[$rsvp_phid] = $rsvps;
}
$event->attachRSVPs($rsvp_map);
}
}
return $events;
}
private function getEventsInRange(array $events) {
$range_start = $this->rangeBegin;
$range_end = $this->rangeEnd;
foreach ($events as $key => $event) {
$event_start = $event->getStartDateTimeEpoch();
$event_end = $event->getEndDateTimeEpoch();
if ($range_start && $event_end < $range_start) {
unset($events[$key]);
}
if ($range_end && $event_start > $range_end) {
unset($events[$key]);
}
}
return $events;
}
private function getRecurrenceWindowStart(
PhabricatorCalendarEvent $event,
$generate_from) {
if (!$generate_from) {
return null;
}
return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from);
}
private function getRecurrenceWindowEnd(
PhabricatorCalendarEvent $event,
$generate_until) {
$end_epochs = array();
if ($generate_until) {
$end_epochs[] = $generate_until;
}
$until_epoch = $event->getUntilDateTimeEpoch();
if ($until_epoch) {
$end_epochs[] = $until_epoch;
}
if (!$end_epochs) {
return null;
}
return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs));
}
private function getRecurrenceLimit(
PhabricatorCalendarEvent $event,
$raw_limit) {
$count = $event->getRecurrenceCount();
if ($count && ($count <= $raw_limit)) {
return ($count - 1);
}
return $raw_limit;
}
}
diff --git a/src/applications/countdown/query/PhabricatorCountdownQuery.php b/src/applications/countdown/query/PhabricatorCountdownQuery.php
index e6c410ee49..67a2f3a9e3 100644
--- a/src/applications/countdown/query/PhabricatorCountdownQuery.php
+++ b/src/applications/countdown/query/PhabricatorCountdownQuery.php
@@ -1,108 +1,107 @@
<?php
final class PhabricatorCountdownQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $upcoming;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withUpcoming() {
$this->upcoming = true;
return $this;
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
public function newResultObject() {
return new PhabricatorCountdown();
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'authorPHID in (%Ls)',
$this->authorPHIDs);
}
if ($this->upcoming !== null) {
$where[] = qsprintf(
$conn,
'epoch >= %d',
PhabricatorTime::getNow());
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorCountdownApplication';
}
public function getBuiltinOrders() {
return array(
'ending' => array(
'vector' => array('-epoch', '-id'),
'name' => pht('End Date (Past to Future)'),
),
'unending' => array(
'vector' => array('epoch', 'id'),
'name' => pht('End Date (Future to Past)'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return array(
'epoch' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'epoch',
'type' => 'int',
),
) + 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(),
);
}
}
diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php
index fdd4904bee..a385fc5252 100644
--- a/src/applications/differential/query/DifferentialRevisionQuery.php
+++ b/src/applications/differential/query/DifferentialRevisionQuery.php
@@ -1,1029 +1,1028 @@
<?php
/**
* @task config Query Configuration
* @task exec Query Execution
* @task internal Internals
*/
final class DifferentialRevisionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $pathIDs = array();
private $authors = array();
private $draftAuthors = array();
private $ccs = array();
private $reviewers = array();
private $revIDs = array();
private $commitHashes = array();
private $commitPHIDs = array();
private $phids = array();
private $responsibles = array();
private $branches = array();
private $repositoryPHIDs;
private $updatedEpochMin;
private $updatedEpochMax;
private $statuses;
private $isOpen;
private $createdEpochMin;
private $createdEpochMax;
const ORDER_MODIFIED = 'order-modified';
const ORDER_CREATED = 'order-created';
private $needActiveDiffs = false;
private $needDiffIDs = false;
private $needCommitPHIDs = false;
private $needHashes = false;
private $needReviewers = false;
private $needReviewerAuthority;
private $needDrafts;
private $needFlags;
/* -( Query Configuration )------------------------------------------------ */
/**
* Filter results to revisions which affect a Diffusion path ID in a given
* repository. You can call this multiple times to select revisions for
* several paths.
*
* @param int Diffusion repository ID.
* @param int Diffusion path ID.
* @return this
* @task config
*/
public function withPath($repository_id, $path_id) {
$this->pathIDs[] = array(
'repositoryID' => $repository_id,
'pathID' => $path_id,
);
return $this;
}
/**
* Filter results to revisions authored by one of the given PHIDs. Calling
* this function will clear anything set by previous calls to
* @{method:withAuthors}.
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withAuthors(array $author_phids) {
$this->authors = $author_phids;
return $this;
}
/**
* Filter results to revisions which CC one of the listed people. Calling this
* function will clear anything set by previous calls to @{method:withCCs}.
*
* @param array List of PHIDs of subscribers.
* @return this
* @task config
*/
public function withCCs(array $cc_phids) {
$this->ccs = $cc_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided PHIDs as
* reviewers. Calling this function will clear anything set by previous calls
* to @{method:withReviewers}.
*
* @param array List of PHIDs of reviewers
* @return this
* @task config
*/
public function withReviewers(array $reviewer_phids) {
$this->reviewers = $reviewer_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided commit hashes.
* Calling this function will clear anything set by previous calls to
* @{method:withCommitHashes}.
*
* @param array List of pairs <Class
* ArcanistDifferentialRevisionHash::HASH_$type constant,
* hash>
* @return this
* @task config
*/
public function withCommitHashes(array $commit_hashes) {
$this->commitHashes = $commit_hashes;
return $this;
}
/**
* Filter results to revisions that have one of the provided PHIDs as
* commits. Calling this function will clear anything set by previous calls
* to @{method:withCommitPHIDs}.
*
* @param array List of PHIDs of commits
* @return this
* @task config
*/
public function withCommitPHIDs(array $commit_phids) {
$this->commitPHIDs = $commit_phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withIsOpen($is_open) {
$this->isOpen = $is_open;
return $this;
}
/**
* Filter results to revisions on given branches.
*
* @param list List of branch names.
* @return this
* @task config
*/
public function withBranches(array $branches) {
$this->branches = $branches;
return $this;
}
/**
* Filter results to only return revisions whose ids are in the given set.
*
* @param array List of revision ids
* @return this
* @task config
*/
public function withIDs(array $ids) {
$this->revIDs = $ids;
return $this;
}
/**
* Filter results to only return revisions whose PHIDs are in the given set.
*
* @param array List of revision PHIDs
* @return this
* @task config
*/
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
/**
* Given a set of users, filter results to return only revisions they are
* responsible for (i.e., they are either authors or reviewers).
*
* @param array List of user PHIDs.
* @return this
* @task config
*/
public function withResponsibleUsers(array $responsible_phids) {
$this->responsibles = $responsible_phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
public function withUpdatedEpochBetween($min, $max) {
$this->updatedEpochMin = $min;
$this->updatedEpochMax = $max;
return $this;
}
public function withCreatedEpochBetween($min, $max) {
$this->createdEpochMin = $min;
$this->createdEpochMax = $max;
return $this;
}
/**
* Set whether or not the query should load the active diff for each
* revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needActiveDiffs($need_active_diffs) {
$this->needActiveDiffs = $need_active_diffs;
return $this;
}
/**
* Set whether or not the query should load the associated commit PHIDs for
* each revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needCommitPHIDs($need_commit_phids) {
$this->needCommitPHIDs = $need_commit_phids;
return $this;
}
/**
* Set whether or not the query should load associated diff IDs for each
* revision.
*
* @param bool True to load and attach diff IDs.
* @return this
* @task config
*/
public function needDiffIDs($need_diff_ids) {
$this->needDiffIDs = $need_diff_ids;
return $this;
}
/**
* Set whether or not the query should load associated commit hashes for each
* revision.
*
* @param bool True to load and attach commit hashes.
* @return this
* @task config
*/
public function needHashes($need_hashes) {
$this->needHashes = $need_hashes;
return $this;
}
/**
* Set whether or not the query should load associated reviewers.
*
* @param bool True to load and attach reviewers.
* @return this
* @task config
*/
public function needReviewers($need_reviewers) {
$this->needReviewers = $need_reviewers;
return $this;
}
/**
* Request information about the viewer's authority to act on behalf of each
* reviewer. In particular, they have authority to act on behalf of projects
* they are a member of.
*
* @param bool True to load and attach authority.
* @return this
* @task config
*/
public function needReviewerAuthority($need_reviewer_authority) {
$this->needReviewerAuthority = $need_reviewer_authority;
return $this;
}
public function needFlags($need_flags) {
$this->needFlags = $need_flags;
return $this;
}
public function needDrafts($need_drafts) {
$this->needDrafts = $need_drafts;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
public function newResultObject() {
return new DifferentialRevision();
}
/**
* Execute the query as configured, returning matching
* @{class:DifferentialRevision} objects.
*
* @return list List of matching DifferentialRevision objects.
* @task exec
*/
protected function loadPage() {
$data = $this->loadData();
$data = $this->didLoadRawRows($data);
$table = $this->newResultObject();
return $table->loadAllFromArray($data);
}
protected function willFilterPage(array $revisions) {
$viewer = $this->getViewer();
$repository_phids = mpull($revisions, 'getRepositoryPHID');
$repository_phids = array_filter($repository_phids);
$repositories = array();
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
}
// If a revision is associated with a repository:
//
// - the viewer must be able to see the repository; or
// - the viewer must have an automatic view capability.
//
// In the latter case, we'll load the revision but not load the repository.
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
foreach ($revisions as $key => $revision) {
$repo_phid = $revision->getRepositoryPHID();
if (!$repo_phid) {
// The revision has no associated repository. Attach `null` and move on.
$revision->attachRepository(null);
continue;
}
$repository = idx($repositories, $repo_phid);
if ($repository) {
// The revision has an associated repository, and the viewer can see
// it. Attach it and move on.
$revision->attachRepository($repository);
continue;
}
if ($revision->hasAutomaticCapability($can_view, $viewer)) {
// The revision has an associated repository which the viewer can not
// see, but the viewer has an automatic capability on this revision.
// Load the revision without attaching a repository.
$revision->attachRepository(null);
continue;
}
if ($this->getViewer()->isOmnipotent()) {
// The viewer is omnipotent. Allow the revision to load even without
// a repository.
$revision->attachRepository(null);
continue;
}
// The revision has an associated repository, and the viewer can't see
// it, and the viewer has no special capabilities. Filter out this
// revision.
$this->didRejectResult($revision);
unset($revisions[$key]);
}
if (!$revisions) {
return array();
}
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
if ($this->needCommitPHIDs) {
$this->loadCommitPHIDs($conn_r, $revisions);
}
$need_active = $this->needActiveDiffs;
$need_ids = $need_active || $this->needDiffIDs;
if ($need_ids) {
$this->loadDiffIDs($conn_r, $revisions);
}
if ($need_active) {
$this->loadActiveDiffs($conn_r, $revisions);
}
if ($this->needHashes) {
$this->loadHashes($conn_r, $revisions);
}
if ($this->needReviewers || $this->needReviewerAuthority) {
$this->loadReviewers($conn_r, $revisions);
}
return $revisions;
}
protected function didFilterPage(array $revisions) {
$viewer = $this->getViewer();
if ($this->needFlags) {
$flags = id(new PhabricatorFlagQuery())
->setViewer($viewer)
->withOwnerPHIDs(array($viewer->getPHID()))
->withObjectPHIDs(mpull($revisions, 'getPHID'))
->execute();
$flags = mpull($flags, null, 'getObjectPHID');
foreach ($revisions as $revision) {
$revision->attachFlag(
$viewer,
idx($flags, $revision->getPHID()));
}
}
if ($this->needDrafts) {
PhabricatorDraftEngine::attachDrafts(
$viewer,
$revisions);
}
return $revisions;
}
private function loadData() {
$table = $this->newResultObject();
$conn = $table->establishConnection('r');
$selects = array();
// NOTE: If the query includes "responsiblePHIDs", we execute it as a
// UNION of revisions they own and revisions they're reviewing. This has
// much better performance than doing it with JOIN/WHERE.
if ($this->responsibles) {
$basic_authors = $this->authors;
$basic_reviewers = $this->reviewers;
try {
// Build the query where the responsible users are authors.
$this->authors = array_merge($basic_authors, $this->responsibles);
$this->reviewers = $basic_reviewers;
$selects[] = $this->buildSelectStatement($conn);
// Build the query where the responsible users are reviewers, or
// projects they are members of are reviewers.
$this->authors = $basic_authors;
$this->reviewers = array_merge($basic_reviewers, $this->responsibles);
$selects[] = $this->buildSelectStatement($conn);
// Put everything back like it was.
$this->authors = $basic_authors;
$this->reviewers = $basic_reviewers;
} catch (Exception $ex) {
$this->authors = $basic_authors;
$this->reviewers = $basic_reviewers;
throw $ex;
}
} else {
$selects[] = $this->buildSelectStatement($conn);
}
if (count($selects) > 1) {
$unions = null;
foreach ($selects as $select) {
if (!$unions) {
$unions = $select;
continue;
}
$unions = qsprintf(
$conn,
'%Q UNION DISTINCT %Q',
$unions,
$select);
}
$query = qsprintf(
$conn,
'%Q %Q %Q',
$unions,
$this->buildOrderClause($conn, true),
$this->buildLimitClause($conn));
} else {
$query = head($selects);
}
return queryfx_all($conn, '%Q', $query);
}
private function buildSelectStatement(AphrontDatabaseConnection $conn_r) {
$table = new DifferentialRevision();
$select = $this->buildSelectClause($conn_r);
$from = qsprintf(
$conn_r,
'FROM %T r',
$table->getTableName());
$joins = $this->buildJoinsClause($conn_r);
$where = $this->buildWhereClause($conn_r);
$group_by = $this->buildGroupClause($conn_r);
$having = $this->buildHavingClause($conn_r);
$order_by = $this->buildOrderClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
return qsprintf(
$conn_r,
'(%Q %Q %Q %Q %Q %Q %Q %Q)',
$select,
$from,
$joins,
$where,
$group_by,
$having,
$order_by,
$limit);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function buildJoinsClause(AphrontDatabaseConnection $conn) {
$joins = array();
if ($this->pathIDs) {
$path_table = new DifferentialAffectedPath();
$joins[] = qsprintf(
$conn,
'JOIN %T p ON p.revisionID = r.id',
$path_table->getTableName());
}
if ($this->commitHashes) {
$joins[] = qsprintf(
$conn,
'JOIN %T hash_rel ON hash_rel.revisionID = r.id',
ArcanistDifferentialRevisionHash::TABLE_NAME);
}
if ($this->ccs) {
$joins[] = qsprintf(
$conn,
'JOIN %T e_ccs ON e_ccs.src = r.phid '.
'AND e_ccs.type = %s '.
'AND e_ccs.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
$this->ccs);
}
if ($this->reviewers) {
$joins[] = qsprintf(
$conn,
'JOIN %T reviewer ON reviewer.revisionPHID = r.phid
AND reviewer.reviewerStatus != %s
AND reviewer.reviewerPHID in (%Ls)',
id(new DifferentialReviewer())->getTableName(),
DifferentialReviewerStatus::STATUS_RESIGNED,
$this->reviewers);
}
if ($this->draftAuthors) {
$joins[] = qsprintf(
$conn,
'JOIN %T has_draft ON has_draft.srcPHID = r.phid
AND has_draft.type = %s
AND has_draft.dstPHID IN (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasDraftEdgeType::EDGECONST,
$this->draftAuthors);
}
if ($this->commitPHIDs) {
$joins[] = qsprintf(
$conn,
'JOIN %T commits ON commits.revisionID = r.id',
DifferentialRevision::TABLE_COMMIT);
}
$joins[] = $this->buildJoinClauseParts($conn);
return $this->formatJoinClause($conn, $joins);
}
/**
* @task internal
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->pathIDs) {
$path_clauses = array();
$repo_info = igroup($this->pathIDs, 'repositoryID');
foreach ($repo_info as $repository_id => $paths) {
$path_clauses[] = qsprintf(
$conn,
'(p.repositoryID = %d AND p.pathID IN (%Ld))',
$repository_id,
ipull($paths, 'pathID'));
}
$path_clauses = qsprintf($conn, '%LO', $path_clauses);
$where[] = $path_clauses;
}
if ($this->authors) {
$where[] = qsprintf(
$conn,
'r.authorPHID IN (%Ls)',
$this->authors);
}
if ($this->revIDs) {
$where[] = qsprintf(
$conn,
'r.id IN (%Ld)',
$this->revIDs);
}
if ($this->repositoryPHIDs) {
$where[] = qsprintf(
$conn,
'r.repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->commitHashes) {
$hash_clauses = array();
foreach ($this->commitHashes as $info) {
list($type, $hash) = $info;
$hash_clauses[] = qsprintf(
$conn,
'(hash_rel.type = %s AND hash_rel.hash = %s)',
$type,
$hash);
}
$hash_clauses = qsprintf($conn, '%LO', $hash_clauses);
$where[] = $hash_clauses;
}
if ($this->commitPHIDs) {
$where[] = qsprintf(
$conn,
'commits.commitPHID IN (%Ls)',
$this->commitPHIDs);
}
if ($this->phids) {
$where[] = qsprintf(
$conn,
'r.phid IN (%Ls)',
$this->phids);
}
if ($this->branches) {
$where[] = qsprintf(
$conn,
'r.branchName in (%Ls)',
$this->branches);
}
if ($this->updatedEpochMin !== null) {
$where[] = qsprintf(
$conn,
'r.dateModified >= %d',
$this->updatedEpochMin);
}
if ($this->updatedEpochMax !== null) {
$where[] = qsprintf(
$conn,
'r.dateModified <= %d',
$this->updatedEpochMax);
}
if ($this->createdEpochMin !== null) {
$where[] = qsprintf(
$conn,
'r.dateCreated >= %d',
$this->createdEpochMin);
}
if ($this->createdEpochMax !== null) {
$where[] = qsprintf(
$conn,
'r.dateCreated <= %d',
$this->createdEpochMax);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'r.status in (%Ls)',
$this->statuses);
}
if ($this->isOpen !== null) {
if ($this->isOpen) {
$statuses = DifferentialLegacyQuery::getModernValues(
DifferentialLegacyQuery::STATUS_OPEN);
} else {
$statuses = DifferentialLegacyQuery::getModernValues(
DifferentialLegacyQuery::STATUS_CLOSED);
}
$where[] = qsprintf(
$conn,
'r.status in (%Ls)',
$statuses);
}
$where[] = $this->buildWhereClauseParts($conn);
return $this->formatWhereClause($conn, $where);
}
/**
* @task internal
*/
protected function shouldGroupQueryResultRows() {
$join_triggers = array_merge(
$this->pathIDs,
$this->ccs,
$this->reviewers);
if (count($join_triggers) > 1) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getBuiltinOrders() {
$orders = parent::getBuiltinOrders() + array(
'updated' => array(
'vector' => array('updated', 'id'),
'name' => pht('Date Updated (Latest First)'),
'aliases' => array(self::ORDER_MODIFIED),
),
'outdated' => array(
'vector' => array('-updated', '-id'),
'name' => pht('Date Updated (Oldest First)'),
),
);
// Alias the "newest" builtin to the historical key for it.
$orders['newest']['aliases'][] = self::ORDER_CREATED;
return $orders;
}
protected function getDefaultOrderVector() {
return array('updated', 'id');
}
public function getOrderableColumns() {
return array(
'updated' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'dateModified',
'type' => 'int',
),
) + 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(),
);
}
private function loadCommitPHIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$commit_phids = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
DifferentialRevision::TABLE_COMMIT,
mpull($revisions, 'getID'));
$commit_phids = igroup($commit_phids, 'revisionID');
foreach ($revisions as $revision) {
$phids = idx($commit_phids, $revision->getID(), array());
$phids = ipull($phids, 'commitPHID');
$revision->attachCommitPHIDs($phids);
}
}
private function loadDiffIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$diff_ids = queryfx_all(
$conn_r,
'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld)
ORDER BY id DESC',
$diff_table->getTableName(),
mpull($revisions, 'getID'));
$diff_ids = igroup($diff_ids, 'revisionID');
foreach ($revisions as $revision) {
$ids = idx($diff_ids, $revision->getID(), array());
$ids = ipull($ids, 'id');
$revision->attachDiffIDs($ids);
}
}
private function loadActiveDiffs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$load_ids = array();
foreach ($revisions as $revision) {
$diffs = $revision->getDiffIDs();
if ($diffs) {
$load_ids[] = max($diffs);
}
}
$active_diffs = array();
if ($load_ids) {
$active_diffs = $diff_table->loadAllWhere(
'id IN (%Ld)',
$load_ids);
}
$active_diffs = mpull($active_diffs, null, 'getRevisionID');
foreach ($revisions as $revision) {
$revision->attachActiveDiff(idx($active_diffs, $revision->getID()));
}
}
private function loadHashes(
AphrontDatabaseConnection $conn_r,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
'differential_revisionhash',
mpull($revisions, 'getID'));
$data = igroup($data, 'revisionID');
foreach ($revisions as $revision) {
$hashes = idx($data, $revision->getID(), array());
$list = array();
foreach ($hashes as $hash) {
$list[] = array($hash['type'], $hash['hash']);
}
$revision->attachHashes($list);
}
}
private function loadReviewers(
AphrontDatabaseConnection $conn,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$reviewer_table = new DifferentialReviewer();
$reviewer_rows = queryfx_all(
$conn,
'SELECT * FROM %T WHERE revisionPHID IN (%Ls)
ORDER BY id ASC',
$reviewer_table->getTableName(),
mpull($revisions, 'getPHID'));
$reviewer_list = $reviewer_table->loadAllFromArray($reviewer_rows);
$reviewer_map = mgroup($reviewer_list, 'getRevisionPHID');
foreach ($reviewer_map as $key => $reviewers) {
$reviewer_map[$key] = mpull($reviewers, null, 'getReviewerPHID');
}
$viewer = $this->getViewer();
$viewer_phid = $viewer->getPHID();
$allow_key = 'differential.allow-self-accept';
$allow_self = PhabricatorEnv::getEnvConfig($allow_key);
// Figure out which of these reviewers the viewer has authority to act as.
if ($this->needReviewerAuthority && $viewer_phid) {
$authority = $this->loadReviewerAuthority(
$revisions,
$reviewer_map,
$allow_self);
}
foreach ($revisions as $revision) {
$reviewers = idx($reviewer_map, $revision->getPHID(), array());
foreach ($reviewers as $reviewer_phid => $reviewer) {
if ($this->needReviewerAuthority) {
if (!$viewer_phid) {
// Logged-out users never have authority.
$has_authority = false;
} else if ((!$allow_self) &&
($revision->getAuthorPHID() == $viewer_phid)) {
// The author can never have authority unless we allow self-accept.
$has_authority = false;
} else {
// Otherwise, look up whether the viewer has authority.
$has_authority = isset($authority[$reviewer_phid]);
}
$reviewer->attachAuthority($viewer, $has_authority);
}
$reviewers[$reviewer_phid] = $reviewer;
}
$revision->attachReviewers($reviewers);
}
}
private function loadReviewerAuthority(
array $revisions,
array $reviewers,
$allow_self) {
$revision_map = mpull($revisions, null, 'getPHID');
$viewer_phid = $this->getViewer()->getPHID();
// Find all the project/package reviewers which the user may have authority
// over.
$project_phids = array();
$package_phids = array();
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
$package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
foreach ($reviewers as $revision_phid => $reviewer_list) {
if (!$allow_self) {
if ($revision_map[$revision_phid]->getAuthorPHID() == $viewer_phid) {
// If self-review isn't permitted, the user will never have
// authority over projects on revisions they authored because you
// can't accept your own revisions, so we don't need to load any
// data about these reviewers.
continue;
}
}
foreach ($reviewer_list as $reviewer_phid => $reviewer) {
$phid_type = phid_get_type($reviewer_phid);
if ($phid_type == $project_type) {
$project_phids[] = $reviewer_phid;
}
if ($phid_type == $package_type) {
$package_phids[] = $reviewer_phid;
}
}
}
// The viewer has authority over themselves.
$user_authority = array_fuse(array($viewer_phid));
// And over any projects they are a member of.
$project_authority = array();
if ($project_phids) {
$project_authority = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->withMemberPHIDs(array($viewer_phid))
->execute();
$project_authority = mpull($project_authority, 'getPHID');
$project_authority = array_fuse($project_authority);
}
// And over any packages they own.
$package_authority = array();
if ($package_phids) {
$package_authority = id(new PhabricatorOwnersPackageQuery())
->setViewer($this->getViewer())
->withPHIDs($package_phids)
->withAuthorityPHIDs(array($viewer_phid))
->execute();
$package_authority = mpull($package_authority, 'getPHID');
$package_authority = array_fuse($package_authority);
}
return $user_authority + $project_authority + $package_authority;
}
public function getQueryApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function getPrimaryTableAlias() {
return 'r';
}
}
diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php
index 03eb0b9edf..bc555a4fb4 100644
--- a/src/applications/diffusion/query/DiffusionCommitQuery.php
+++ b/src/applications/diffusion/query/DiffusionCommitQuery.php
@@ -1,975 +1,974 @@
<?php
final class DiffusionCommitQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $defaultRepository;
private $identifiers;
private $repositoryIDs;
private $repositoryPHIDs;
private $identifierMap;
private $responsiblePHIDs;
private $statuses;
private $packagePHIDs;
private $unreachable;
private $needAuditRequests;
private $needAuditAuthority;
private $auditIDs;
private $auditorPHIDs;
private $epochMin;
private $epochMax;
private $importing;
private $ancestorsOf;
private $needCommitData;
private $needDrafts;
private $needIdentities;
private $mustFilterRefs = false;
private $refRepository;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
/**
* Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234",
* or "a9caf12". When an identifier matches multiple commits, they will all
* be returned; callers should be prepared to deal with more results than
* they queried for.
*/
public function withIdentifiers(array $identifiers) {
// Some workflows (like blame lookups) can pass in large numbers of
// duplicate identifiers. We only care about unique identifiers, so
// get rid of duplicates immediately.
$identifiers = array_fuse($identifiers);
$this->identifiers = $identifiers;
return $this;
}
/**
* Look up commits in a specific repository. This is a shorthand for calling
* @{method:withDefaultRepository} and @{method:withRepositoryIDs}.
*/
public function withRepository(PhabricatorRepository $repository) {
$this->withDefaultRepository($repository);
$this->withRepositoryIDs(array($repository->getID()));
return $this;
}
/**
* Look up commits in a specific repository. Prefer
* @{method:withRepositoryIDs}; the underlying table is keyed by ID such
* that this method requires a separate initial query to map PHID to ID.
*/
public function withRepositoryPHIDs(array $phids) {
$this->repositoryPHIDs = $phids;
return $this;
}
/**
* If a default repository is provided, ambiguous commit identifiers will
* be assumed to belong to the default repository.
*
* For example, "r123" appearing in a commit message in repository X is
* likely to be unambiguously "rX123". Normally the reference would be
* considered ambiguous, but if you provide a default repository it will
* be correctly resolved.
*/
public function withDefaultRepository(PhabricatorRepository $repository) {
$this->defaultRepository = $repository;
return $this;
}
public function withRepositoryIDs(array $repository_ids) {
$this->repositoryIDs = array_unique($repository_ids);
return $this;
}
public function needCommitData($need) {
$this->needCommitData = $need;
return $this;
}
public function needDrafts($need) {
$this->needDrafts = $need;
return $this;
}
public function needIdentities($need) {
$this->needIdentities = $need;
return $this;
}
public function needAuditRequests($need) {
$this->needAuditRequests = $need;
return $this;
}
public function needAuditAuthority(array $users) {
assert_instances_of($users, 'PhabricatorUser');
$this->needAuditAuthority = $users;
return $this;
}
public function withAuditIDs(array $ids) {
$this->auditIDs = $ids;
return $this;
}
public function withAuditorPHIDs(array $auditor_phids) {
$this->auditorPHIDs = $auditor_phids;
return $this;
}
public function withResponsiblePHIDs(array $responsible_phids) {
$this->responsiblePHIDs = $responsible_phids;
return $this;
}
public function withPackagePHIDs(array $package_phids) {
$this->packagePHIDs = $package_phids;
return $this;
}
public function withUnreachable($unreachable) {
$this->unreachable = $unreachable;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withEpochRange($min, $max) {
$this->epochMin = $min;
$this->epochMax = $max;
return $this;
}
public function withImporting($importing) {
$this->importing = $importing;
return $this;
}
public function withAncestorsOf(array $refs) {
$this->ancestorsOf = $refs;
return $this;
}
public function getIdentifierMap() {
if ($this->identifierMap === null) {
throw new Exception(
pht(
'You must %s the query before accessing the identifier map.',
'execute()'));
}
return $this->identifierMap;
}
protected function getPrimaryTableAlias() {
return 'commit';
}
protected function willExecute() {
if ($this->identifierMap === null) {
$this->identifierMap = array();
}
}
public function newResultObject() {
return new PhabricatorRepositoryCommit();
}
protected function loadPage() {
$table = $this->newResultObject();
$conn = $table->establishConnection('r');
$empty_exception = null;
$subqueries = array();
if ($this->responsiblePHIDs) {
$base_authors = $this->authorPHIDs;
$base_auditors = $this->auditorPHIDs;
$responsible_phids = $this->responsiblePHIDs;
if ($base_authors) {
$all_authors = array_merge($base_authors, $responsible_phids);
} else {
$all_authors = $responsible_phids;
}
if ($base_auditors) {
$all_auditors = array_merge($base_auditors, $responsible_phids);
} else {
$all_auditors = $responsible_phids;
}
$this->authorPHIDs = $all_authors;
$this->auditorPHIDs = $base_auditors;
try {
$subqueries[] = $this->buildStandardPageQuery(
$conn,
$table->getTableName());
} catch (PhabricatorEmptyQueryException $ex) {
$empty_exception = $ex;
}
$this->authorPHIDs = $base_authors;
$this->auditorPHIDs = $all_auditors;
try {
$subqueries[] = $this->buildStandardPageQuery(
$conn,
$table->getTableName());
} catch (PhabricatorEmptyQueryException $ex) {
$empty_exception = $ex;
}
} else {
$subqueries[] = $this->buildStandardPageQuery(
$conn,
$table->getTableName());
}
if (!$subqueries) {
throw $empty_exception;
}
if (count($subqueries) > 1) {
$unions = null;
foreach ($subqueries as $subquery) {
if (!$unions) {
$unions = qsprintf(
$conn,
'(%Q)',
$subquery);
continue;
}
$unions = qsprintf(
$conn,
'%Q UNION DISTINCT (%Q)',
$unions,
$subquery);
}
$query = qsprintf(
$conn,
'%Q %Q %Q',
$unions,
$this->buildOrderClause($conn, true),
$this->buildLimitClause($conn));
} else {
$query = head($subqueries);
}
$rows = queryfx_all($conn, '%Q', $query);
$rows = $this->didLoadRawRows($rows);
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $commits) {
$repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID');
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIDs($repository_ids)
->execute();
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
$result = array();
foreach ($commits as $key => $commit) {
$repo = idx($repos, $commit->getRepositoryID());
if ($repo) {
$commit->attachRepository($repo);
} else {
$this->didRejectResult($commit);
unset($commits[$key]);
continue;
}
// Build the identifierMap
if ($this->identifiers !== null) {
$ids = $this->identifiers;
$prefixes = array(
'r'.$commit->getRepository()->getCallsign(),
'r'.$commit->getRepository()->getCallsign().':',
'R'.$commit->getRepository()->getID().':',
'', // No prefix is valid too and will only match the commitIdentifier
);
$suffix = $commit->getCommitIdentifier();
if ($commit->getRepository()->isSVN()) {
foreach ($prefixes as $prefix) {
if (isset($ids[$prefix.$suffix])) {
$result[$prefix.$suffix][] = $commit;
}
}
} else {
// This awkward construction is so we can link the commits up in O(N)
// time instead of O(N^2).
for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) {
$part = substr($suffix, 0, $ii);
foreach ($prefixes as $prefix) {
if (isset($ids[$prefix.$part])) {
$result[$prefix.$part][] = $commit;
}
}
}
}
}
}
if ($result) {
foreach ($result as $identifier => $matching_commits) {
if (count($matching_commits) == 1) {
$result[$identifier] = head($matching_commits);
} else {
// This reference is ambiguous (it matches more than one commit) so
// don't link it.
unset($result[$identifier]);
}
}
$this->identifierMap += $result;
}
return $commits;
}
protected function didFilterPage(array $commits) {
$viewer = $this->getViewer();
if ($this->mustFilterRefs) {
// If this flag is set, the query has an "Ancestors Of" constraint and
// at least one of the constraining refs had too many ancestors for us
// to apply the constraint with a big "commitIdentifier IN (%Ls)" clause.
// We're going to filter each page and hope we get a full result set
// before the query overheats.
$ancestor_list = mpull($commits, 'getCommitIdentifier');
$ancestor_list = array_values($ancestor_list);
foreach ($this->ancestorsOf as $ref) {
try {
$ancestor_list = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
DiffusionRequest::newFromDictionary(
array(
'repository' => $this->refRepository,
'user' => $viewer,
)),
'diffusion.internal.ancestors',
array(
'ref' => $ref,
'commits' => $ancestor_list,
));
} catch (ConduitClientException $ex) {
throw new PhabricatorSearchConstraintException(
$ex->getMessage());
}
if (!$ancestor_list) {
break;
}
}
$ancestor_list = array_fuse($ancestor_list);
foreach ($commits as $key => $commit) {
$identifier = $commit->getCommitIdentifier();
if (!isset($ancestor_list[$identifier])) {
$this->didRejectResult($commit);
unset($commits[$key]);
}
}
if (!$commits) {
return $commits;
}
}
if ($this->needCommitData) {
$data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$data = mpull($data, null, 'getCommitID');
foreach ($commits as $commit) {
$commit_data = idx($data, $commit->getID());
if (!$commit_data) {
$commit_data = new PhabricatorRepositoryCommitData();
}
$commit->attachCommitData($commit_data);
}
}
if ($this->needAuditRequests) {
$requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
'commitPHID IN (%Ls)',
mpull($commits, 'getPHID'));
$requests = mgroup($requests, 'getCommitPHID');
foreach ($commits as $commit) {
$audit_requests = idx($requests, $commit->getPHID(), array());
$commit->attachAudits($audit_requests);
foreach ($audit_requests as $audit_request) {
$audit_request->attachCommit($commit);
}
}
}
if ($this->needIdentities) {
$identity_phids = array_merge(
mpull($commits, 'getAuthorIdentityPHID'),
mpull($commits, 'getCommitterIdentityPHID'));
$data = id(new PhabricatorRepositoryIdentityQuery())
->withPHIDs($identity_phids)
->setViewer($this->getViewer())
->execute();
$data = mpull($data, null, 'getPHID');
foreach ($commits as $commit) {
$author_identity = idx($data, $commit->getAuthorIdentityPHID());
$committer_identity = idx($data, $commit->getCommitterIdentityPHID());
$commit->attachIdentities($author_identity, $committer_identity);
}
}
if ($this->needDrafts) {
PhabricatorDraftEngine::attachDrafts(
$viewer,
$commits);
}
if ($this->needAuditAuthority) {
$authority_users = $this->needAuditAuthority;
// NOTE: This isn't very efficient since we're running two queries per
// user, but there's currently no way to figure out authority for
// multiple users in one query. Today, we only ever request authority for
// a single user and single commit, so this has no practical impact.
// NOTE: We're querying with the viewership of query viewer, not the
// actual users. If the viewer can't see a project or package, they
// won't be able to see who has authority on it. This is safer than
// showing them true authority, and should never matter today, but it
// also doesn't seem like a significant disclosure and might be
// reasonable to adjust later if it causes something weird or confusing
// to happen.
$authority_map = array();
foreach ($authority_users as $authority_user) {
$authority_phid = $authority_user->getPHID();
if (!$authority_phid) {
continue;
}
$result_phids = array();
// Users have authority over themselves.
$result_phids[] = $authority_phid;
// Users have authority over packages they own.
$owned_packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withAuthorityPHIDs(array($authority_phid))
->execute();
foreach ($owned_packages as $package) {
$result_phids[] = $package->getPHID();
}
// Users have authority over projects they're members of.
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($authority_phid))
->execute();
foreach ($projects as $project) {
$result_phids[] = $project->getPHID();
}
$result_phids = array_fuse($result_phids);
foreach ($commits as $commit) {
$attach_phids = $result_phids;
// NOTE: When modifying your own commits, you act only on behalf of
// yourself, not your packages or projects. The idea here is that you
// can't accept your own commits. In the future, this might change or
// depend on configuration.
$author_phid = $commit->getAuthorPHID();
if ($author_phid == $authority_phid) {
$attach_phids = array($author_phid);
$attach_phids = array_fuse($attach_phids);
}
$commit->attachAuditAuthority($authority_user, $attach_phids);
}
}
}
return $commits;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->repositoryPHIDs !== null) {
$map_repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($this->repositoryPHIDs)
->execute();
if (!$map_repositories) {
throw new PhabricatorEmptyQueryException();
}
$repository_ids = mpull($map_repositories, 'getID');
if ($this->repositoryIDs !== null) {
$repository_ids = array_merge($repository_ids, $this->repositoryIDs);
}
$this->withRepositoryIDs($repository_ids);
}
if ($this->ancestorsOf !== null) {
if (count($this->repositoryIDs) !== 1) {
throw new PhabricatorSearchConstraintException(
pht(
'To search for commits which are ancestors of particular refs, '.
'you must constrain the search to exactly one repository.'));
}
$repository_id = head($this->repositoryIDs);
$history_limit = $this->getRawResultLimit() * 32;
$viewer = $this->getViewer();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIDs(array($repository_id))
->executeOne();
if (!$repository) {
throw new PhabricatorEmptyQueryException();
}
if ($repository->isSVN()) {
throw new PhabricatorSearchConstraintException(
pht(
'Subversion does not support searching for ancestors of '.
'a particular ref. This operation is not meaningful in '.
'Subversion.'));
}
if ($repository->isHg()) {
throw new PhabricatorSearchConstraintException(
pht(
'Mercurial does not currently support searching for ancestors of '.
'a particular ref.'));
}
$can_constrain = true;
$history_identifiers = array();
foreach ($this->ancestorsOf as $key => $ref) {
try {
$raw_history = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'user' => $viewer,
)),
'diffusion.historyquery',
array(
'commit' => $ref,
'limit' => $history_limit,
));
} catch (ConduitClientException $ex) {
throw new PhabricatorSearchConstraintException(
$ex->getMessage());
}
$ref_identifiers = array();
foreach ($raw_history['pathChanges'] as $change) {
$ref_identifiers[] = $change['commitIdentifier'];
}
// If this ref had fewer total commits than the limit, we're safe to
// apply the constraint as a large `IN (...)` query for a list of
// commit identifiers. This is efficient.
if ($history_limit) {
if (count($ref_identifiers) >= $history_limit) {
$can_constrain = false;
break;
}
}
$history_identifiers += array_fuse($ref_identifiers);
}
// If all refs had a small number of ancestors, we can just put the
// constraint into the query here and we're done. Otherwise, we need
// to filter each page after it comes out of the MySQL layer.
if ($can_constrain) {
$where[] = qsprintf(
$conn,
'commit.commitIdentifier IN (%Ls)',
$history_identifiers);
} else {
$this->mustFilterRefs = true;
$this->refRepository = $repository;
}
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'commit.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'commit.phid IN (%Ls)',
$this->phids);
}
if ($this->repositoryIDs !== null) {
$where[] = qsprintf(
$conn,
'commit.repositoryID IN (%Ld)',
$this->repositoryIDs);
}
if ($this->authorPHIDs !== null) {
$author_phids = $this->authorPHIDs;
if ($author_phids) {
$author_phids = $this->selectPossibleAuthors($author_phids);
if (!$author_phids) {
throw new PhabricatorEmptyQueryException(
pht('Author PHIDs contain no possible authors.'));
}
}
$where[] = qsprintf(
$conn,
'commit.authorPHID IN (%Ls)',
$author_phids);
}
if ($this->epochMin !== null) {
$where[] = qsprintf(
$conn,
'commit.epoch >= %d',
$this->epochMin);
}
if ($this->epochMax !== null) {
$where[] = qsprintf(
$conn,
'commit.epoch <= %d',
$this->epochMax);
}
if ($this->importing !== null) {
if ($this->importing) {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) != %d',
PhabricatorRepositoryCommit::IMPORTED_ALL,
PhabricatorRepositoryCommit::IMPORTED_ALL);
} else {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = %d',
PhabricatorRepositoryCommit::IMPORTED_ALL,
PhabricatorRepositoryCommit::IMPORTED_ALL);
}
}
if ($this->identifiers !== null) {
$min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
$refs = array();
$bare = array();
foreach ($this->identifiers as $identifier) {
$matches = null;
preg_match('/^(?:[rR]([A-Z]+:?|[0-9]+:))?(.*)$/',
$identifier, $matches);
$repo = nonempty(rtrim($matches[1], ':'), null);
$commit_identifier = nonempty($matches[2], null);
if ($repo === null) {
if ($this->defaultRepository) {
$repo = $this->defaultRepository->getPHID();
}
}
if ($repo === null) {
if (strlen($commit_identifier) < $min_unqualified) {
continue;
}
$bare[] = $commit_identifier;
} else {
$refs[] = array(
'repository' => $repo,
'identifier' => $commit_identifier,
);
}
}
$sql = array();
foreach ($bare as $identifier) {
$sql[] = qsprintf(
$conn,
'(commit.commitIdentifier LIKE %> AND '.
'LENGTH(commit.commitIdentifier) = 40)',
$identifier);
}
if ($refs) {
$repositories = ipull($refs, 'repository');
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIdentifiers($repositories);
$repos->execute();
$repos = $repos->getIdentifierMap();
foreach ($refs as $key => $ref) {
$repo = idx($repos, $ref['repository']);
if (!$repo) {
continue;
}
if ($repo->isSVN()) {
if (!ctype_digit((string)$ref['identifier'])) {
continue;
}
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d AND commit.commitIdentifier = %s)',
$repo->getID(),
// NOTE: Because the 'commitIdentifier' column is a string, MySQL
// ignores the index if we hand it an integer. Hand it a string.
// See T3377.
(int)$ref['identifier']);
} else {
if (strlen($ref['identifier']) < $min_qualified) {
continue;
}
$identifier = $ref['identifier'];
if (strlen($identifier) == 40) {
// MySQL seems to do slightly better with this version if the
// clause, so issue it if we have a full commit hash.
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d
AND commit.commitIdentifier = %s)',
$repo->getID(),
$identifier);
} else {
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d
AND commit.commitIdentifier LIKE %>)',
$repo->getID(),
$identifier);
}
}
}
}
if (!$sql) {
// If we discarded all possible identifiers (e.g., they all referenced
// bogus repositories or were all too short), make sure the query finds
// nothing.
throw new PhabricatorEmptyQueryException(
pht('No commit identifiers.'));
}
$where[] = qsprintf($conn, '%LO', $sql);
}
if ($this->auditIDs !== null) {
$where[] = qsprintf(
$conn,
'auditor.id IN (%Ld)',
$this->auditIDs);
}
if ($this->auditorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'auditor.auditorPHID IN (%Ls)',
$this->auditorPHIDs);
}
if ($this->statuses !== null) {
$statuses = DiffusionCommitAuditStatus::newModernKeys(
$this->statuses);
$where[] = qsprintf(
$conn,
'commit.auditStatus IN (%Ls)',
$statuses);
}
if ($this->packagePHIDs !== null) {
$where[] = qsprintf(
$conn,
'package.dst IN (%Ls)',
$this->packagePHIDs);
}
if ($this->unreachable !== null) {
if ($this->unreachable) {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = %d',
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE,
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE);
} else {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = 0',
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE);
}
}
return $where;
}
protected function didFilterResults(array $filtered) {
if ($this->identifierMap) {
foreach ($this->identifierMap as $name => $commit) {
if (isset($filtered[$commit->getPHID()])) {
unset($this->identifierMap[$name]);
}
}
}
}
private function shouldJoinAuditor() {
return ($this->auditIDs || $this->auditorPHIDs);
}
private function shouldJoinOwners() {
return (bool)$this->packagePHIDs;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$join = parent::buildJoinClauseParts($conn);
$audit_request = new PhabricatorRepositoryAuditRequest();
if ($this->shouldJoinAuditor()) {
$join[] = qsprintf(
$conn,
'JOIN %T auditor ON commit.phid = auditor.commitPHID',
$audit_request->getTableName());
}
if ($this->shouldJoinOwners()) {
$join[] = qsprintf(
$conn,
'JOIN %T package ON commit.phid = package.src
AND package.type = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
DiffusionCommitHasPackageEdgeType::EDGECONST);
}
return $join;
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinAuditor()) {
return true;
}
if ($this->shouldJoinOwners()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'epoch' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'epoch',
'type' => 'int',
'reverse' => false,
),
);
}
- 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(),
);
}
public function getBuiltinOrders() {
$parent = parent::getBuiltinOrders();
// Rename the default ID-based orders.
$parent['importnew'] = array(
'name' => pht('Import Date (Newest First)'),
) + $parent['newest'];
$parent['importold'] = array(
'name' => pht('Import Date (Oldest First)'),
) + $parent['oldest'];
return array(
'newest' => array(
'vector' => array('epoch', 'id'),
'name' => pht('Commit Date (Newest First)'),
),
'oldest' => array(
'vector' => array('-epoch', '-id'),
'name' => pht('Commit Date (Oldest First)'),
),
) + $parent;
}
private function selectPossibleAuthors(array $phids) {
// See PHI1057. Select PHIDs which might possibly be commit authors from
// a larger list of PHIDs. This primarily filters out packages and projects
// from "Responsible Users: ..." queries. Our goal in performing this
// filtering is to improve the performance of the final query.
foreach ($phids as $key => $phid) {
if (phid_get_type($phid) !== PhabricatorPeopleUserPHIDType::TYPECONST) {
unset($phids[$key]);
}
}
return $phids;
}
}
diff --git a/src/applications/diviner/query/DivinerBookQuery.php b/src/applications/diviner/query/DivinerBookQuery.php
index d540d971b0..2d6527ec96 100644
--- a/src/applications/diviner/query/DivinerBookQuery.php
+++ b/src/applications/diviner/query/DivinerBookQuery.php
@@ -1,201 +1,200 @@
<?php
final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $names;
private $nameLike;
private $namePrefix;
private $repositoryPHIDs;
private $needProjectPHIDs;
private $needRepositories;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNameLike($name) {
$this->nameLike = $name;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
public function needProjectPHIDs($need_phids) {
$this->needProjectPHIDs = $need_phids;
return $this;
}
public function needRepositories($need_repositories) {
$this->needRepositories = $need_repositories;
return $this;
}
protected function loadPage() {
$table = new DivinerLiveBook();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function didFilterPage(array $books) {
assert_instances_of($books, 'DivinerLiveBook');
if ($this->needRepositories) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($books, 'getRepositoryPHID'))
->execute();
$repositories = mpull($repositories, null, 'getPHID');
foreach ($books as $key => $book) {
if ($book->getRepositoryPHID() === null) {
$book->attachRepository(null);
continue;
}
$repository = idx($repositories, $book->getRepositoryPHID());
if (!$repository) {
$this->didRejectResult($book);
unset($books[$key]);
continue;
}
$book->attachRepository($repository);
}
}
if ($this->needProjectPHIDs) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($books, 'getPHID'))
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($books as $book) {
$project_phids = $edge_query->getDestinationPHIDs(
array(
$book->getPHID(),
));
$book->attachProjectPHIDs($project_phids);
}
}
return $books;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if (strlen($this->nameLike)) {
$where[] = qsprintf(
$conn,
'name LIKE %~',
$this->nameLike);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if (strlen($this->namePrefix)) {
$where[] = qsprintf(
$conn,
'name LIKE %>',
$this->namePrefix);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
}
public function getQueryApplicationClass() {
return 'PhabricatorDivinerApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'column' => 'name',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
- 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(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php
index 4058325140..c903fbb37f 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php
@@ -1,153 +1,152 @@
<?php
final class HarbormasterBuildPlanQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $statuses;
private $datasourceQuery;
private $planAutoKeys;
private $needBuildSteps;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withDatasourceQuery($query) {
$this->datasourceQuery = $query;
return $this;
}
public function withPlanAutoKeys(array $keys) {
$this->planAutoKeys = $keys;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new HarbormasterBuildPlanNameNgrams(),
$ngrams);
}
public function needBuildSteps($need) {
$this->needBuildSteps = $need;
return $this;
}
public function newResultObject() {
return new HarbormasterBuildPlan();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function didFilterPage(array $page) {
if ($this->needBuildSteps) {
$plan_phids = mpull($page, 'getPHID');
$steps = id(new HarbormasterBuildStepQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withBuildPlanPHIDs($plan_phids)
->execute();
$steps = mgroup($steps, 'getBuildPlanPHID');
foreach ($page as $plan) {
$plan_steps = idx($steps, $plan->getPHID(), array());
$plan->attachBuildSteps($plan_steps);
}
}
return $page;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'plan.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'plan.phid IN (%Ls)',
$this->phids);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'plan.planStatus IN (%Ls)',
$this->statuses);
}
if (strlen($this->datasourceQuery)) {
$where[] = qsprintf(
$conn,
'plan.name LIKE %>',
$this->datasourceQuery);
}
if ($this->planAutoKeys !== null) {
$where[] = qsprintf(
$conn,
'plan.planAutoKey IN (%Ls)',
$this->planAutoKeys);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'plan';
}
public function getQueryApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'column' => 'name',
'type' => 'string',
'reverse' => true,
),
);
}
- 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(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name', 'id'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/macro/query/PhabricatorMacroQuery.php b/src/applications/macro/query/PhabricatorMacroQuery.php
index 3ba30502d5..7635b68b73 100644
--- a/src/applications/macro/query/PhabricatorMacroQuery.php
+++ b/src/applications/macro/query/PhabricatorMacroQuery.php
@@ -1,269 +1,268 @@
<?php
final class PhabricatorMacroQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $names;
private $nameLike;
private $namePrefix;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $flagColor;
private $needFiles;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_ACTIVE = 'status-active';
const STATUS_DISABLED = 'status-disabled';
public static function getStatusOptions() {
return array(
self::STATUS_ACTIVE => pht('Active Macros'),
self::STATUS_DISABLED => pht('Disabled Macros'),
self::STATUS_ANY => pht('Active and Disabled Macros'),
);
}
public static function getFlagColorsOptions() {
$options = array(
'-1' => pht('(No Filtering)'),
'-2' => pht('(Marked With Any Flag)'),
);
foreach (PhabricatorFlagColor::getColorNameMap() as $color => $name) {
$options[$color] = $name;
}
return $options;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withNameLike($name) {
$this->nameLike = $name;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withFlagColor($flag_color) {
$this->flagColor = $flag_color;
return $this;
}
public function needFiles($need_files) {
$this->needFiles = $need_files;
return $this;
}
public function newResultObject() {
return new PhabricatorFileImageMacro();
}
protected function loadPage() {
return $this->loadStandardPage(new PhabricatorFileImageMacro());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'm.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'm.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'm.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if (strlen($this->nameLike)) {
$where[] = qsprintf(
$conn,
'm.name LIKE %~',
$this->nameLike);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'm.name IN (%Ls)',
$this->names);
}
if (strlen($this->namePrefix)) {
$where[] = qsprintf(
$conn,
'm.name LIKE %>',
$this->namePrefix);
}
switch ($this->status) {
case self::STATUS_ACTIVE:
$where[] = qsprintf(
$conn,
'm.isDisabled = 0');
break;
case self::STATUS_DISABLED:
$where[] = qsprintf(
$conn,
'm.isDisabled = 1');
break;
case self::STATUS_ANY:
break;
default:
throw new Exception(pht("Unknown status '%s'!", $this->status));
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'm.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'm.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->flagColor != '-1' && $this->flagColor !== null) {
if ($this->flagColor == '-2') {
$flag_colors = array_keys(PhabricatorFlagColor::getColorNameMap());
} else {
$flag_colors = array($this->flagColor);
}
$flags = id(new PhabricatorFlagQuery())
->withOwnerPHIDs(array($this->getViewer()->getPHID()))
->withTypes(array(PhabricatorMacroMacroPHIDType::TYPECONST))
->withColors($flag_colors)
->setViewer($this->getViewer())
->execute();
if (empty($flags)) {
throw new PhabricatorEmptyQueryException(pht('No matching flags.'));
} else {
$where[] = qsprintf(
$conn,
'm.phid IN (%Ls)',
mpull($flags, 'getObjectPHID'));
}
}
return $where;
}
protected function didFilterPage(array $macros) {
if ($this->needFiles) {
$file_phids = mpull($macros, 'getFilePHID');
$files = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($macros as $key => $macro) {
$file = idx($files, $macro->getFilePHID());
if (!$file) {
unset($macros[$key]);
continue;
}
$macro->attachFile($file);
}
}
return $macros;
}
protected function getPrimaryTableAlias() {
return 'm';
}
public function getQueryApplicationClass() {
return 'PhabricatorMacroApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => 'm',
'column' => 'name',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
- 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(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
index 6d6ccb2ed2..67b4836a5a 100644
--- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
+++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
@@ -1,452 +1,451 @@
<?php
final class PhabricatorOwnersPackageQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $ownerPHIDs;
private $authorityPHIDs;
private $repositoryPHIDs;
private $paths;
private $statuses;
private $controlMap = array();
private $controlResults;
private $needPaths;
/**
* Query owner PHIDs exactly. This does not expand authorities, so a user
* PHID will not match projects the user is a member of.
*/
public function withOwnerPHIDs(array $phids) {
$this->ownerPHIDs = $phids;
return $this;
}
/**
* Query owner authority. This will expand authorities, so a user PHID will
* match both packages they own directly and packages owned by a project they
* are a member of.
*/
public function withAuthorityPHIDs(array $phids) {
$this->authorityPHIDs = $phids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withRepositoryPHIDs(array $phids) {
$this->repositoryPHIDs = $phids;
return $this;
}
public function withPaths(array $paths) {
$this->paths = $paths;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withControl($repository_phid, array $paths) {
if (empty($this->controlMap[$repository_phid])) {
$this->controlMap[$repository_phid] = array();
}
foreach ($paths as $path) {
$path = (string)$path;
$this->controlMap[$repository_phid][$path] = $path;
}
// We need to load paths to execute control queries.
$this->needPaths = true;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new PhabricatorOwnersPackageNameNgrams(),
$ngrams);
}
public function needPaths($need_paths) {
$this->needPaths = $need_paths;
return $this;
}
public function newResultObject() {
return new PhabricatorOwnersPackage();
}
protected function willExecute() {
$this->controlResults = array();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $packages) {
$package_ids = mpull($packages, 'getID');
$owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID IN (%Ld)',
$package_ids);
$owners = mgroup($owners, 'getPackageID');
foreach ($packages as $package) {
$package->attachOwners(idx($owners, $package->getID(), array()));
}
return $packages;
}
protected function didFilterPage(array $packages) {
$package_ids = mpull($packages, 'getID');
if ($this->needPaths) {
$paths = id(new PhabricatorOwnersPath())->loadAllWhere(
'packageID IN (%Ld)',
$package_ids);
$paths = mgroup($paths, 'getPackageID');
foreach ($packages as $package) {
$package->attachPaths(idx($paths, $package->getID(), array()));
}
}
if ($this->controlMap) {
foreach ($packages as $package) {
// If this package is archived, it's no longer a controlling package
// for any path. In particular, it can not force active packages with
// weak dominion to give up control.
if ($package->isArchived()) {
continue;
}
$this->controlResults[$package->getID()] = $package;
}
}
return $packages;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinOwnersTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T o ON o.packageID = p.id',
id(new PhabricatorOwnersOwner())->getTableName());
}
if ($this->shouldJoinPathTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T rpath ON rpath.packageID = p.id',
id(new PhabricatorOwnersPath())->getTableName());
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'p.phid IN (%Ls)',
$this->phids);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'p.id IN (%Ld)',
$this->ids);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'rpath.repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->authorityPHIDs !== null) {
$authority_phids = $this->expandAuthority($this->authorityPHIDs);
$where[] = qsprintf(
$conn,
'o.userPHID IN (%Ls)',
$authority_phids);
}
if ($this->ownerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'o.userPHID IN (%Ls)',
$this->ownerPHIDs);
}
if ($this->paths !== null) {
$where[] = qsprintf(
$conn,
'rpath.pathIndex IN (%Ls)',
$this->getFragmentIndexesForPaths($this->paths));
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'p.status IN (%Ls)',
$this->statuses);
}
if ($this->controlMap) {
$clauses = array();
foreach ($this->controlMap as $repository_phid => $paths) {
$indexes = $this->getFragmentIndexesForPaths($paths);
$clauses[] = qsprintf(
$conn,
'(rpath.repositoryPHID = %s AND rpath.pathIndex IN (%Ls))',
$repository_phid,
$indexes);
}
$where[] = qsprintf($conn, '%LO', $clauses);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinOwnersTable()) {
return true;
}
if ($this->shouldJoinPathTable()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
- 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(),
);
}
public function getQueryApplicationClass() {
return 'PhabricatorOwnersApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function shouldJoinOwnersTable() {
if ($this->ownerPHIDs !== null) {
return true;
}
if ($this->authorityPHIDs !== null) {
return true;
}
return false;
}
private function shouldJoinPathTable() {
if ($this->repositoryPHIDs !== null) {
return true;
}
if ($this->paths !== null) {
return true;
}
if ($this->controlMap) {
return true;
}
return false;
}
private function expandAuthority(array $phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs($phids)
->execute();
$project_phids = mpull($projects, 'getPHID');
return array_fuse($phids) + array_fuse($project_phids);
}
private function getFragmentsForPaths(array $paths) {
$fragments = array();
foreach ($paths as $path) {
foreach (PhabricatorOwnersPackage::splitPath($path) as $fragment) {
$fragments[$fragment] = $fragment;
}
}
return $fragments;
}
private function getFragmentIndexesForPaths(array $paths) {
$indexes = array();
foreach ($this->getFragmentsForPaths($paths) as $fragment) {
$indexes[] = PhabricatorHash::digestForIndex($fragment);
}
return $indexes;
}
/* -( Path Control )------------------------------------------------------- */
/**
* Get a list of all packages which control a path or its parent directories,
* ordered from weakest to strongest.
*
* The first package has the most specific claim on the path; the last
* package has the most general claim. Multiple packages may have claims of
* equal strength, so this ordering is primarily one of usability and
* convenience.
*
* @return list<PhabricatorOwnersPackage> List of controlling packages.
*/
public function getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = false) {
$path = (string)$path;
if (!isset($this->controlMap[$repository_phid][$path])) {
throw new PhutilInvalidStateException('withControl');
}
if ($this->controlResults === null) {
throw new PhutilInvalidStateException('execute');
}
$packages = $this->controlResults;
$weak_dominion = PhabricatorOwnersPackage::DOMINION_WEAK;
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
$matches = array();
foreach ($packages as $package_id => $package) {
$best_match = null;
$include = false;
$repository_paths = $package->getPathsForRepository($repository_phid);
foreach ($repository_paths as $package_path) {
$strength = $package_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if ($strength > $best_match) {
$best_match = $strength;
$include = !$package_path->getExcluded();
}
}
if ($best_match && $include) {
if ($ignore_dominion) {
$is_weak = false;
} else {
$is_weak = ($package->getDominion() == $weak_dominion);
}
$matches[$package_id] = array(
'strength' => $best_match,
'weak' => $is_weak,
'package' => $package,
);
}
}
// At each strength level, drop weak packages if there are also strong
// packages of the same strength.
$strength_map = igroup($matches, 'strength');
foreach ($strength_map as $strength => $package_list) {
$any_strong = false;
foreach ($package_list as $package_id => $package) {
if (!$package['weak']) {
$any_strong = true;
break;
}
}
if ($any_strong) {
foreach ($package_list as $package_id => $package) {
if ($package['weak']) {
unset($matches[$package_id]);
}
}
}
}
$matches = isort($matches, 'strength');
$matches = array_reverse($matches);
$strongest = null;
foreach ($matches as $package_id => $match) {
if ($strongest === null) {
$strongest = $match['strength'];
}
if ($match['strength'] === $strongest) {
continue;
}
if ($match['weak']) {
unset($matches[$package_id]);
}
}
return array_values(ipull($matches, 'package'));
}
}
diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php
index 542b685e29..5e737aaf90 100644
--- a/src/applications/people/query/PhabricatorPeopleQuery.php
+++ b/src/applications/people/query/PhabricatorPeopleQuery.php
@@ -1,635 +1,634 @@
<?php
final class PhabricatorPeopleQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $usernames;
private $realnames;
private $emails;
private $phids;
private $ids;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $isAdmin;
private $isSystemAgent;
private $isMailingList;
private $isDisabled;
private $isApproved;
private $nameLike;
private $nameTokens;
private $namePrefixes;
private $isEnrolledInMultiFactor;
private $needPrimaryEmail;
private $needProfile;
private $needProfileImage;
private $needAvailability;
private $needBadgeAwards;
private $cacheKeys = array();
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withEmails(array $emails) {
$this->emails = $emails;
return $this;
}
public function withRealnames(array $realnames) {
$this->realnames = $realnames;
return $this;
}
public function withUsernames(array $usernames) {
$this->usernames = $usernames;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withIsAdmin($admin) {
$this->isAdmin = $admin;
return $this;
}
public function withIsSystemAgent($system_agent) {
$this->isSystemAgent = $system_agent;
return $this;
}
public function withIsMailingList($mailing_list) {
$this->isMailingList = $mailing_list;
return $this;
}
public function withIsDisabled($disabled) {
$this->isDisabled = $disabled;
return $this;
}
public function withIsApproved($approved) {
$this->isApproved = $approved;
return $this;
}
public function withNameLike($like) {
$this->nameLike = $like;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withNamePrefixes(array $prefixes) {
$this->namePrefixes = $prefixes;
return $this;
}
public function withIsEnrolledInMultiFactor($enrolled) {
$this->isEnrolledInMultiFactor = $enrolled;
return $this;
}
public function needPrimaryEmail($need) {
$this->needPrimaryEmail = $need;
return $this;
}
public function needProfile($need) {
$this->needProfile = $need;
return $this;
}
public function needProfileImage($need) {
$cache_key = PhabricatorUserProfileImageCacheType::KEY_URI;
if ($need) {
$this->cacheKeys[$cache_key] = true;
} else {
unset($this->cacheKeys[$cache_key]);
}
return $this;
}
public function needAvailability($need) {
$this->needAvailability = $need;
return $this;
}
public function needUserSettings($need) {
$cache_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
if ($need) {
$this->cacheKeys[$cache_key] = true;
} else {
unset($this->cacheKeys[$cache_key]);
}
return $this;
}
public function needBadgeAwards($need) {
$cache_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
if ($need) {
$this->cacheKeys[$cache_key] = true;
} else {
unset($this->cacheKeys[$cache_key]);
}
return $this;
}
public function newResultObject() {
return new PhabricatorUser();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function didFilterPage(array $users) {
if ($this->needProfile) {
$user_list = mpull($users, null, 'getPHID');
$profiles = new PhabricatorUserProfile();
$profiles = $profiles->loadAllWhere(
'userPHID IN (%Ls)',
array_keys($user_list));
$profiles = mpull($profiles, null, 'getUserPHID');
foreach ($user_list as $user_phid => $user) {
$profile = idx($profiles, $user_phid);
if (!$profile) {
$profile = PhabricatorUserProfile::initializeNewProfile($user);
}
$user->attachUserProfile($profile);
}
}
if ($this->needAvailability) {
$rebuild = array();
foreach ($users as $user) {
$cache = $user->getAvailabilityCache();
if ($cache !== null) {
$user->attachAvailability($cache);
} else {
$rebuild[] = $user;
}
}
if ($rebuild) {
$this->rebuildAvailabilityCache($rebuild);
}
}
$this->fillUserCaches($users);
return $users;
}
protected function shouldGroupQueryResultRows() {
if ($this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->emails) {
$email_table = new PhabricatorUserEmail();
$joins[] = qsprintf(
$conn,
'JOIN %T email ON email.userPHID = user.PHID',
$email_table->getTableName());
}
if ($this->nameTokens) {
foreach ($this->nameTokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',
PhabricatorUser::NAMETOKEN_TABLE,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->usernames !== null) {
$where[] = qsprintf(
$conn,
'user.userName IN (%Ls)',
$this->usernames);
}
if ($this->namePrefixes) {
$parts = array();
foreach ($this->namePrefixes as $name_prefix) {
$parts[] = qsprintf(
$conn,
'user.username LIKE %>',
$name_prefix);
}
$where[] = qsprintf($conn, '%LO', $parts);
}
if ($this->emails !== null) {
$where[] = qsprintf(
$conn,
'email.address IN (%Ls)',
$this->emails);
}
if ($this->realnames !== null) {
$where[] = qsprintf(
$conn,
'user.realName IN (%Ls)',
$this->realnames);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'user.phid IN (%Ls)',
$this->phids);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'user.id IN (%Ld)',
$this->ids);
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'user.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'user.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->isAdmin !== null) {
$where[] = qsprintf(
$conn,
'user.isAdmin = %d',
(int)$this->isAdmin);
}
if ($this->isDisabled !== null) {
$where[] = qsprintf(
$conn,
'user.isDisabled = %d',
(int)$this->isDisabled);
}
if ($this->isApproved !== null) {
$where[] = qsprintf(
$conn,
'user.isApproved = %d',
(int)$this->isApproved);
}
if ($this->isSystemAgent !== null) {
$where[] = qsprintf(
$conn,
'user.isSystemAgent = %d',
(int)$this->isSystemAgent);
}
if ($this->isMailingList !== null) {
$where[] = qsprintf(
$conn,
'user.isMailingList = %d',
(int)$this->isMailingList);
}
if (strlen($this->nameLike)) {
$where[] = qsprintf(
$conn,
'user.username LIKE %~ OR user.realname LIKE %~',
$this->nameLike,
$this->nameLike);
}
if ($this->isEnrolledInMultiFactor !== null) {
$where[] = qsprintf(
$conn,
'user.isEnrolledInMultiFactor = %d',
(int)$this->isEnrolledInMultiFactor);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'user';
}
public function getQueryApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'username' => array(
'table' => 'user',
'column' => 'username',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
- 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(),
);
}
private function rebuildAvailabilityCache(array $rebuild) {
$rebuild = mpull($rebuild, null, 'getPHID');
// Limit the window we look at because far-future events are largely
// irrelevant and this makes the cache cheaper to build and allows it to
// self-heal over time.
$min_range = PhabricatorTime::getNow();
$max_range = $min_range + phutil_units('72 hours in seconds');
// NOTE: We don't need to generate ghosts here, because we only care if
// the user is attending, and you can't attend a ghost event: RSVP'ing
// to it creates a real event.
$events = id(new PhabricatorCalendarEventQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withInvitedPHIDs(array_keys($rebuild))
->withIsCancelled(false)
->withDateRange($min_range, $max_range)
->execute();
// Group all the events by invited user. Only examine events that users
// are actually attending.
$map = array();
$invitee_map = array();
foreach ($events as $event) {
foreach ($event->getInvitees() as $invitee) {
if (!$invitee->isAttending()) {
continue;
}
// If the user is set to "Available" for this event, don't consider it
// when computing their away status.
if (!$invitee->getDisplayAvailability($event)) {
continue;
}
$invitee_phid = $invitee->getInviteePHID();
if (!isset($rebuild[$invitee_phid])) {
continue;
}
$map[$invitee_phid][] = $event;
$event_phid = $event->getPHID();
$invitee_map[$invitee_phid][$event_phid] = $invitee;
}
}
// We need to load these users' timezone settings to figure out their
// availability if they're attending all-day events.
$this->needUserSettings(true);
$this->fillUserCaches($rebuild);
foreach ($rebuild as $phid => $user) {
$events = idx($map, $phid, array());
// We loaded events with the omnipotent user, but want to shift them
// into the user's timezone before building the cache because they will
// be unavailable during their own local day.
foreach ($events as $event) {
$event->applyViewerTimezone($user);
}
$cursor = $min_range;
$next_event = null;
if ($events) {
// Find the next time when the user has no meetings. If we move forward
// because of an event, we check again for events after that one ends.
while (true) {
foreach ($events as $event) {
$from = $event->getStartDateTimeEpochForCache();
$to = $event->getEndDateTimeEpochForCache();
if (($from <= $cursor) && ($to > $cursor)) {
$cursor = $to;
if (!$next_event) {
$next_event = $event;
}
continue 2;
}
}
break;
}
}
if ($cursor > $min_range) {
$invitee = $invitee_map[$phid][$next_event->getPHID()];
$availability_type = $invitee->getDisplayAvailability($next_event);
$availability = array(
'until' => $cursor,
'eventPHID' => $next_event->getPHID(),
'availability' => $availability_type,
);
// We only cache this availability until the end of the current event,
// since the event PHID (and possibly the availability type) are only
// valid for that long.
// NOTE: This doesn't handle overlapping events with the greatest
// possible care. In theory, if you're attending multiple events
// simultaneously we should accommodate that. However, it's complex
// to compute, rare, and probably not confusing most of the time.
$availability_ttl = $next_event->getEndDateTimeEpochForCache();
} else {
$availability = array(
'until' => null,
'eventPHID' => null,
'availability' => null,
);
// Cache that the user is available until the next event they are
// invited to starts.
$availability_ttl = $max_range;
foreach ($events as $event) {
$from = $event->getStartDateTimeEpochForCache();
if ($from > $cursor) {
$availability_ttl = min($from, $availability_ttl);
}
}
}
// Never TTL the cache to longer than the maximum range we examined.
$availability_ttl = min($availability_ttl, $max_range);
$user->writeAvailabilityCache($availability, $availability_ttl);
$user->attachAvailability($availability);
}
}
private function fillUserCaches(array $users) {
if (!$this->cacheKeys) {
return;
}
$user_map = mpull($users, null, 'getPHID');
$keys = array_keys($this->cacheKeys);
$hashes = array();
foreach ($keys as $key) {
$hashes[] = PhabricatorHash::digestForIndex($key);
}
$types = PhabricatorUserCacheType::getAllCacheTypes();
// First, pull any available caches. If we wanted to be particularly clever
// we could do this with JOINs in the main query.
$cache_table = new PhabricatorUserCache();
$cache_conn = $cache_table->establishConnection('r');
$cache_data = queryfx_all(
$cache_conn,
'SELECT cacheKey, userPHID, cacheData, cacheType FROM %T
WHERE cacheIndex IN (%Ls) AND userPHID IN (%Ls)',
$cache_table->getTableName(),
$hashes,
array_keys($user_map));
$skip_validation = array();
// After we read caches from the database, discard any which have data that
// invalid or out of date. This allows cache types to implement TTLs or
// versions instead of or in addition to explicit cache clears.
foreach ($cache_data as $row_key => $row) {
$cache_type = $row['cacheType'];
if (isset($skip_validation[$cache_type])) {
continue;
}
if (empty($types[$cache_type])) {
unset($cache_data[$row_key]);
continue;
}
$type = $types[$cache_type];
if (!$type->shouldValidateRawCacheData()) {
$skip_validation[$cache_type] = true;
continue;
}
$user = $user_map[$row['userPHID']];
$raw_data = $row['cacheData'];
if (!$type->isRawCacheDataValid($user, $row['cacheKey'], $raw_data)) {
unset($cache_data[$row_key]);
continue;
}
}
$need = array();
$cache_data = igroup($cache_data, 'userPHID');
foreach ($user_map as $user_phid => $user) {
$raw_rows = idx($cache_data, $user_phid, array());
$raw_data = ipull($raw_rows, 'cacheData', 'cacheKey');
foreach ($keys as $key) {
if (isset($raw_data[$key]) || array_key_exists($key, $raw_data)) {
continue;
}
$need[$key][$user_phid] = $user;
}
$user->attachRawCacheData($raw_data);
}
// If we missed any cache values, bulk-construct them now. This is
// usually much cheaper than generating them on-demand for each user
// record.
if (!$need) {
return;
}
$writes = array();
foreach ($need as $cache_key => $need_users) {
$type = PhabricatorUserCacheType::getCacheTypeForKey($cache_key);
if (!$type) {
continue;
}
$data = $type->newValueForUsers($cache_key, $need_users);
foreach ($data as $user_phid => $raw_value) {
$data[$user_phid] = $raw_value;
$writes[] = array(
'userPHID' => $user_phid,
'key' => $cache_key,
'type' => $type,
'value' => $raw_value,
);
}
foreach ($need_users as $user_phid => $user) {
if (isset($data[$user_phid]) || array_key_exists($user_phid, $data)) {
$user->attachRawCacheData(
array(
$cache_key => $data[$user_phid],
));
}
}
}
PhabricatorUserCache::writeCaches($writes);
}
}
diff --git a/src/applications/phame/query/PhamePostQuery.php b/src/applications/phame/query/PhamePostQuery.php
index 85ef470cea..d7396e553f 100644
--- a/src/applications/phame/query/PhamePostQuery.php
+++ b/src/applications/phame/query/PhamePostQuery.php
@@ -1,194 +1,190 @@
<?php
final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $blogPHIDs;
private $bloggerPHIDs;
private $visibility;
private $publishedAfter;
private $phids;
private $needHeaderImage;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBloggerPHIDs(array $blogger_phids) {
$this->bloggerPHIDs = $blogger_phids;
return $this;
}
public function withBlogPHIDs(array $blog_phids) {
$this->blogPHIDs = $blog_phids;
return $this;
}
public function withVisibility(array $visibility) {
$this->visibility = $visibility;
return $this;
}
public function withPublishedAfter($time) {
$this->publishedAfter = $time;
return $this;
}
public function needHeaderImage($need) {
$this->needHeaderImage = $need;
return $this;
}
public function newResultObject() {
return new PhamePost();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $posts) {
// We require blogs to do visibility checks, so load them unconditionally.
$blog_phids = mpull($posts, 'getBlogPHID');
$blogs = id(new PhameBlogQuery())
->setViewer($this->getViewer())
->needProfileImage(true)
->withPHIDs($blog_phids)
->execute();
$blogs = mpull($blogs, null, 'getPHID');
foreach ($posts as $key => $post) {
$blog_phid = $post->getBlogPHID();
$blog = idx($blogs, $blog_phid);
if (!$blog) {
$this->didRejectResult($post);
unset($posts[$key]);
continue;
}
$post->attachBlog($blog);
}
if ($this->needHeaderImage) {
$file_phids = mpull($posts, 'getHeaderImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($posts as $post) {
$file = idx($files, $post->getHeaderImagePHID());
if ($file) {
$post->attachHeaderImageFile($file);
}
}
}
return $posts;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'p.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'p.phid IN (%Ls)',
$this->phids);
}
if ($this->bloggerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'p.bloggerPHID IN (%Ls)',
$this->bloggerPHIDs);
}
if ($this->visibility !== null) {
$where[] = qsprintf(
$conn,
'p.visibility IN (%Ld)',
$this->visibility);
}
if ($this->publishedAfter !== null) {
$where[] = qsprintf(
$conn,
'p.datePublished > %d',
$this->publishedAfter);
}
if ($this->blogPHIDs !== null) {
$where[] = qsprintf(
$conn,
'p.blogPHID in (%Ls)',
$this->blogPHIDs);
}
return $where;
}
public function getBuiltinOrders() {
return array(
'datePublished' => array(
'vector' => array('datePublished', 'id'),
'name' => pht('Publish Date'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'datePublished' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'datePublished',
'type' => 'int',
'reverse' => false,
),
);
}
- 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() {
// TODO: Does setting this break public blogs?
return null;
}
protected function getPrimaryTableAlias() {
return 'p';
}
}
diff --git a/src/applications/phlux/query/PhluxVariableQuery.php b/src/applications/phlux/query/PhluxVariableQuery.php
index 75abd044d0..8ec4bc9334 100644
--- a/src/applications/phlux/query/PhluxVariableQuery.php
+++ b/src/applications/phlux/query/PhluxVariableQuery.php
@@ -1,95 +1,95 @@
<?php
final class PhluxVariableQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $keys;
private $phids;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withKeys(array $keys) {
$this->keys = $keys;
return $this;
}
protected function loadPage() {
$table = new PhluxVariable();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($rows);
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->keys !== null) {
$where[] = qsprintf(
$conn,
'variableKey IN (%Ls)',
$this->keys);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
}
protected function getDefaultOrderVector() {
return array('key');
}
public function getOrderableColumns() {
return array(
'key' => array(
'column' => 'variableKey',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $object = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
+ 'id' => (int)$object->getID(),
'key' => $object->getVariableKey(),
);
}
public function getQueryApplicationClass() {
return 'PhabricatorPhluxApplication';
}
}
diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php
index cf5122c020..6400771a00 100644
--- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php
+++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php
@@ -1,333 +1,332 @@
<?php
final class PhrequentUserTimeQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
const ORDER_ID_ASC = 0;
const ORDER_ID_DESC = 1;
const ORDER_STARTED_ASC = 2;
const ORDER_STARTED_DESC = 3;
const ORDER_ENDED_ASC = 4;
const ORDER_ENDED_DESC = 5;
const ENDED_YES = 0;
const ENDED_NO = 1;
const ENDED_ALL = 2;
private $ids;
private $userPHIDs;
private $objectPHIDs;
private $ended = self::ENDED_ALL;
private $needPreemptingEvents;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withEnded($ended) {
$this->ended = $ended;
return $this;
}
public function setOrder($order) {
switch ($order) {
case self::ORDER_ID_ASC:
$this->setOrderVector(array('-id'));
break;
case self::ORDER_ID_DESC:
$this->setOrderVector(array('id'));
break;
case self::ORDER_STARTED_ASC:
$this->setOrderVector(array('-start', '-id'));
break;
case self::ORDER_STARTED_DESC:
$this->setOrderVector(array('start', 'id'));
break;
case self::ORDER_ENDED_ASC:
$this->setOrderVector(array('-end', '-id'));
break;
case self::ORDER_ENDED_DESC:
$this->setOrderVector(array('end', 'id'));
break;
default:
throw new Exception(pht('Unknown order "%s".', $order));
}
return $this;
}
public function needPreemptingEvents($need_events) {
$this->needPreemptingEvents = $need_events;
return $this;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
$conn,
'userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
switch ($this->ended) {
case self::ENDED_ALL:
break;
case self::ENDED_YES:
$where[] = qsprintf(
$conn,
'dateEnded IS NOT NULL');
break;
case self::ENDED_NO:
$where[] = qsprintf(
$conn,
'dateEnded IS NULL');
break;
default:
throw new Exception(pht("Unknown ended '%s'!", $this->ended));
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'start' => array(
'column' => 'dateStarted',
'type' => 'int',
),
'end' => array(
'column' => 'dateEnded',
'type' => 'int',
'null' => 'head',
),
);
}
- 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(),
);
}
protected function loadPage() {
$usertime = new PhrequentUserTime();
$conn = $usertime->establishConnection('r');
$data = queryfx_all(
$conn,
'SELECT usertime.* FROM %T usertime %Q %Q %Q',
$usertime->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $usertime->loadAllFromArray($data);
}
protected function didFilterPage(array $page) {
if ($this->needPreemptingEvents) {
$usertime = new PhrequentUserTime();
$conn_r = $usertime->establishConnection('r');
$preempt = array();
foreach ($page as $event) {
$preempt[] = qsprintf(
$conn_r,
'(userPHID = %s AND
(dateStarted BETWEEN %d AND %d) AND
(dateEnded IS NULL OR dateEnded > %d))',
$event->getUserPHID(),
$event->getDateStarted(),
nonempty($event->getDateEnded(), PhabricatorTime::getNow()),
$event->getDateStarted());
}
$preempting_events = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE %LO ORDER BY dateStarted ASC, id ASC',
$usertime->getTableName(),
$preempt);
$preempting_events = $usertime->loadAllFromArray($preempting_events);
$preempting_events = mgroup($preempting_events, 'getUserPHID');
foreach ($page as $event) {
$e_start = $event->getDateStarted();
$e_end = $event->getDateEnded();
$select = array();
$user_events = idx($preempting_events, $event->getUserPHID(), array());
foreach ($user_events as $u_event) {
if ($u_event->getID() == $event->getID()) {
// Don't allow an event to preempt itself.
continue;
}
$u_start = $u_event->getDateStarted();
$u_end = $u_event->getDateEnded();
if ($u_start < $e_start) {
// This event started before our event started, so it's not
// preempting us.
continue;
}
if ($u_start == $e_start) {
if ($u_event->getID() < $event->getID()) {
// This event started at the same time as our event started,
// but has a lower ID, so it's not preempting us.
continue;
}
}
if (($e_end !== null) && ($u_start > $e_end)) {
// Our event has ended, and this event started after it ended.
continue;
}
if (($u_end !== null) && ($u_end < $e_start)) {
// This event ended before our event began.
continue;
}
$select[] = $u_event;
}
$event->attachPreemptingEvents($select);
}
}
return $page;
}
/* -( Helper Functions ) --------------------------------------------------- */
public static function getEndedSearchOptions() {
return array(
self::ENDED_ALL => pht('All'),
self::ENDED_NO => pht('No'),
self::ENDED_YES => pht('Yes'),
);
}
public static function getOrderSearchOptions() {
return array(
self::ORDER_STARTED_ASC => pht('by furthest start date'),
self::ORDER_STARTED_DESC => pht('by nearest start date'),
self::ORDER_ENDED_ASC => pht('by furthest end date'),
self::ORDER_ENDED_DESC => pht('by nearest end date'),
);
}
public static function getUserTotalObjectsTracked(
PhabricatorUser $user,
$limit = PHP_INT_MAX) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
$count = queryfx_one(
$conn,
'SELECT COUNT(usertime.id) N FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.dateEnded IS NULL '.
'LIMIT %d',
$usertime_dao->getTableName(),
$user->getPHID(),
$limit);
return $count['N'];
}
public static function isUserTrackingObject(
PhabricatorUser $user,
$phid) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
$count = queryfx_one(
$conn,
'SELECT COUNT(usertime.id) N FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
return $count['N'] > 0;
}
public static function getUserTimeSpentOnObject(
PhabricatorUser $user,
$phid) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
// First calculate all the time spent where the
// usertime blocks have ended.
$sum_ended = queryfx_one(
$conn,
'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '.
'FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NOT NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
// Now calculate the time spent where the usertime
// blocks have not yet ended.
$sum_not_ended = queryfx_one(
$conn,
'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '.
'FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
return $sum_ended['N'] + $sum_not_ended['N'];
}
public function getQueryApplicationClass() {
return 'PhabricatorPhrequentApplication';
}
}
diff --git a/src/applications/phurl/query/PhabricatorPhurlURLQuery.php b/src/applications/phurl/query/PhabricatorPhurlURLQuery.php
index 74cced0771..6efbbd5b4c 100644
--- a/src/applications/phurl/query/PhabricatorPhurlURLQuery.php
+++ b/src/applications/phurl/query/PhabricatorPhurlURLQuery.php
@@ -1,119 +1,112 @@
<?php
final class PhabricatorPhurlURLQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $names;
private $longURLs;
private $aliases;
private $authorPHIDs;
public function newResultObject() {
return new PhabricatorPhurlURL();
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
id(new PhabricatorPhurlURLNameNgrams()),
$ngrams);
}
public function withLongURLs(array $long_urls) {
$this->longURLs = $long_urls;
return $this;
}
public function withAliases(array $aliases) {
$this->aliases = $aliases;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
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());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'url.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'url.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'url.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'url.name IN (%Ls)',
$this->names);
}
if ($this->longURLs !== null) {
$where[] = qsprintf(
$conn,
'url.longURL IN (%Ls)',
$this->longURLs);
}
if ($this->aliases !== null) {
$where[] = qsprintf(
$conn,
'url.alias IN (%Ls)',
$this->aliases);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'url';
}
public function getQueryApplicationClass() {
return 'PhabricatorPhurlApplication';
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index f6087f7d2a..b08a58501f 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,878 +1,877 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $watcherPHIDs;
private $slugs;
private $slugNormals;
private $slugMap;
private $allSlugs;
private $names;
private $namePrefixes;
private $nameTokens;
private $icons;
private $colors;
private $ancestorPHIDs;
private $parentPHIDs;
private $isMilestone;
private $hasSubprojects;
private $minDepth;
private $maxDepth;
private $minMilestoneNumber;
private $maxMilestoneNumber;
private $subtypes;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $statuses;
private $needSlugs;
private $needMembers;
private $needAncestorMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withWatcherPHIDs(array $watcher_phids) {
$this->watcherPHIDs = $watcher_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefixes(array $prefixes) {
$this->namePrefixes = $prefixes;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function withParentProjectPHIDs($parent_phids) {
$this->parentPHIDs = $parent_phids;
return $this;
}
public function withAncestorProjectPHIDs($ancestor_phids) {
$this->ancestorPHIDs = $ancestor_phids;
return $this;
}
public function withIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function withHasSubprojects($has_subprojects) {
$this->hasSubprojects = $has_subprojects;
return $this;
}
public function withDepthBetween($min, $max) {
$this->minDepth = $min;
$this->maxDepth = $max;
return $this;
}
public function withMilestoneNumberBetween($min, $max) {
$this->minMilestoneNumber = $min;
$this->maxMilestoneNumber = $max;
return $this;
}
public function withSubtypes(array $subtypes) {
$this->subtypes = $subtypes;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needAncestorMembers($need_ancestor_members) {
$this->needAncestorMembers = $need_ancestor_members;
return $this;
}
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
'milestoneNumber' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'milestoneNumber',
'type' => 'int',
),
'status' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'status',
'type' => 'int',
),
);
}
- 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(),
);
}
public function getSlugMap() {
if ($this->slugMap === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->slugMap;
}
protected function willExecute() {
$this->slugMap = array();
$this->slugNormals = array();
$this->allSlugs = array();
if ($this->slugs) {
foreach ($this->slugs as $slug) {
if (PhabricatorSlug::isValidProjectSlug($slug)) {
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
$this->slugNormals[$slug] = $normal;
$this->allSlugs[$normal] = $normal;
}
// NOTE: At least for now, we query for the normalized slugs but also
// for the slugs exactly as entered. This allows older projects with
// slugs that are no longer valid to continue to work.
$this->allSlugs[$slug] = $slug;
}
}
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $projects) {
$ancestor_paths = array();
foreach ($projects as $project) {
foreach ($project->getAncestorProjectPaths() as $path) {
$ancestor_paths[$path] = $path;
}
}
if ($ancestor_paths) {
$ancestors = id(new PhabricatorProject())->loadAllWhere(
'projectPath IN (%Ls)',
$ancestor_paths);
} else {
$ancestors = array();
}
$projects = $this->linkProjectGraph($projects, $ancestors);
$viewer_phid = $this->getViewer()->getPHID();
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$types = array();
$types[] = $material_type;
if ($this->needWatchers) {
$types[] = $watcher_type;
}
$all_graph = $this->getAllReachableAncestors($projects);
// NOTE: Although we may not need much information about ancestors, we
// always need to test if the viewer is a member, because we will return
// ancestor projects to the policy filter via ExtendedPolicy calls. If
// we skip populating membership data on a parent, the policy framework
// will think the user is not a member of the parent project.
$all_sources = array();
foreach ($all_graph as $project) {
// For milestones, we need parent members.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
$all_sources[$parent_phid] = $parent_phid;
}
$phid = $project->getPHID();
$all_sources[$phid] = $phid;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_sources)
->withEdgeTypes($types);
$need_all_edges =
$this->needMembers ||
$this->needWatchers ||
$this->needAncestorMembers;
// If we only need to know if the viewer is a member, we can restrict
// the query to just their PHID.
$any_edges = true;
if (!$need_all_edges) {
if ($viewer_phid) {
$edge_query->withDestinationPHIDs(array($viewer_phid));
} else {
// If we don't need members or watchers and don't have a viewer PHID
// (viewer is logged-out or omnipotent), they'll never be a member
// so we don't need to issue this query at all.
$any_edges = false;
}
}
if ($any_edges) {
$edge_query->execute();
}
$membership_projects = array();
foreach ($all_graph as $project) {
$project_phid = $project->getPHID();
if ($project->isMilestone()) {
$source_phids = array($project->getParentProjectPHID());
} else {
$source_phids = array($project_phid);
}
if ($any_edges) {
$member_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($material_type));
} else {
$member_phids = array();
}
if (in_array($viewer_phid, $member_phids)) {
$membership_projects[$project_phid] = $project;
}
if ($this->needMembers || $this->needAncestorMembers) {
$project->attachMemberPHIDs($member_phids);
}
if ($this->needWatchers) {
$watcher_phids = $edge_query->getDestinationPHIDs(
array($project_phid),
array($watcher_type));
$project->attachWatcherPHIDs($watcher_phids);
$project->setIsUserWatcher(
$viewer_phid,
in_array($viewer_phid, $watcher_phids));
}
}
// If we loaded ancestor members, we've already populated membership
// lists above, so we can skip this step.
if (!$this->needAncestorMembers) {
$member_graph = $this->getAllReachableAncestors($membership_projects);
foreach ($all_graph as $phid => $project) {
$is_member = isset($member_graph[$phid]);
$project->setIsUserMember($viewer_phid, $is_member);
}
}
return $projects;
}
protected function didFilterPage(array $projects) {
$viewer = $this->getViewer();
if ($this->needImages) {
$need_images = $projects;
// First, try to load custom profile images for any projects with custom
// images.
$file_phids = array();
foreach ($need_images as $key => $project) {
$image_phid = $project->getProfileImagePHID();
if ($image_phid) {
$file_phids[$key] = $image_phid;
}
}
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($file_phids as $key => $image_phid) {
$file = idx($files, $image_phid);
if (!$file) {
continue;
}
$need_images[$key]->attachProfileImageFile($file);
unset($need_images[$key]);
}
}
// For projects with default images, or projects where the custom image
// failed to load, load a builtin image.
if ($need_images) {
$builtin_map = array();
$builtins = array();
foreach ($need_images as $key => $project) {
$icon = $project->getIcon();
$builtin_name = PhabricatorProjectIconSet::getIconImage($icon);
$builtin_name = 'projects/'.$builtin_name;
$builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
->setName($builtin_name);
$builtin_key = $builtin->getBuiltinFileKey();
$builtins[] = $builtin;
$builtin_map[$key] = $builtin_key;
}
$builtin_files = PhabricatorFile::loadBuiltins(
$viewer,
$builtins);
foreach ($need_images as $key => $project) {
$builtin_key = $builtin_map[$key];
$builtin_file = $builtin_files[$builtin_key];
$project->attachProfileImageFile($builtin_file);
}
}
}
$this->loadSlugs($projects);
return $projects;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
pht(
"Unknown project status '%s'!",
$this->status));
}
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$filter);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->watcherPHIDs !== null) {
$where[] = qsprintf(
$conn,
'w.dst IN (%Ls)',
$this->watcherPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'slug.slug IN (%Ls)',
$this->allSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if ($this->namePrefixes) {
$parts = array();
foreach ($this->namePrefixes as $name_prefix) {
$parts[] = qsprintf(
$conn,
'name LIKE %>',
$name_prefix);
}
$where[] = qsprintf($conn, '%LO', $parts);
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn,
'color IN (%Ls)',
$this->colors);
}
if ($this->parentPHIDs !== null) {
$where[] = qsprintf(
$conn,
'parentProjectPHID IN (%Ls)',
$this->parentPHIDs);
}
if ($this->ancestorPHIDs !== null) {
$ancestor_paths = queryfx_all(
$conn,
'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
id(new PhabricatorProject())->getTableName(),
$this->ancestorPHIDs);
if (!$ancestor_paths) {
throw new PhabricatorEmptyQueryException();
}
$sql = array();
foreach ($ancestor_paths as $ancestor_path) {
$sql[] = qsprintf(
$conn,
'(projectPath LIKE %> AND projectDepth > %d)',
$ancestor_path['projectPath'],
$ancestor_path['projectDepth']);
}
$where[] = qsprintf($conn, '%LO', $sql);
$where[] = qsprintf(
$conn,
'parentProjectPHID IS NOT NULL');
}
if ($this->isMilestone !== null) {
if ($this->isMilestone) {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NULL');
}
}
if ($this->hasSubprojects !== null) {
$where[] = qsprintf(
$conn,
'hasSubprojects = %d',
(int)$this->hasSubprojects);
}
if ($this->minDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth >= %d',
$this->minDepth);
}
if ($this->maxDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth <= %d',
$this->maxDepth);
}
if ($this->minMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber >= %d',
$this->minMilestoneNumber);
}
if ($this->maxMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber <= %d',
$this->maxMilestoneNumber);
}
if ($this->subtypes !== null) {
$where[] = qsprintf(
$conn,
'subtype IN (%Ls)',
$this->subtypes);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
}
if ($this->watcherPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T w ON w.src = p.phid AND w.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
$name_tokens = $this->getNameTokensForQuery($this->nameTokens);
foreach ($name_tokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function linkProjectGraph(array $projects, array $ancestors) {
$ancestor_map = mpull($ancestors, null, 'getPHID');
$projects_map = mpull($projects, null, 'getPHID');
$all_map = $projects_map + $ancestor_map;
$done = array();
foreach ($projects as $key => $project) {
$seen = array($project->getPHID() => true);
if (!$this->linkProject($project, $all_map, $done, $seen)) {
$this->didRejectResult($project);
unset($projects[$key]);
continue;
}
foreach ($project->getAncestorProjects() as $ancestor) {
$seen[$ancestor->getPHID()] = true;
}
}
return $projects;
}
private function linkProject($project, array $all, array $done, array $seen) {
$parent_phid = $project->getParentProjectPHID();
// This project has no parent, so just attach `null` and return.
if (!$parent_phid) {
$project->attachParentProject(null);
return true;
}
// This project has a parent, but it failed to load.
if (empty($all[$parent_phid])) {
return false;
}
// Test for graph cycles. If we encounter one, we're going to hide the
// entire cycle since we can't meaningfully resolve it.
if (isset($seen[$parent_phid])) {
return false;
}
$seen[$parent_phid] = true;
$parent = $all[$parent_phid];
$project->attachParentProject($parent);
if (!empty($done[$parent_phid])) {
return true;
}
return $this->linkProject($parent, $all, $done, $seen);
}
private function getAllReachableAncestors(array $projects) {
$ancestors = array();
$seen = mpull($projects, null, 'getPHID');
$stack = $projects;
while ($stack) {
$project = array_pop($stack);
$phid = $project->getPHID();
$ancestors[$phid] = $project;
$parent_phid = $project->getParentProjectPHID();
if (!$parent_phid) {
continue;
}
if (isset($seen[$parent_phid])) {
continue;
}
$seen[$parent_phid] = true;
$stack[] = $project->getParentProject();
}
return $ancestors;
}
private function loadSlugs(array $projects) {
// Build a map from primary slugs to projects.
$primary_map = array();
foreach ($projects as $project) {
$primary_slug = $project->getPrimarySlug();
if ($primary_slug === null) {
continue;
}
$primary_map[$primary_slug] = $project;
}
// Link up all of the queried slugs which correspond to primary
// slugs. If we can link up everything from this (no slugs were queried,
// or only primary slugs were queried) we don't need to load anything
// else.
$unknown = $this->slugNormals;
foreach ($unknown as $input => $normal) {
if (isset($primary_map[$input])) {
$match = $input;
} else if (isset($primary_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $primary_map[$match]->getPHID(),
);
unset($unknown[$input]);
}
// If we need slugs, we have to load everything.
// If we still have some queried slugs which we haven't mapped, we only
// need to look for them.
// If we've mapped everything, we don't have to do any work.
$project_phids = mpull($projects, 'getPHID');
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls)',
$project_phids);
} else if ($unknown) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls) AND slug IN (%Ls)',
$project_phids,
$unknown);
} else {
$slugs = array();
}
// Link up any slugs we were not able to link up earlier.
$extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
foreach ($unknown as $input => $normal) {
if (isset($extra_map[$input])) {
$match = $input;
} else if (isset($extra_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $extra_map[$match],
);
unset($unknown[$input]);
}
if ($this->needSlugs) {
$slug_groups = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slug_groups, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
}
private function getNameTokensForQuery(array $tokens) {
// When querying for projects by name, only actually search for the five
// longest tokens. MySQL can get grumpy with a large number of JOINs
// with LIKEs and queries for more than 5 tokens are essentially never
// legitimate searches for projects, but users copy/pasting nonsense.
// See also PHI47.
$length_map = array();
foreach ($tokens as $token) {
$length_map[$token] = strlen($token);
}
arsort($length_map);
$length_map = array_slice($length_map, 0, 5, true);
return array_keys($length_map);
}
}
diff --git a/src/applications/releeph/query/ReleephProductQuery.php b/src/applications/releeph/query/ReleephProductQuery.php
index c039950379..118b9919a8 100644
--- a/src/applications/releeph/query/ReleephProductQuery.php
+++ b/src/applications/releeph/query/ReleephProductQuery.php
@@ -1,146 +1,144 @@
<?php
final class ReleephProductQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $active;
private $ids;
private $phids;
private $repositoryPHIDs;
const ORDER_ID = 'order-id';
const ORDER_NAME = 'order-name';
public function withActive($active) {
$this->active = $active;
return $this;
}
public function setOrder($order) {
switch ($order) {
case self::ORDER_ID:
$this->setOrderVector(array('id'));
break;
case self::ORDER_NAME:
$this->setOrderVector(array('name'));
break;
default:
throw new Exception(pht('Order "%s" not supported.', $order));
}
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
protected function loadPage() {
$table = new ReleephProject();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $projects) {
assert_instances_of($projects, 'ReleephProject');
$repository_phids = mpull($projects, 'getRepositoryPHID');
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
foreach ($projects as $key => $project) {
$repo = idx($repositories, $project->getRepositoryPHID());
if (!$repo) {
unset($projects[$key]);
continue;
}
$project->attachRepository($repo);
}
return $projects;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->active !== null) {
$where[] = qsprintf(
$conn,
'isActive = %d',
(int)$this->active);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ls)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'column' => 'name',
'unique' => true,
'reverse' => true,
'type' => 'string',
),
);
}
- 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(),
);
}
public function getQueryApplicationClass() {
return 'PhabricatorReleephApplication';
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index 1f9cc9c4d0..def502e11f 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,3059 +1,3063 @@
<?php
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*
* @task cursors Query Cursors
* @task clauses Building Query Clauses
* @task appsearch Integration with ApplicationSearch
* @task customfield Integration with CustomField
* @task paging Paging
* @task order Result Ordering
* @task edgelogic Working with Edge Logic
* @task spaces Working with Spaces
*/
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
private $externalCursorString;
private $internalCursorObject;
private $isQueryOrderReversed = false;
private $applicationSearchConstraints = array();
private $internalPaging;
private $orderVector;
private $groupVector;
private $builtinOrder;
private $edgeLogicConstraints = array();
private $edgeLogicConstraintsAreValid = false;
private $spacePHIDs;
private $spaceIsArchived;
private $ngrams = array();
private $ferretEngine;
private $ferretTokens = array();
private $ferretTables = array();
private $ferretQuery;
private $ferretMetadata = array();
/* -( 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) {
return $this->newInternalCursorObjectFromID($cursor);
}
protected function newPagingMapFromCursorObject(
PhabricatorQueryCursor $cursor,
array $keys) {
$object = $cursor->getObject();
+ return $this->newPagingMapFromPartialObject($object);
+ }
+
+ protected function newPagingMapFromPartialObject($object) {
return array(
'id' => (int)$object->getID(),
);
}
final protected function newInternalCursorObjectFromID($id) {
$viewer = $this->getViewer();
$query = newv(get_class($this), array());
$query
->setParentQuery($this)
->setViewer($viewer)
->withIDs(array((int)$id));
// 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());
// 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) {
// TODO: Raise a more tailored exception here and make the UI a little
// prettier?
throw new Exception(
pht(
'Cursor "%s" does not identify a valid object in query "%s".',
$id,
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 private function getExternalCursorStringForResult($object) {
$cursor = $this->newExternalCursorStringForResult($object);
if (!is_string($cursor)) {
throw new Exception(
pht(
'Expected "newExternalCursorStringForResult()" in class "%s" to '.
'return a string, but got "%s".',
get_class($this),
phutil_describe_type($cursor)));
}
return $cursor;
}
final private function getExternalCursorString() {
return $this->externalCursorString;
}
final private function setExternalCursorString($external_cursor) {
$this->externalCursorString = $external_cursor;
return $this;
}
final private function getIsQueryOrderReversed() {
return $this->isQueryOrderReversed;
}
final private function setIsQueryOrderReversed($is_reversed) {
$this->isQueryOrderReversed = $is_reversed;
return $this;
}
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));
$this->setInternalCursorObject($cursor);
}
final public function getFerretMetadata() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Unable to retrieve Ferret engine metadata, this class ("%s") does '.
'not support the Ferret engine.',
get_class($this)));
}
return $this->ferretMetadata;
}
protected function loadStandardPage(PhabricatorLiskDAO $table) {
$rows = $this->loadStandardPageRows($table);
return $table->loadAllFromArray($rows);
}
protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
$conn = $table->establishConnection('r');
return $this->loadStandardPageRowsWithConnection(
$conn,
$table->getTableName());
}
protected function loadStandardPageRowsWithConnection(
AphrontDatabaseConnection $conn,
$table_name) {
$query = $this->buildStandardPageQuery($conn, $table_name);
$rows = queryfx_all($conn, '%Q', $query);
$rows = $this->didLoadRawRows($rows);
return $rows;
}
protected function buildStandardPageQuery(
AphrontDatabaseConnection $conn,
$table_name) {
$table_alias = $this->getPrimaryTableAlias();
if ($table_alias === null) {
$table_alias = qsprintf($conn, '');
} else {
$table_alias = qsprintf($conn, '%T', $table_alias);
}
return qsprintf(
$conn,
'%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$table_name,
$table_alias,
$this->buildJoinClause($conn),
$this->buildWhereClause($conn),
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
}
protected function didLoadRawRows(array $rows) {
if ($this->ferretEngine) {
foreach ($rows as $row) {
$phid = $row['phid'];
$metadata = id(new PhabricatorFerretMetadata())
->setPHID($phid)
->setEngine($this->ferretEngine)
->setRelevance(idx($row, '_ft_rank'));
$this->ferretMetadata[$phid] = $metadata;
unset($row['_ft_rank']);
}
}
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();
if ($limit) {
return qsprintf($conn, 'LIMIT %d', $limit);
}
}
return qsprintf($conn, '');
}
protected function shouldLimitResults() {
return true;
}
final protected function didLoadResults(array $results) {
if ($this->getIsQueryOrderReversed()) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
$limit = $pager->getPageSize();
$this->setLimit($limit + 1);
if (strlen($pager->getAfterID())) {
$this->setExternalCursorString($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setExternalCursorString($pager->getBeforeID());
$this->setIsQueryOrderReversed(true);
}
$results = $this->execute();
$count = count($results);
$sliced_results = $pager->sliceResults($results);
if ($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)) {
$last_object = last($sliced_results);
$cursor = $this->getExternalCursorStringForResult($last_object);
$pager->setNextPageID($cursor);
}
if ($pager->getAfterID() ||
($pager->getBeforeID() && ($count > $limit))) {
$head_object = head($sliced_results);
$cursor = $this->getExternalCursorStringForResult($head_object);
$pager->setPrevPageID($cursor);
}
}
return $sliced_results;
}
/**
* Return the alias this query uses to identify the primary table.
*
* Some automatic query constructions may need to be qualified with a table
* alias if the query performs joins which make column names ambiguous. If
* this is the case, return the alias for the primary table the query
* uses; generally the object table which has `id` and `phid` columns.
*
* @return string Alias for the primary table.
*/
protected function getPrimaryTableAlias() {
return null;
}
public function newResultObject() {
return null;
}
/* -( Building Query Clauses )--------------------------------------------- */
/**
* @task clauses
*/
protected function buildSelectClause(AphrontDatabaseConnection $conn) {
$parts = $this->buildSelectClauseParts($conn);
return $this->formatSelectClause($conn, $parts);
}
/**
* @task clauses
*/
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = array();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$select[] = qsprintf($conn, '%T.*', $alias);
} else {
$select[] = qsprintf($conn, '*');
}
$select[] = $this->buildEdgeLogicSelectClause($conn);
$select[] = $this->buildFerretSelectClause($conn);
return $select;
}
/**
* @task clauses
*/
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$joins = $this->buildJoinClauseParts($conn);
return $this->formatJoinClause($conn, $joins);
}
/**
* @task clauses
*/
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = array();
$joins[] = $this->buildEdgeLogicJoinClause($conn);
$joins[] = $this->buildApplicationSearchJoinClause($conn);
$joins[] = $this->buildNgramsJoinClause($conn);
$joins[] = $this->buildFerretJoinClause($conn);
return $joins;
}
/**
* @task clauses
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = $this->buildWhereClauseParts($conn);
return $this->formatWhereClause($conn, $where);
}
/**
* @task clauses
*/
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
$where[] = $this->buildEdgeLogicWhereClause($conn);
$where[] = $this->buildSpacesWhereClause($conn);
$where[] = $this->buildNgramsWhereClause($conn);
$where[] = $this->buildFerretWhereClause($conn);
$where[] = $this->buildApplicationSearchWhereClause($conn);
return $where;
}
/**
* @task clauses
*/
protected function buildHavingClause(AphrontDatabaseConnection $conn) {
$having = $this->buildHavingClauseParts($conn);
return $this->formatHavingClause($conn, $having);
}
/**
* @task clauses
*/
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
$having = array();
$having[] = $this->buildEdgeLogicHavingClause($conn);
return $having;
}
/**
* @task clauses
*/
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
if (!$this->shouldGroupQueryResultRows()) {
return qsprintf($conn, '');
}
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn($conn));
}
/**
* @task clauses
*/
protected function shouldGroupQueryResultRows() {
if ($this->shouldGroupEdgeLogicResultRows()) {
return true;
}
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return true;
}
if ($this->shouldGroupNgramResultRows()) {
return true;
}
if ($this->shouldGroupFerretResultRows()) {
return true;
}
return false;
}
/* -( Paging )------------------------------------------------------------- */
/**
* @task paging
*/
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
// 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();
}
$value_map = $this->getPagingMapFromCursorObject(
$cursor_object,
$keys);
$columns = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
$column = $orderable[$key];
$column['value'] = $value_map[$key];
// If the vector component is reversed, we need to reverse whatever the
// order of the column is.
if ($order->getIsReversed()) {
$column['reverse'] = !idx($column, 'reverse', false);
}
$columns[] = $column;
}
return $this->buildPagingClauseFromMultipleColumns(
$conn,
$columns,
array(
'reversed' => $reversed,
));
}
/**
* Simplifies the task of constructing a paging clause across multiple
* columns. In the general case, this looks like:
*
* A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c)
*
* To build a clause, specify the name, type, and value of each column
* to include:
*
* $this->buildPagingClauseFromMultipleColumns(
* $conn_r,
* array(
* array(
* 'table' => 't',
* 'column' => 'title',
* 'type' => 'string',
* 'value' => $cursor->getTitle(),
* 'reverse' => true,
* ),
* array(
* 'table' => 't',
* 'column' => 'id',
* 'type' => 'int',
* 'value' => $cursor->getID(),
* ),
* ),
* array(
* 'reversed' => $is_reversed,
* ));
*
* This method will then return a composable clause for inclusion in WHERE.
*
* @param AphrontDatabaseConnection Connection query will execute on.
* @param list<map> Column description dictionaries.
* @param map Additional construction options.
* @return string Query clause.
* @task paging
*/
final protected function buildPagingClauseFromMultipleColumns(
AphrontDatabaseConnection $conn,
array $columns,
array $options) {
foreach ($columns as $column) {
PhutilTypeSpec::checkMap(
$column,
array(
'table' => 'optional string|null',
'column' => 'string',
'value' => 'wild',
'type' => 'string',
'reverse' => 'optional bool',
'unique' => 'optional bool',
'null' => 'optional string|null',
));
}
PhutilTypeSpec::checkMap(
$options,
array(
'reversed' => 'optional bool',
));
$is_query_reversed = idx($options, 'reversed', false);
$clauses = array();
$accumulated = array();
$last_key = last_key($columns);
foreach ($columns as $key => $column) {
$type = $column['type'];
$null = idx($column, 'null');
if ($column['value'] === null) {
if ($null) {
$value = null;
} else {
throw new Exception(
pht(
'Column "%s" has null value, but does not specify a null '.
'behavior.',
$key));
}
} else {
switch ($type) {
case 'int':
$value = qsprintf($conn, '%d', $column['value']);
break;
case 'float':
$value = qsprintf($conn, '%f', $column['value']);
break;
case 'string':
$value = qsprintf($conn, '%s', $column['value']);
break;
default:
throw new Exception(
pht(
'Column "%s" has unknown column type "%s".',
$column['column'],
$type));
}
}
$is_column_reversed = idx($column, 'reverse', false);
$reverse = ($is_query_reversed xor $is_column_reversed);
$clause = $accumulated;
$table_name = idx($column, 'table');
$column_name = $column['column'];
if ($table_name !== null) {
$field = qsprintf($conn, '%T.%T', $table_name, $column_name);
} else {
$field = qsprintf($conn, '%T', $column_name);
}
$parts = array();
if ($null) {
$can_page_if_null = ($null === 'head');
$can_page_if_nonnull = ($null === 'tail');
if ($reverse) {
$can_page_if_null = !$can_page_if_null;
$can_page_if_nonnull = !$can_page_if_nonnull;
}
$subclause = null;
if ($can_page_if_null && $value === null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NOT NULL)',
$field);
} else if ($can_page_if_nonnull && $value !== null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NULL)',
$field);
}
}
if ($value !== null) {
$parts[] = qsprintf(
$conn,
'%Q %Q %Q',
$field,
$reverse ? qsprintf($conn, '>') : qsprintf($conn, '<'),
$value);
}
if ($parts) {
$clause[] = qsprintf($conn, '%LO', $parts);
}
if ($clause) {
$clauses[] = qsprintf($conn, '%LA', $clause);
}
if ($value === null) {
$accumulated[] = qsprintf(
$conn,
'%Q IS NULL',
$field);
} else {
$accumulated[] = qsprintf(
$conn,
'%Q = %Q',
$field,
$value);
}
}
if ($clauses) {
return qsprintf($conn, '%LO', $clauses);
}
return qsprintf($conn, '');
}
/* -( Result Ordering )---------------------------------------------------- */
/**
* Select a result ordering.
*
* This is a high-level method which selects an ordering from a predefined
* list of builtin orders, as provided by @{method:getBuiltinOrders}. These
* options are user-facing and not exhaustive, but are generally convenient
* and meaningful.
*
* You can also use @{method:setOrderVector} to specify a low-level ordering
* across individual orderable columns. This offers greater control but is
* also more involved.
*
* @param string Key of a builtin order supported by this query.
* @return this
* @task order
*/
public function setOrder($order) {
$aliases = $this->getBuiltinOrderAliasMap();
if (empty($aliases[$order])) {
throw new Exception(
pht(
'Query "%s" does not support a builtin order "%s". Supported orders '.
'are: %s.',
get_class($this),
$order,
implode(', ', array_keys($aliases))));
}
$this->builtinOrder = $aliases[$order];
$this->orderVector = null;
return $this;
}
/**
* Set a grouping order to apply before primary result ordering.
*
* This allows you to preface the query order vector with additional orders,
* so you can effect "group by" queries while still respecting "order by".
*
* This is a high-level method which works alongside @{method:setOrder}. For
* lower-level control over order vectors, use @{method:setOrderVector}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setGroupVector($vector) {
$this->groupVector = $vector;
$this->orderVector = null;
return $this;
}
/**
* Get builtin orders for this class.
*
* In application UIs, we want to be able to present users with a small
* selection of meaningful order options (like "Order by Title") rather than
* an exhaustive set of column ordering options.
*
* Meaningful user-facing orders are often really orders across multiple
* columns: for example, a "title" ordering is usually implemented as a
* "title, id" ordering under the hood.
*
* Builtin orders provide a mapping from convenient, understandable
* user-facing orders to implementations.
*
* A builtin order should provide these keys:
*
* - `vector` (`list<string>`): The actual order vector to use.
* - `name` (`string`): Human-readable order name.
*
* @return map<string, wild> Map from builtin order keys to specification.
* @task order
*/
public function getBuiltinOrders() {
$orders = array(
'newest' => array(
'vector' => array('id'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-id'),
'name' => pht('Creation (Oldest First)'),
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$legacy_key = 'custom:'.$field->getFieldKey();
$modern_key = $field->getModernFieldKey();
$orders[$modern_key] = array(
'vector' => array($modern_key, 'id'),
'name' => $field->getFieldName(),
'aliases' => array($legacy_key),
);
$orders['-'.$modern_key] = array(
'vector' => array('-'.$modern_key, '-id'),
'name' => pht('%s (Reversed)', $field->getFieldName()),
);
}
}
if ($this->supportsFerretEngine()) {
$orders['relevance'] = array(
'vector' => array('rank', 'fulltext-modified', 'id'),
'name' => pht('Relevance'),
);
}
return $orders;
}
public function getBuiltinOrderAliasMap() {
$orders = $this->getBuiltinOrders();
$map = array();
foreach ($orders as $key => $order) {
$keys = array();
$keys[] = $key;
foreach (idx($order, 'aliases', array()) as $alias) {
$keys[] = $alias;
}
foreach ($keys as $alias) {
if (isset($map[$alias])) {
throw new Exception(
pht(
'Two builtin orders ("%s" and "%s") define the same key or '.
'alias ("%s"). Each order alias and key must be unique and '.
'identify a single order.',
$key,
$map[$alias],
$alias));
}
$map[$alias] = $key;
}
}
return $map;
}
/**
* Set a low-level column ordering.
*
* This is a low-level method which offers granular control over column
* ordering. In most cases, applications can more easily use
* @{method:setOrder} to choose a high-level builtin order.
*
* To set an order vector, specify a list of order keys as provided by
* @{method:getOrderableColumns}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setOrderVector($vector) {
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
$orderable = $this->getOrderableColumns();
// Make sure that all the components identify valid columns.
$unique = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (empty($orderable[$key])) {
$valid = implode(', ', array_keys($orderable));
throw new Exception(
pht(
'This query ("%s") does not support sorting by order key "%s". '.
'Supported orders are: %s.',
get_class($this),
$key,
$valid));
}
$unique[$key] = idx($orderable[$key], 'unique', false);
}
// Make sure that the last column is unique so that this is a strong
// ordering which can be used for paging.
$last = last($unique);
if ($last !== true) {
throw new Exception(
pht(
'Order vector "%s" is invalid: the last column in an order must '.
'be a column with unique values, but "%s" is not unique.',
$vector->getAsString(),
last_key($unique)));
}
// Make sure that other columns are not unique; an ordering like "id, name"
// does not make sense because only "id" can ever have an effect.
array_pop($unique);
foreach ($unique as $key => $is_unique) {
if ($is_unique) {
throw new Exception(
pht(
'Order vector "%s" is invalid: only the last column in an order '.
'may be unique, but "%s" is a unique column and not the last '.
'column in the order.',
$vector->getAsString(),
$key));
}
}
$this->orderVector = $vector;
return $this;
}
/**
* Get the effective order vector.
*
* @return PhabricatorQueryOrderVector Effective vector.
* @task order
*/
protected function getOrderVector() {
if (!$this->orderVector) {
if ($this->builtinOrder !== null) {
$builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder);
$vector = $builtin_order['vector'];
} else {
$vector = $this->getDefaultOrderVector();
}
if ($this->groupVector) {
$group = PhabricatorQueryOrderVector::newFromVector($this->groupVector);
$group->appendVector($vector);
$vector = $group;
}
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
// We call setOrderVector() here to apply checks to the default vector.
// This catches any errors in the implementation.
$this->setOrderVector($vector);
}
return $this->orderVector;
}
/**
* @task order
*/
protected function getDefaultOrderVector() {
return array('id');
}
/**
* @task order
*/
public function getOrderableColumns() {
$cache = PhabricatorCaches::getRequestCache();
$class = get_class($this);
$cache_key = 'query.orderablecolumns.'.$class;
$columns = $cache->getKey($cache_key);
if ($columns !== null) {
return $columns;
}
$columns = array(
'id' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'id',
'reverse' => false,
'type' => 'int',
'unique' => true,
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$digest = $field->getFieldIndex();
$key = $field->getModernFieldKey();
$columns[$key] = array(
'table' => 'appsearch_order_'.$digest,
'column' => 'indexValue',
'type' => $index->getIndexValueType(),
'null' => 'tail',
'customfield' => true,
'customfield.index.table' => $index->getTableName(),
'customfield.index.key' => $digest,
);
}
}
if ($this->supportsFerretEngine()) {
$columns['rank'] = array(
'table' => null,
'column' => '_ft_rank',
'type' => 'int',
);
$columns['fulltext-created'] = array(
'table' => 'ft_doc',
'column' => 'epochCreated',
'type' => 'int',
);
$columns['fulltext-modified'] = array(
'table' => 'ft_doc',
'column' => 'epochModified',
'type' => 'int',
);
}
$cache->setKey($cache_key, $columns);
return $columns;
}
/**
* @task order
*/
final protected function buildOrderClause(
AphrontDatabaseConnection $conn,
$for_union = false) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
$parts = array();
foreach ($vector as $order) {
$part = $orderable[$order->getOrderKey()];
if ($order->getIsReversed()) {
$part['reverse'] = !idx($part, 'reverse', false);
}
$parts[] = $part;
}
return $this->formatOrderClause($conn, $parts, $for_union);
}
/**
* @task order
*/
protected function formatOrderClause(
AphrontDatabaseConnection $conn,
array $parts,
$for_union = false) {
$is_query_reversed = $this->getIsQueryOrderReversed();
$sql = array();
foreach ($parts as $key => $part) {
$is_column_reversed = !empty($part['reverse']);
$descending = true;
if ($is_query_reversed) {
$descending = !$descending;
}
if ($is_column_reversed) {
$descending = !$descending;
}
$table = idx($part, 'table');
// When we're building an ORDER BY clause for a sequence of UNION
// statements, we can't refer to tables from the subqueries.
if ($for_union) {
$table = null;
}
$column = $part['column'];
if ($table !== null) {
$field = qsprintf($conn, '%T.%T', $table, $column);
} else {
$field = qsprintf($conn, '%T', $column);
}
$null = idx($part, 'null');
if ($null) {
switch ($null) {
case 'head':
$null_field = qsprintf($conn, '(%Q IS NULL)', $field);
break;
case 'tail':
$null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field);
break;
default:
throw new Exception(
pht(
'NULL value "%s" is invalid. Valid values are "head" and '.
'"tail".',
$null));
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $null_field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $null_field);
}
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $field);
}
}
return qsprintf($conn, 'ORDER BY %LQ', $sql);
}
/* -( Application Search )------------------------------------------------- */
/**
* Constrain the query with an ApplicationSearch index, requiring field values
* contain at least one of the values in a set.
*
* This constraint can build the most common types of queries, like:
*
* - Find users with shirt sizes "X" or "XL".
* - Find shoes with size "13".
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param string|list<string> One or more values to filter by.
* @return this
* @task appsearch
*/
public function withApplicationSearchContainsConstraint(
PhabricatorCustomFieldIndexStorage $index,
$value) {
$values = (array)$value;
$data_values = array();
$constraint_values = array();
foreach ($values as $value) {
if ($value instanceof PhabricatorQueryConstraint) {
$constraint_values[] = $value;
} else {
$data_values[] = $value;
}
}
$alias = 'appsearch_'.count($this->applicationSearchConstraints);
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => '=',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'alias' => $alias,
'value' => $values,
'data' => $data_values,
'constraints' => $constraint_values,
);
return $this;
}
/**
* Constrain the query with an ApplicationSearch index, requiring values
* exist in a given range.
*
* This constraint is useful for expressing date ranges:
*
* - Find events between July 1st and July 7th.
*
* The ends of the range are inclusive, so a `$min` of `3` and a `$max` of
* `5` will match fields with values `3`, `4`, or `5`. Providing `null` for
* either end of the range will leave that end of the constraint open.
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param int|null Minimum permissible value, inclusive.
* @param int|null Maximum permissible value, inclusive.
* @return this
* @task appsearch
*/
public function withApplicationSearchRangeConstraint(
PhabricatorCustomFieldIndexStorage $index,
$min,
$max) {
$index_type = $index->getIndexValueType();
if ($index_type != 'int') {
throw new Exception(
pht(
'Attempting to apply a range constraint to a field with index type '.
'"%s", expected type "%s".',
$index_type,
'int'));
}
$alias = 'appsearch_'.count($this->applicationSearchConstraints);
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => 'range',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'alias' => $alias,
'value' => array($min, $max),
'data' => null,
'constraints' => null,
);
return $this;
}
/**
* Get the name of the query's primary object PHID column, for constructing
* JOIN clauses. Normally (and by default) this is just `"phid"`, but it may
* be something more exotic.
*
* See @{method:getPrimaryTableAlias} if the column needs to be qualified with
* a table alias.
*
* @param AphrontDatabaseConnection Connection executing queries.
* @return PhutilQueryString Column name.
* @task appsearch
*/
protected function getApplicationSearchObjectPHIDColumn(
AphrontDatabaseConnection $conn) {
if ($this->getPrimaryTableAlias()) {
return qsprintf($conn, '%T.phid', $this->getPrimaryTableAlias());
} else {
return qsprintf($conn, 'phid');
}
}
/**
* Determine if the JOINs built by ApplicationSearch might cause each primary
* object to return multiple result rows. Generally, this means the query
* needs an extra GROUP BY clause.
*
* @return bool True if the query may return multiple rows for each object.
* @task appsearch
*/
protected function getApplicationSearchMayJoinMultipleRows() {
foreach ($this->applicationSearchConstraints as $constraint) {
$type = $constraint['type'];
$value = $constraint['value'];
$cond = $constraint['cond'];
switch ($cond) {
case '=':
switch ($type) {
case 'string':
case 'int':
if (count($value) > 1) {
return true;
}
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
break;
case 'range':
// NOTE: It's possible to write a custom field where multiple rows
// match a range constraint, but we don't currently ship any in the
// upstream and I can't immediately come up with cases where this
// would make sense.
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
return false;
}
/**
* Construct a GROUP BY clause appropriate for ApplicationSearch constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Group clause.
* @task appsearch
*/
protected function buildApplicationSearchGroupClause(
AphrontDatabaseConnection $conn) {
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn($conn));
} else {
return qsprintf($conn, '');
}
}
/**
* Construct a JOIN clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Join clause.
* @task appsearch
*/
protected function buildApplicationSearchJoinClause(
AphrontDatabaseConnection $conn) {
$joins = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$table = $constraint['table'];
$alias = $constraint['alias'];
$index = $constraint['index'];
$cond = $constraint['cond'];
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
switch ($cond) {
case '=':
// Figure out whether we need to do a LEFT JOIN or not. We need to
// LEFT JOIN if we're going to select "IS NULL" rows.
$join_type = qsprintf($conn, 'JOIN');
foreach ($constraint['constraints'] as $query_constraint) {
$op = $query_constraint->getOperator();
if ($op === PhabricatorQueryConstraint::OPERATOR_NULL) {
$join_type = qsprintf($conn, 'LEFT JOIN');
break;
}
}
$joins[] = qsprintf(
$conn,
'%Q %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$join_type,
$table,
$alias,
$alias,
$phid_column,
$alias,
$index);
break;
case 'range':
list($min, $max) = $constraint['value'];
if (($min === null) && ($max === null)) {
// If there's no actual range constraint, just move on.
break;
}
if ($min === null) {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue <= %d',
$alias,
$max);
} else if ($max === null) {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue >= %d',
$alias,
$min);
} else {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue BETWEEN %d AND %d',
$alias,
$min,
$max);
}
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s
AND (%Q)',
$table,
$alias,
$alias,
$phid_column,
$alias,
$index,
$constraint_clause);
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
foreach ($vector as $order) {
$spec = $orderable[$order->getOrderKey()];
if (empty($spec['customfield'])) {
continue;
}
$table = $spec['customfield.index.table'];
$alias = $spec['table'];
$key = $spec['customfield.index.key'];
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$table,
$alias,
$alias,
$phid_column,
$alias,
$key);
}
if ($joins) {
return qsprintf($conn, '%LJ', $joins);
} else {
return qsprintf($conn, '');
}
}
/**
* Construct a WHERE clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return list<string> Where clause parts.
* @task appsearch
*/
protected function buildApplicationSearchWhereClause(
AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$alias = $constraint['alias'];
$cond = $constraint['cond'];
$type = $constraint['type'];
$data_values = $constraint['data'];
$constraint_values = $constraint['constraints'];
$constraint_parts = array();
switch ($cond) {
case '=':
if ($data_values) {
switch ($type) {
case 'string':
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IN (%Ls)',
$alias,
$data_values);
break;
case 'int':
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IN (%Ld)',
$alias,
$data_values);
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
}
if ($constraint_values) {
foreach ($constraint_values as $value) {
$op = $value->getOperator();
switch ($op) {
case PhabricatorQueryConstraint::OPERATOR_NULL:
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_ANY:
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IS NOT NULL',
$alias);
break;
default:
throw new Exception(
pht(
'No support for applying operator "%s" against '.
'index of type "%s".',
$op,
$type));
}
}
}
if ($constraint_parts) {
$where[] = qsprintf($conn, '%LO', $constraint_parts);
}
break;
}
}
return $where;
}
/* -( Integration with CustomField )--------------------------------------- */
/**
* @task customfield
*/
protected function getPagingValueMapForCustomFields(
PhabricatorCustomFieldInterface $object) {
// We have to get the current field values on the cursor object.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->getViewer());
$fields->readFieldsFromStorage($object);
$map = array();
foreach ($fields->getFields() as $field) {
$map['custom:'.$field->getFieldKey()] = $field->getValueForStorage();
}
return $map;
}
/**
* @task customfield
*/
protected function isCustomFieldOrderKey($key) {
$prefix = 'custom:';
return !strncmp($key, $prefix, strlen($prefix));
}
/* -( Ferret )------------------------------------------------------------- */
public function supportsFerretEngine() {
$object = $this->newResultObject();
return ($object instanceof PhabricatorFerretInterface);
}
public function withFerretQuery(
PhabricatorFerretEngine $engine,
PhabricatorSavedQuery $query) {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
$this->ferretEngine = $engine;
$this->ferretQuery = $query;
return $this;
}
public function getFerretTokens() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
return $this->ferretTokens;
}
public function withFerretConstraint(
PhabricatorFerretEngine $engine,
array $fulltext_tokens) {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
if ($this->ferretEngine) {
throw new Exception(
pht(
'Query may not have multiple fulltext constraints.'));
}
if (!$fulltext_tokens) {
return $this;
}
$this->ferretEngine = $engine;
$this->ferretTokens = $fulltext_tokens;
$current_function = $engine->getDefaultFunctionKey();
$table_map = array();
$idx = 1;
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$function = $raw_token->getFunction();
if ($function === null) {
$function = $current_function;
}
$raw_field = $engine->getFieldForFunction($function);
if (!isset($table_map[$function])) {
$alias = 'ftfield_'.$idx++;
$table_map[$function] = array(
'alias' => $alias,
'key' => $raw_field,
);
}
$current_function = $function;
}
// Join the title field separately so we can rank results.
$table_map['rank'] = array(
'alias' => 'ft_rank',
'key' => PhabricatorSearchDocumentFieldType::FIELD_TITLE,
);
$this->ferretTables = $table_map;
return $this;
}
protected function buildFerretSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
if (!$this->supportsFerretEngine()) {
return $select;
}
$vector = $this->getOrderVector();
if (!$vector->containsKey('rank')) {
// We only need to SELECT the virtual "_ft_rank" column if we're
// actually sorting the results by rank.
return $select;
}
if (!$this->ferretEngine) {
$select[] = qsprintf($conn, '0 _ft_rank');
return $select;
}
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$table_alias = 'ft_rank';
$parts = array();
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$value = $raw_token->getValue();
if ($raw_token->getOperator() == $op_not) {
// Ignore "not" terms when ranking, since they aren't useful.
continue;
}
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
if ($is_substring) {
$parts[] = qsprintf(
$conn,
'IF(%T.rawCorpus LIKE %~, 2, 0)',
$table_alias,
$value);
continue;
}
if ($raw_token->isQuoted()) {
$is_quoted = true;
$is_stemmed = false;
} else {
$is_quoted = false;
$is_stemmed = true;
}
$term_constraints = array();
$term_value = $engine->newTermsCorpus($value);
$parts[] = qsprintf(
$conn,
'IF(%T.termCorpus LIKE %~, 2, 0)',
$table_alias,
$term_value);
if ($is_stemmed) {
$stem_value = $stemmer->stemToken($value);
$stem_value = $engine->newTermsCorpus($stem_value);
$parts[] = qsprintf(
$conn,
'IF(%T.normalCorpus LIKE %~, 1, 0)',
$table_alias,
$stem_value);
}
}
$parts[] = qsprintf($conn, '%d', 0);
$sum = array_shift($parts);
foreach ($parts as $part) {
$sum = qsprintf(
$conn,
'%Q + %Q',
$sum,
$part);
}
$select[] = qsprintf(
$conn,
'%Q _ft_rank',
$sum);
return $select;
}
protected function buildFerretJoinClause(AphrontDatabaseConnection $conn) {
if (!$this->ferretEngine) {
return array();
}
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$ngram_table = $engine->getNgramsTableName();
$flat = array();
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
// If this is a negated term like "-pomegranate", don't join the ngram
// table since we aren't looking for documents with this term. (We could
// LEFT JOIN the table and require a NULL row, but this is probably more
// trouble than it's worth.)
if ($raw_token->getOperator() == $op_not) {
continue;
}
$value = $raw_token->getValue();
$length = count(phutil_utf8v($value));
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
// If the user specified a substring query for a substring which is
// shorter than the ngram length, we can't use the ngram index, so
// don't do a join. We'll fall back to just doing LIKE on the full
// corpus.
if ($is_substring) {
if ($length < 3) {
continue;
}
}
if ($raw_token->isQuoted()) {
$is_stemmed = false;
} else {
$is_stemmed = true;
}
if ($is_substring) {
$ngrams = $engine->getSubstringNgramsFromString($value);
} else {
$terms_value = $engine->newTermsCorpus($value);
$ngrams = $engine->getTermNgramsFromString($terms_value);
// If this is a stemmed term, only look for ngrams present in both the
// unstemmed and stemmed variations.
if ($is_stemmed) {
// Trim the boundary space characters so the stemmer recognizes this
// is (or, at least, may be) a normal word and activates.
$terms_value = trim($terms_value, ' ');
$stem_value = $stemmer->stemToken($terms_value);
$stem_ngrams = $engine->getTermNgramsFromString($stem_value);
$ngrams = array_intersect($ngrams, $stem_ngrams);
}
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $ngram_table,
'ngram' => $ngram,
);
}
}
// Remove common ngrams, like "the", which occur too frequently in
// documents to be useful in constraining the query. The best ngrams
// are obscure sequences which occur in very few documents.
if ($flat) {
$common_ngrams = queryfx_all(
$conn,
'SELECT ngram FROM %T WHERE ngram IN (%Ls)',
$engine->getCommonNgramsTableName(),
ipull($flat, 'ngram'));
$common_ngrams = ipull($common_ngrams, 'ngram', 'ngram');
foreach ($flat as $key => $spec) {
$ngram = $spec['ngram'];
if (isset($common_ngrams[$ngram])) {
unset($flat[$key]);
continue;
}
// NOTE: MySQL discards trailing whitespace in CHAR(X) columns.
$trim_ngram = rtrim($ngram, ' ');
if (isset($common_ngrams[$trim_ngram])) {
unset($flat[$key]);
continue;
}
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$phid_column = qsprintf($conn, '%T.%T', $alias, 'phid');
} else {
$phid_column = qsprintf($conn, '%T', 'phid');
}
$document_table = $engine->getDocumentTableName();
$field_table = $engine->getFieldTableName();
$joins = array();
$joins[] = qsprintf(
$conn,
'JOIN %T ft_doc ON ft_doc.objectPHID = %Q',
$document_table,
$phid_column);
$idx = 1;
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$alias = 'ftngram_'.$idx++;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.documentID = ft_doc.id AND %T.ngram = %s',
$table,
$alias,
$alias,
$alias,
$ngram);
}
foreach ($this->ferretTables as $table) {
$alias = $table['alias'];
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON ft_doc.id = %T.documentID
AND %T.fieldKey = %s',
$field_table,
$alias,
$alias,
$alias,
$table['key']);
}
return $joins;
}
protected function buildFerretWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->ferretEngine) {
return array();
}
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$table_map = $this->ferretTables;
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$op_exact = PhutilSearchQueryCompiler::OPERATOR_EXACT;
$where = array();
$current_function = 'all';
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$value = $raw_token->getValue();
$function = $raw_token->getFunction();
if ($function === null) {
$function = $current_function;
}
$current_function = $function;
$table_alias = $table_map[$function]['alias'];
$is_not = ($raw_token->getOperator() == $op_not);
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
// If we're doing exact search, just test the raw corpus.
$is_exact = ($raw_token->getOperator() == $op_exact);
if ($is_exact) {
if ($is_not) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus != %s)',
$table_alias,
$value);
} else {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus = %s)',
$table_alias,
$value);
}
continue;
}
// If we're doing substring search, we just match against the raw corpus
// and we're done.
if ($is_substring) {
if ($is_not) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus NOT LIKE %~)',
$table_alias,
$value);
} else {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus LIKE %~)',
$table_alias,
$value);
}
continue;
}
// Otherwise, we need to match against the term corpus and the normal
// corpus, so that searching for "raw" does not find "strawberry".
if ($raw_token->isQuoted()) {
$is_quoted = true;
$is_stemmed = false;
} else {
$is_quoted = false;
$is_stemmed = true;
}
// Never stem negated queries, since this can exclude results users
// did not mean to exclude and generally confuse things.
if ($is_not) {
$is_stemmed = false;
}
$term_constraints = array();
$term_value = $engine->newTermsCorpus($value);
if ($is_not) {
$term_constraints[] = qsprintf(
$conn,
'(%T.termCorpus NOT LIKE %~)',
$table_alias,
$term_value);
} else {
$term_constraints[] = qsprintf(
$conn,
'(%T.termCorpus LIKE %~)',
$table_alias,
$term_value);
}
if ($is_stemmed) {
$stem_value = $stemmer->stemToken($value);
$stem_value = $engine->newTermsCorpus($stem_value);
$term_constraints[] = qsprintf(
$conn,
'(%T.normalCorpus LIKE %~)',
$table_alias,
$stem_value);
}
if ($is_not) {
$where[] = qsprintf(
$conn,
'%LA',
$term_constraints);
} else if ($is_quoted) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus LIKE %~ AND %LO)',
$table_alias,
$value,
$term_constraints);
} else {
$where[] = qsprintf(
$conn,
'%LO',
$term_constraints);
}
}
if ($this->ferretQuery) {
$query = $this->ferretQuery;
$author_phids = $query->getParameter('authorPHIDs');
if ($author_phids) {
$where[] = qsprintf(
$conn,
'ft_doc.authorPHID IN (%Ls)',
$author_phids);
}
$with_unowned = $query->getParameter('withUnowned');
$with_any = $query->getParameter('withAnyOwner');
if ($with_any && $with_unowned) {
throw new PhabricatorEmptyQueryException(
pht(
'This query matches only unowned documents owned by anyone, '.
'which is impossible.'));
}
$owner_phids = $query->getParameter('ownerPHIDs');
if ($owner_phids && !$with_any) {
if ($with_unowned) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IN (%Ls) OR ft_doc.ownerPHID IS NULL',
$owner_phids);
} else {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IN (%Ls)',
$owner_phids);
}
} else if ($with_unowned) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IS NULL');
}
if ($with_any) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IS NOT NULL');
}
$rel_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
$statuses = $query->getParameter('statuses');
$is_closed = null;
if ($statuses) {
$statuses = array_fuse($statuses);
if (count($statuses) == 1) {
if (isset($statuses[$rel_open])) {
$is_closed = 0;
} else {
$is_closed = 1;
}
}
}
if ($is_closed !== null) {
$where[] = qsprintf(
$conn,
'ft_doc.isClosed = %d',
$is_closed);
}
}
return $where;
}
protected function shouldGroupFerretResultRows() {
return (bool)$this->ferretTokens;
}
/* -( Ngrams )------------------------------------------------------------- */
protected function withNgramsConstraint(
PhabricatorSearchNgrams $index,
$value) {
if (strlen($value)) {
$this->ngrams[] = array(
'index' => $index,
'value' => $value,
'length' => count(phutil_utf8v($value)),
);
}
return $this;
}
protected function buildNgramsJoinClause(AphrontDatabaseConnection $conn) {
$flat = array();
foreach ($this->ngrams as $spec) {
$index = $spec['index'];
$value = $spec['value'];
$length = $spec['length'];
if ($length >= 3) {
$ngrams = $index->getNgramsFromString($value, 'query');
$prefix = false;
} else if ($length == 2) {
$ngrams = $index->getNgramsFromString($value, 'prefix');
$prefix = false;
} else {
$ngrams = array(' '.$value);
$prefix = true;
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $index->getTableName(),
'ngram' => $ngram,
'prefix' => $prefix,
);
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$id_column = qsprintf($conn, '%T.%T', $alias, 'id');
} else {
$id_column = qsprintf($conn, '%T', 'id');
}
$idx = 1;
$joins = array();
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$prefix = $spec['prefix'];
$alias = 'ngm'.$idx++;
if ($prefix) {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram LIKE %>',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
} else {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram = %s',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
}
}
return $joins;
}
protected function buildNgramsWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->ngrams as $ngram) {
$index = $ngram['index'];
$value = $ngram['value'];
$column = $index->getColumnName();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$column = qsprintf($conn, '%T.%T', $alias, $column);
} else {
$column = qsprintf($conn, '%T', $column);
}
$tokens = $index->tokenizeString($value);
foreach ($tokens as $token) {
$where[] = qsprintf(
$conn,
'%Q LIKE %~',
$column,
$token);
}
}
return $where;
}
protected function shouldGroupNgramResultRows() {
return (bool)$this->ngrams;
}
/* -( Edge Logic )--------------------------------------------------------- */
/**
* Convenience method for specifying edge logic constraints with a list of
* PHIDs.
*
* @param const Edge constant.
* @param const Constraint operator.
* @param list<phid> List of PHIDs.
* @return this
* @task edgelogic
*/
public function withEdgeLogicPHIDs($edge_type, $operator, array $phids) {
$constraints = array();
foreach ($phids as $phid) {
$constraints[] = new PhabricatorQueryConstraint($operator, $phid);
}
return $this->withEdgeLogicConstraints($edge_type, $constraints);
}
/**
* @return this
* @task edgelogic
*/
public function withEdgeLogicConstraints($edge_type, array $constraints) {
assert_instances_of($constraints, 'PhabricatorQueryConstraint');
$constraints = mgroup($constraints, 'getOperator');
foreach ($constraints as $operator => $list) {
foreach ($list as $item) {
$this->edgeLogicConstraints[$edge_type][$operator][] = $item;
}
}
$this->edgeLogicConstraintsAreValid = false;
return $this;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
$this->validateEdgeLogicConstraints();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(%T.dst)) %T',
$alias,
$this->buildEdgeLogicTableAliasCount($alias));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// This is tricky. We have a query which specifies multiple
// projects, each of which may have an arbitrarily large number
// of descendants.
// Suppose the projects are "Engineering" and "Operations", and
// "Engineering" has subprojects X, Y and Z.
// We first use `FIELD(dst, X, Y, Z)` to produce a 0 if a row
// is not part of Engineering at all, or some number other than
// 0 if it is.
// Then we use `IF(..., idx, NULL)` to convert the 0 to a NULL and
// any other value to an index (say, 1) for the ancestor.
// We build these up for every ancestor, then use `COALESCE(...)`
// to select the non-null one, giving us an ancestor which this
// row is a member of.
// From there, we use `COUNT(DISTINCT(...))` to make sure that
// each result row is a member of all ancestors.
if (count($list) > 1) {
$idx = 1;
$parts = array();
foreach ($list as $constraint) {
$parts[] = qsprintf(
$conn,
'IF(FIELD(%T.dst, %Ls) != 0, %d, NULL)',
$alias,
(array)$constraint->getValue(),
$idx++);
}
$parts = qsprintf($conn, '%LQ', $parts);
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(COALESCE(%Q))) %T',
$parts,
$this->buildEdgeLogicTableAliasAncestor($alias));
}
break;
default:
break;
}
}
}
return $select;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicJoinClause(AphrontDatabaseConnection $conn) {
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
$joins = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
// If we're going to process an only() operator, build a list of the
// acceptable set of PHIDs first. We'll only match results which have
// no edges to any other PHIDs.
$all_phids = array();
if (isset($constraints[PhabricatorQueryConstraint::OPERATOR_ONLY])) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$all_phids[$v] = $v;
}
}
break;
}
}
}
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
$phids = array();
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$phids[$v] = $v;
}
}
$phids = array_keys($phids);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
// If we're including results with no matches, we have to degrade
// this to a LEFT join. We'll use WHERE to select matching rows
// later.
if ($has_null) {
$join_type = qsprintf($conn, 'LEFT');
} else {
$join_type = qsprintf($conn, '');
}
$joins[] = qsprintf(
$conn,
'%Q JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$join_type,
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type);
break;
case PhabricatorQueryConstraint::OPERATOR_ONLY:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst NOT IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$all_phids);
break;
}
}
}
return $joins;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$full = array();
$null = array();
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_ONLY:
$full[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if ($has_null) {
$full[] = qsprintf(
$conn,
'%T.dst IS NOT NULL',
$alias);
}
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$null[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
}
}
if ($full && $null) {
$where[] = qsprintf($conn, '(%LA OR %LA)', $full, $null);
} else if ($full) {
foreach ($full as $condition) {
$where[] = $condition;
}
} else if ($null) {
foreach ($null as $condition) {
$where[] = $condition;
}
}
}
return $where;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicHavingClause(AphrontDatabaseConnection $conn) {
$having = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasCount($alias),
count($list));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasAncestor($alias),
count($list));
}
break;
}
}
}
return $having;
}
/**
* @task edgelogic
*/
public function shouldGroupEdgeLogicResultRows() {
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if (count($list) > 1) {
return true;
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// NOTE: We must always group query results rows when using an
// "ANCESTOR" operator because a single task may be related to
// two different descendants of a particular ancestor. For
// discussion, see T12753.
return true;
case PhabricatorQueryConstraint::OPERATOR_NULL:
case PhabricatorQueryConstraint::OPERATOR_ONLY:
return true;
}
}
}
return false;
}
/**
* @task edgelogic
*/
private function getEdgeLogicTableAlias($operator, $type) {
return 'edgelogic_'.$operator.'_'.$type;
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasCount($alias) {
return $alias.'_count';
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasAncestor($alias) {
return $alias.'_ancestor';
}
/**
* Select certain edge logic constraint values.
*
* @task edgelogic
*/
protected function getEdgeLogicValues(
array $edge_types,
array $operators) {
$values = array();
$constraint_lists = $this->edgeLogicConstraints;
if ($edge_types) {
$constraint_lists = array_select_keys($constraint_lists, $edge_types);
}
foreach ($constraint_lists as $type => $constraints) {
if ($operators) {
$constraints = array_select_keys($constraints, $operators);
}
foreach ($constraints as $operator => $list) {
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$values[] = $v;
}
}
}
}
return $values;
}
/**
* Validate edge logic constraints for the query.
*
* @return this
* @task edgelogic
*/
private function validateEdgeLogicConstraints() {
if ($this->edgeLogicConstraintsAreValid) {
return $this;
}
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_EMPTY:
throw new PhabricatorEmptyQueryException(
pht('This query specifies an empty constraint.'));
}
}
}
// This should probably be more modular, eventually, but we only do
// project-based edge logic today.
$project_phids = $this->getEdgeLogicValues(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
),
array(
PhabricatorQueryConstraint::OPERATOR_AND,
PhabricatorQueryConstraint::OPERATOR_OR,
PhabricatorQueryConstraint::OPERATOR_NOT,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
));
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($project_phids as $phid) {
if (empty($projects[$phid])) {
throw new PhabricatorEmptyQueryException(
pht(
'This query is constrained by a project you do not have '.
'permission to see.'));
}
}
}
$op_and = PhabricatorQueryConstraint::OPERATOR_AND;
$op_or = PhabricatorQueryConstraint::OPERATOR_OR;
$op_ancestor = PhabricatorQueryConstraint::OPERATOR_ANCESTOR;
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_ONLY:
if (count($list) > 1) {
throw new PhabricatorEmptyQueryException(
pht(
'This query specifies only() more than once.'));
}
$have_and = idx($constraints, $op_and);
$have_or = idx($constraints, $op_or);
$have_ancestor = idx($constraints, $op_ancestor);
if (!$have_and && !$have_or && !$have_ancestor) {
throw new PhabricatorEmptyQueryException(
pht(
'This query specifies only(), but no other constraints '.
'which it can apply to.'));
}
break;
}
}
}
$this->edgeLogicConstraintsAreValid = true;
return $this;
}
/* -( Spaces )------------------------------------------------------------- */
/**
* Constrain the query to return results from only specific Spaces.
*
* Pass a list of Space PHIDs, or `null` to represent the default space. Only
* results in those Spaces will be returned.
*
* Queries are always constrained to include only results from spaces the
* viewer has access to.
*
* @param list<phid|null>
* @task spaces
*/
public function withSpacePHIDs(array $space_phids) {
$object = $this->newResultObject();
if (!$object) {
throw new Exception(
pht(
'This query (of class "%s") does not implement newResultObject(), '.
'but must implement this method to enable support for Spaces.',
get_class($this)));
}
if (!($object instanceof PhabricatorSpacesInterface)) {
throw new Exception(
pht(
'This query (of class "%s") returned an object of class "%s" from '.
'getNewResultObject(), but it does not implement the required '.
'interface ("%s"). Objects must implement this interface to enable '.
'Spaces support.',
get_class($this),
get_class($object),
'PhabricatorSpacesInterface'));
}
$this->spacePHIDs = $space_phids;
return $this;
}
public function withSpaceIsArchived($archived) {
$this->spaceIsArchived = $archived;
return $this;
}
/**
* Constrain the query to include only results in valid Spaces.
*
* This method builds part of a WHERE clause which considers the spaces the
* viewer has access to see with any explicit constraint on spaces added by
* @{method:withSpacePHIDs}.
*
* @param AphrontDatabaseConnection Database connection.
* @return string Part of a WHERE clause.
* @task spaces
*/
private function buildSpacesWhereClause(AphrontDatabaseConnection $conn) {
$object = $this->newResultObject();
if (!$object) {
return null;
}
if (!($object instanceof PhabricatorSpacesInterface)) {
return null;
}
$viewer = $this->getViewer();
// If we have an omnipotent viewer and no formal space constraints, don't
// emit a clause. This primarily enables older migrations to run cleanly,
// without fataling because they try to match a `spacePHID` column which
// does not exist yet. See T8743, T8746.
if ($viewer->isOmnipotent()) {
if ($this->spaceIsArchived === null && $this->spacePHIDs === null) {
return null;
}
}
// See T13240. If this query raises policy exceptions, don't filter objects
// in the MySQL layer. We want them to reach the application layer so we
// can reject them and raise an exception.
if ($this->shouldRaisePolicyExceptions()) {
return null;
}
$space_phids = array();
$include_null = false;
$all = PhabricatorSpacesNamespaceQuery::getAllSpaces();
if (!$all) {
// If there are no spaces at all, implicitly give the viewer access to
// the default space.
$include_null = true;
} else {
// Otherwise, give them access to the spaces they have permission to
// see.
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$viewer);
foreach ($viewer_spaces as $viewer_space) {
if ($this->spaceIsArchived !== null) {
if ($viewer_space->getIsArchived() != $this->spaceIsArchived) {
continue;
}
}
$phid = $viewer_space->getPHID();
$space_phids[$phid] = $phid;
if ($viewer_space->getIsDefaultNamespace()) {
$include_null = true;
}
}
}
// If we have additional explicit constraints, evaluate them now.
if ($this->spacePHIDs !== null) {
$explicit = array();
$explicit_null = false;
foreach ($this->spacePHIDs as $phid) {
if ($phid === null) {
$space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
} else {
$space = idx($all, $phid);
}
if ($space) {
$phid = $space->getPHID();
$explicit[$phid] = $phid;
if ($space->getIsDefaultNamespace()) {
$explicit_null = true;
}
}
}
// If the viewer can see the default space but it isn't on the explicit
// list of spaces to query, don't match it.
if ($include_null && !$explicit_null) {
$include_null = false;
}
// Include only the spaces common to the viewer and the constraints.
$space_phids = array_intersect_key($space_phids, $explicit);
}
if (!$space_phids && !$include_null) {
if ($this->spacePHIDs === null) {
throw new PhabricatorEmptyQueryException(
pht('You do not have access to any spaces.'));
} else {
throw new PhabricatorEmptyQueryException(
pht(
'You do not have access to any of the spaces this query '.
'is constrained to.'));
}
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$col = qsprintf($conn, '%T.spacePHID', $alias);
} else {
$col = qsprintf($conn, 'spacePHID');
}
if ($space_phids && $include_null) {
return qsprintf(
$conn,
'(%Q IN (%Ls) OR %Q IS NULL)',
$col,
$space_phids,
$col);
} else if ($space_phids) {
return qsprintf(
$conn,
'%Q IN (%Ls)',
$col,
$space_phids);
} else {
return qsprintf(
$conn,
'%Q IS NULL',
$col);
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Jan 19 2025, 23:27 (6 w, 6 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129902
Default Alt Text
(277 KB)

Event Timeline