diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php index b986c0ee6f..ad0bbbac1b 100644 --- a/src/applications/files/query/PhabricatorFileQuery.php +++ b/src/applications/files/query/PhabricatorFileQuery.php @@ -1,342 +1,342 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; 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 withContentHashes(array $content_hashes) { $this->contentHashes = $content_hashes; return $this; } /** * Select files which are transformations of some other file. For example, * you can use this query to find previously generated thumbnails of an image * file. * * As a parameter, provide a list of transformation specifications. Each * specification is a dictionary with the keys `originalPHID` and `transform`. * The `originalPHID` is the PHID of the original file (the file which was * transformed) and the `transform` is the name of the transform to query * for. If you pass `true` as the `transform`, all transformations of the * file will be selected. * * For example: * * array( * array( * 'originalPHID' => 'PHID-FILE-aaaa', * 'transform' => 'sepia', * ), * array( * 'originalPHID' => 'PHID-FILE-bbbb', * 'transform' => true, * ), * ) * * This selects the `"sepia"` transformation of the file with PHID * `PHID-FILE-aaaa` and all transformations of the file with PHID * `PHID-FILE-bbbb`. * * @param list List of transform specifications, described above. * @return this */ public function withTransforms(array $specs) { foreach ($specs as $spec) { if (!is_array($spec) || empty($spec['originalPHID']) || empty($spec['transform'])) { throw new Exception( "Transform specification must be a dictionary with keys ". "'originalPHID' and 'transform'!"); } } $this->transforms = $specs; return $this; } public function withLengthBetween($min, $max) { $this->minLength = $min; $this->maxLength = $max; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withIsPartial($partial) { $this->isPartial = $partial; return $this; } public function showOnlyExplicitUploads($explicit_uploads) { $this->explicitUploads = $explicit_uploads; return $this; } protected function loadPage() { $table = new PhabricatorFile(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT f.* FROM %T f %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $files = $table->loadAllFromArray($data); if (!$files) { return $files; } // We need to load attached objects to perform policy checks for files. // First, load the edges. $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST; $file_phids = mpull($files, 'getPHID'); $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($file_phids) ->withEdgeTypes(array($edge_type)) ->execute(); $object_phids = array(); foreach ($files as $file) { $phids = array_keys($edges[$file->getPHID()][$edge_type]); $file->attachObjectPHIDs($phids); foreach ($phids as $phid) { $object_phids[$phid] = true; } } // If this file is a transform of another file, load that file too. If you // can see the original file, you can see the thumbnail. // TODO: It might be nice to put this directly on PhabricatorFile and remove // the PhabricatorTransformedFile table, which would be a little simpler. $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 'transformedPHID IN (%Ls)', $file_phids); $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID'); foreach ($xform_phids as $derived_phid => $original_phid) { $object_phids[$original_phid] = true; } $object_phids = array_keys($object_phids); // Now, load the objects. $objects = array(); if ($object_phids) { // NOTE: We're explicitly turning policy exceptions off, since the rule // here is "you can see the file if you can see ANY associated object". // Without this explicit flag, we'll incorrectly throw unless you can // see ALL associated objects. $objects = id(new PhabricatorObjectQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($object_phids) ->setRaisePolicyExceptions(false) ->execute(); $objects = mpull($objects, null, 'getPHID'); } foreach ($files as $file) { $file_objects = array_select_keys($objects, $file->getObjectPHIDs()); $file->attachObjects($file_objects); } foreach ($files as $key => $file) { $original_phid = idx($xform_phids, $file->getPHID()); if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) { // This is a special case for builtin files, which are handled // oddly. $original = null; } else if ($original_phid) { $original = idx($objects, $original_phid); if (!$original) { // If the viewer can't see the original file, also prevent them from // seeing the transformed file. $this->didRejectResult($file); unset($files[$key]); continue; } } else { $original = null; } $file->attachOriginalFile($original); } return $files; } private function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if ($this->transforms) { $joins[] = qsprintf( $conn_r, 'JOIN %T t ON t.transformedPHID = f.phid', id(new PhabricatorTransformedFile())->getTableName()); } return implode(' ', $joins); } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'f.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'f.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn_r, 'f.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->explicitUploads !== null) { $where[] = qsprintf( $conn_r, 'f.isExplicitUpload = true'); } if ($this->transforms !== null) { $clauses = array(); foreach ($this->transforms as $transform) { if ($transform['transform'] === true) { $clauses[] = qsprintf( $conn_r, '(t.originalPHID = %s)', $transform['originalPHID']); } else { $clauses[] = qsprintf( $conn_r, '(t.originalPHID = %s AND t.transform = %s)', $transform['originalPHID'], $transform['transform']); } } $where[] = qsprintf($conn_r, '(%Q)', implode(') OR (', $clauses)); } if ($this->dateCreatedAfter !== null) { $where[] = qsprintf( $conn_r, 'f.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore !== null) { $where[] = qsprintf( $conn_r, 'f.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->contentHashes !== null) { $where[] = qsprintf( $conn_r, 'f.contentHash IN (%Ls)', $this->contentHashes); } if ($this->minLength !== null) { $where[] = qsprintf( $conn_r, 'byteSize >= %d', $this->minLength); } if ($this->maxLength !== null) { $where[] = qsprintf( $conn_r, 'byteSize <= %d', $this->maxLength); } if ($this->names !== null) { $where[] = qsprintf( $conn_r, 'name in (%Ls)', $this->names); } if ($this->isPartial !== null) { $where[] = qsprintf( $conn_r, 'isPartial = %d', (int)$this->isPartial); } return $this->formatWhereClause($where); } - protected function getPagingColumn() { - return 'f.id'; + protected function getPrimaryTableAlias() { + return 'f'; } public function getQueryApplicationClass() { return 'PhabricatorFilesApplication'; } } diff --git a/src/applications/macro/query/PhabricatorMacroQuery.php b/src/applications/macro/query/PhabricatorMacroQuery.php index c7f9534499..a326f97697 100644 --- a/src/applications/macro/query/PhabricatorMacroQuery.php +++ b/src/applications/macro/query/PhabricatorMacroQuery.php @@ -1,236 +1,236 @@ 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 $authors) { $this->authors = $authors; return $this; } public function withNameLike($name) { $this->nameLike = $name; return $this; } public function withNames(array $names) { $this->names = $names; 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; } protected function loadPage() { $macro_table = new PhabricatorFileImageMacro(); $conn = $macro_table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT m.* FROM %T m %Q %Q %Q', $macro_table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $macro_table->loadAllFromArray($rows); } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn, 'm.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn, 'm.phid IN (%Ls)', $this->phids); } if ($this->authors) { $where[] = qsprintf( $conn, 'm.authorPHID IN (%Ls)', $this->authors); } if ($this->nameLike) { $where[] = qsprintf( $conn, 'm.name LIKE %~', $this->nameLike); } if ($this->names) { $where[] = qsprintf( $conn, 'm.name IN (%Ls)', $this->names); } 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("Unknown status '{$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('No matching flags.'); } else { $where[] = qsprintf( $conn, 'm.phid IN (%Ls)', mpull($flags, 'getObjectPHID')); } } $where[] = $this->buildPagingClause($conn); return $this->formatWhereClause($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 getPagingColumn() { - return 'm.id'; + protected function getPrimaryTableAlias() { + return 'm'; } public function getQueryApplicationClass() { return 'PhabricatorMacroApplication'; } } diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index d3f178f3d6..c3e1388425 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -1,1157 +1,1157 @@ authorPHIDs = $authors; return $this; } public function withIDs(array $ids) { $this->taskIDs = $ids; return $this; } public function withPHIDs(array $phids) { $this->taskPHIDs = $phids; return $this; } public function withOwners(array $owners) { $this->includeUnowned = false; foreach ($owners as $k => $phid) { if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS || $phid === null) { $this->includeUnowned = true; unset($owners[$k]); break; } } $this->ownerPHIDs = $owners; return $this; } public function withAllProjects(array $projects) { $this->includeNoProject = false; foreach ($projects as $k => $phid) { if ($phid == ManiphestTaskOwner::PROJECT_NO_PROJECT) { $this->includeNoProject = true; unset($projects[$k]); } } $this->projectPHIDs = $projects; return $this; } /** * Add an additional "all projects" constraint to existing filters. * * This is used by boards to supplement queries. * * @param list List of project PHIDs to add to any existing constraint. * @return this */ public function addWithAllProjects(array $projects) { if ($this->projectPHIDs === null) { $this->projectPHIDs = array(); } return $this->withAllProjects(array_merge($this->projectPHIDs, $projects)); } public function withoutProjects(array $projects) { $this->xprojectPHIDs = $projects; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withPriorities(array $priorities) { $this->priorities = $priorities; return $this; } public function withSubpriorities(array $subpriorities) { $this->subpriorities = $subpriorities; return $this; } public function withSubscribers(array $subscribers) { $this->subscriberPHIDs = $subscribers; return $this; } public function withFullTextSearch($fulltext_search) { $this->fullTextSearch = $fulltext_search; return $this; } public function setGroupBy($group) { $this->groupBy = $group; return $this; } public function setOrderBy($order) { $this->orderBy = $order; return $this; } public function withAnyProjects(array $projects) { $this->anyProjectPHIDs = $projects; return $this; } public function withAnyUserProjects(array $users) { $this->anyUserProjectPHIDs = $users; return $this; } /** * True returns tasks that are blocking other tasks only. * False returns tasks that are not blocking other tasks only. * Null returns tasks regardless of blocking status. */ public function withBlockingTasks($mode) { $this->blockingTasks = $mode; return $this; } public function shouldJoinBlockingTasks() { return $this->blockingTasks !== null; } /** * True returns tasks that are blocked by other tasks only. * False returns tasks that are not blocked by other tasks only. * Null returns tasks regardless of blocked by status. */ public function withBlockedTasks($mode) { $this->blockedTasks = $mode; return $this; } public function shouldJoinBlockedTasks() { return $this->blockedTasks !== null; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withDateModifiedBefore($date_modified_before) { $this->dateModifiedBefore = $date_modified_before; return $this; } public function withDateModifiedAfter($date_modified_after) { $this->dateModifiedAfter = $date_modified_after; return $this; } public function needSubscriberPHIDs($bool) { $this->needSubscriberPHIDs = $bool; return $this; } public function needProjectPHIDs($bool) { $this->needProjectPHIDs = $bool; return $this; } protected function willExecute() { // Make sure the user can see any projects specified in this // query FIRST. if ($this->projectPHIDs) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($this->projectPHIDs) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($this->projectPHIDs as $index => $phid) { $project = idx($projects, $phid); if (!$project) { unset($this->projectPHIDs[$index]); continue; } } if (!$this->projectPHIDs) { $this->projectPolicyCheckFailed = true; } $this->projectPHIDs = array_values($this->projectPHIDs); } } protected function loadPage() { if ($this->projectPolicyCheckFailed) { throw new PhabricatorEmptyQueryException(); } $task_dao = new ManiphestTask(); $conn = $task_dao->establishConnection('r'); $where = array(); $where[] = $this->buildTaskIDsWhereClause($conn); $where[] = $this->buildTaskPHIDsWhereClause($conn); $where[] = $this->buildStatusWhereClause($conn); $where[] = $this->buildStatusesWhereClause($conn); $where[] = $this->buildPrioritiesWhereClause($conn); $where[] = $this->buildDependenciesWhereClause($conn); $where[] = $this->buildAuthorWhereClause($conn); $where[] = $this->buildOwnerWhereClause($conn); $where[] = $this->buildProjectWhereClause($conn); $where[] = $this->buildAnyProjectWhereClause($conn); $where[] = $this->buildAnyUserProjectWhereClause($conn); $where[] = $this->buildXProjectWhereClause($conn); $where[] = $this->buildFullTextWhereClause($conn); if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn, 'task.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn, 'task.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->dateModifiedAfter) { $where[] = qsprintf( $conn, 'task.dateModified >= %d', $this->dateModifiedAfter); } if ($this->dateModifiedBefore) { $where[] = qsprintf( $conn, 'task.dateModified <= %d', $this->dateModifiedBefore); } $where[] = $this->buildPagingClause($conn); $where = $this->formatWhereClause($where); $having = ''; $count = ''; if (count($this->projectPHIDs) > 1) { // We want to treat the query as an intersection query, not a union // query. We sum the project count and require it be the same as the // number of projects we're searching for. $count = ', COUNT(project.dst) projectCount'; $having = qsprintf( $conn, 'HAVING projectCount = %d', count($this->projectPHIDs)); } $order = $this->buildCustomOrderClause($conn); // TODO: Clean up this nonstandardness. if (!$this->getLimit()) { $this->setLimit(self::DEFAULT_PAGE_SIZE); } $group_column = ''; switch ($this->groupBy) { case self::GROUP_PROJECT: $group_column = qsprintf( $conn, ', projectGroupName.indexedObjectPHID projectGroupPHID'); break; } $rows = queryfx_all( $conn, 'SELECT task.* %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', $count, $group_column, $task_dao->getTableName(), $this->buildJoinsClause($conn), $where, $this->buildGroupClause($conn), $having, $order, $this->buildLimitClause($conn)); switch ($this->groupBy) { case self::GROUP_PROJECT: $data = ipull($rows, null, 'id'); break; default: $data = $rows; break; } $tasks = $task_dao->loadAllFromArray($data); switch ($this->groupBy) { case self::GROUP_PROJECT: $results = array(); foreach ($rows as $row) { $task = clone $tasks[$row['id']]; $task->attachGroupByProjectPHID($row['projectGroupPHID']); $results[] = $task; } $tasks = $results; break; } return $tasks; } protected function willFilterPage(array $tasks) { if ($this->groupBy == self::GROUP_PROJECT) { // We should only return project groups which the user can actually see. $project_phids = mpull($tasks, 'getGroupByProjectPHID'); $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($tasks as $key => $task) { if (!$task->getGroupByProjectPHID()) { // This task is either not in any projects, or only in projects // which we're ignoring because they're being queried for explicitly. continue; } if (empty($projects[$task->getGroupByProjectPHID()])) { unset($tasks[$key]); } } } return $tasks; } protected function didFilterPage(array $tasks) { $phids = mpull($tasks, 'getPHID'); if ($this->needProjectPHIDs) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($tasks as $task) { $project_phids = $edge_query->getDestinationPHIDs( array($task->getPHID())); $task->attachProjectPHIDs($project_phids); } } if ($this->needSubscriberPHIDs) { $subscriber_sets = id(new PhabricatorSubscribersQuery()) ->withObjectPHIDs($phids) ->execute(); foreach ($tasks as $task) { $subscribers = idx($subscriber_sets, $task->getPHID(), array()); $task->attachSubscriberPHIDs($subscribers); } } return $tasks; } private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) { if (!$this->taskIDs) { return null; } return qsprintf( $conn, 'task.id in (%Ld)', $this->taskIDs); } private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) { if (!$this->taskPHIDs) { return null; } return qsprintf( $conn, 'task.phid in (%Ls)', $this->taskPHIDs); } private function buildStatusWhereClause(AphrontDatabaseConnection $conn) { static $map = array( self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID, self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE, self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE, ); switch ($this->status) { case self::STATUS_ANY: return null; case self::STATUS_OPEN: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); case self::STATUS_CLOSED: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getClosedStatusConstants()); default: $constant = idx($map, $this->status); if (!$constant) { throw new Exception("Unknown status query '{$this->status}'!"); } return qsprintf( $conn, 'task.status = %s', $constant); } } private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) { if ($this->statuses) { return qsprintf( $conn, 'task.status IN (%Ls)', $this->statuses); } return null; } private function buildPrioritiesWhereClause(AphrontDatabaseConnection $conn) { if ($this->priorities) { return qsprintf( $conn, 'task.priority IN (%Ld)', $this->priorities); } if ($this->subpriorities) { return qsprintf( $conn, 'task.subpriority IN (%Lf)', $this->subpriorities); } return null; } private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) { if (!$this->authorPHIDs) { return null; } return qsprintf( $conn, 'task.authorPHID in (%Ls)', $this->authorPHIDs); } private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) { if (!$this->ownerPHIDs) { if ($this->includeUnowned === null) { return null; } else if ($this->includeUnowned) { return qsprintf( $conn, 'task.ownerPHID IS NULL'); } else { return qsprintf( $conn, 'task.ownerPHID IS NOT NULL'); } } if ($this->includeUnowned) { return qsprintf( $conn, 'task.ownerPHID IN (%Ls) OR task.ownerPHID IS NULL', $this->ownerPHIDs); } else { return qsprintf( $conn, 'task.ownerPHID IN (%Ls)', $this->ownerPHIDs); } } private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) { if (!strlen($this->fullTextSearch)) { return null; } // In doing a fulltext search, we first find all the PHIDs that match the // fulltext search, and then use that to limit the rest of the search $fulltext_query = id(new PhabricatorSavedQuery()) ->setEngineClassName('PhabricatorSearchApplicationSearchEngine') ->setParameter('query', $this->fullTextSearch); // NOTE: Setting this to something larger than 2^53 will raise errors in // ElasticSearch, and billions of results won't fit in memory anyway. $fulltext_query->setParameter('limit', 100000); $fulltext_query->setParameter('type', ManiphestTaskPHIDType::TYPECONST); $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); $fulltext_results = $engine->executeSearch($fulltext_query); if (empty($fulltext_results)) { $fulltext_results = array(null); } return qsprintf( $conn, 'task.phid IN (%Ls)', $fulltext_results); } private function buildDependenciesWhereClause( AphrontDatabaseConnection $conn) { if (!$this->shouldJoinBlockedTasks() && !$this->shouldJoinBlockingTasks()) { return null; } $parts = array(); if ($this->blockingTasks === true) { $parts[] = qsprintf( $conn, 'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockingTasks === false) { $parts[] = qsprintf( $conn, 'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } if ($this->blockedTasks === true) { $parts[] = qsprintf( $conn, 'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockedTasks === false) { $parts[] = qsprintf( $conn, 'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } return '('.implode(') OR (', $parts).')'; } private function buildProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->projectPHIDs && !$this->includeNoProject) { return null; } $parts = array(); if ($this->projectPHIDs) { $parts[] = qsprintf( $conn, 'project.dst in (%Ls)', $this->projectPHIDs); } if ($this->includeNoProject) { $parts[] = qsprintf( $conn, 'project.dst IS NULL'); } return '('.implode(') OR (', $parts).')'; } private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->anyProjectPHIDs) { return null; } return qsprintf( $conn, 'anyproject.dst IN (%Ls)', $this->anyProjectPHIDs); } private function buildAnyUserProjectWhereClause( AphrontDatabaseConnection $conn) { if (!$this->anyUserProjectPHIDs) { return null; } $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($this->anyUserProjectPHIDs) ->execute(); $any_user_project_phids = mpull($projects, 'getPHID'); if (!$any_user_project_phids) { throw new PhabricatorEmptyQueryException(); } return qsprintf( $conn, 'anyproject.dst IN (%Ls)', $any_user_project_phids); } private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) { if (!$this->xprojectPHIDs) { return null; } return qsprintf( $conn, 'xproject.dst IS NULL'); } private function buildCustomOrderClause(AphrontDatabaseConnection $conn) { $reverse = ($this->getBeforeID() xor $this->getReversePaging()); $order = array(); switch ($this->groupBy) { case self::GROUP_NONE: break; case self::GROUP_PRIORITY: $order[] = 'task.priority'; break; case self::GROUP_OWNER: $order[] = 'task.ownerOrdering'; break; case self::GROUP_STATUS: $order[] = 'task.status'; break; case self::GROUP_PROJECT: $order[] = ''; break; default: throw new Exception("Unknown group query '{$this->groupBy}'!"); } $app_order = $this->buildApplicationSearchOrders($conn, $reverse); if (!$app_order) { switch ($this->orderBy) { case self::ORDER_PRIORITY: $order[] = 'task.priority'; $order[] = 'task.subpriority'; $order[] = 'task.dateModified'; break; case self::ORDER_CREATED: $order[] = 'task.id'; break; case self::ORDER_MODIFIED: $order[] = 'task.dateModified'; break; case self::ORDER_TITLE: $order[] = 'task.title'; break; default: throw new Exception("Unknown order query '{$this->orderBy}'!"); } } $order = array_unique($order); if (empty($order) && empty($app_order)) { return null; } foreach ($order as $k => $column) { switch ($column) { case 'subpriority': case 'ownerOrdering': case 'title': if ($reverse) { $order[$k] = "{$column} DESC"; } else { $order[$k] = "{$column} ASC"; } break; case '': // Put "No Project" at the end of the list. if ($reverse) { $order[$k] = 'projectGroupName.indexedObjectName IS NULL DESC, '. 'projectGroupName.indexedObjectName DESC'; } else { $order[$k] = 'projectGroupName.indexedObjectName IS NULL ASC, '. 'projectGroupName.indexedObjectName ASC'; } break; default: if ($reverse) { $order[$k] = "{$column} ASC"; } else { $order[$k] = "{$column} DESC"; } break; } } if ($app_order) { foreach ($app_order as $order_by) { $order[] = $order_by; } } if ($reverse) { $order[] = 'task.id ASC'; } else { $order[] = 'task.id DESC'; } return 'ORDER BY '.implode(', ', $order); } private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; $joins = array(); if ($this->projectPHIDs || $this->includeNoProject) { $joins[] = qsprintf( $conn_r, '%Q JOIN %T project ON project.src = task.phid AND project.type = %d', ($this->includeNoProject ? 'LEFT' : ''), $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } if ($this->shouldJoinBlockingTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocking ON blocking.src = task.phid '. 'AND blocking.type = %d '. 'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid', $edge_table, ManiphestTaskDependedOnByTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->shouldJoinBlockedTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocked ON blocked.src = task.phid '. 'AND blocked.type = %d '. 'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid', $edge_table, ManiphestTaskDependsOnTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T anyproject ON anyproject.src = task.phid AND anyproject.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } if ($this->xprojectPHIDs) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T xproject ON xproject.src = task.phid AND xproject.type = %d AND xproject.dst IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $this->xprojectPHIDs); } if ($this->subscriberPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T e_ccs ON e_ccs.src = task.phid '. 'AND e_ccs.type = %s '. 'AND e_ccs.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasSubscriberEdgeType::EDGECONST, $this->subscriberPHIDs); } switch ($this->groupBy) { case self::GROUP_PROJECT: $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs(); if ($ignore_group_phids) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d AND projectGroup.dst NOT IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $ignore_group_phids); } else { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroupName ON projectGroup.dst = projectGroupName.indexedObjectPHID', id(new ManiphestNameIndex())->getTableName()); break; } $joins[] = $this->buildApplicationSearchJoinClause($conn_r); return implode(' ', $joins); } private function buildGroupClause(AphrontDatabaseConnection $conn_r) { $joined_multiple_rows = (count($this->projectPHIDs) > 1) || (count($this->anyProjectPHIDs) > 1) || $this->shouldJoinBlockingTasks() || $this->shouldJoinBlockedTasks() || ($this->getApplicationSearchMayJoinMultipleRows()); $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); // If we're joining multiple rows, we need to group the results by the // task IDs. if ($joined_multiple_rows) { if ($joined_project_name) { return 'GROUP BY task.phid, projectGroup.dst'; } else { return 'GROUP BY task.phid'; } } else { return ''; } } /** * Return project PHIDs which we should ignore when grouping tasks by * project. For example, if a user issues a query like: * * Tasks in all projects: Frontend, Bugs * * ...then we don't show "Frontend" or "Bugs" groups in the result set, since * they're meaningless as all results are in both groups. * * Similarly, for queries like: * * Tasks in any projects: Public Relations * * ...we ignore the single project, as every result is in that project. (In * the case that there are several "any" projects, we do not ignore them.) * * @return list Project PHIDs which should be ignored in query * construction. */ private function getIgnoreGroupedProjectPHIDs() { $phids = array(); if ($this->projectPHIDs) { $phids[] = $this->projectPHIDs; } if (count($this->anyProjectPHIDs) == 1) { $phids[] = $this->anyProjectPHIDs; } // Maybe we should also exclude the "excludeProjectPHIDs"? It won't // impact the results, but we might end up with a better query plan. // Investigate this on real data? This is likely very rare. return array_mergev($phids); } private function loadCursorObject($id) { $results = id(new ManiphestTaskQuery()) ->setViewer($this->getPagingViewer()) ->withIDs(array((int)$id)) ->execute(); return head($results); } protected function getPagingValue($result) { $id = $result->getID(); switch ($this->groupBy) { case self::GROUP_NONE: return $id; case self::GROUP_PRIORITY: return $id.'.'.$result->getPriority(); case self::GROUP_OWNER: return rtrim($id.'.'.$result->getOwnerPHID(), '.'); case self::GROUP_STATUS: return $id.'.'.$result->getStatus(); case self::GROUP_PROJECT: return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.'); default: throw new Exception("Unknown group query '{$this->groupBy}'!"); } } protected function buildPagingClause(AphrontDatabaseConnection $conn_r) { $default = parent::buildPagingClause($conn_r); $before_id = $this->getBeforeID(); $after_id = $this->getAfterID(); if (!$before_id && !$after_id) { return $default; } $cursor_id = nonempty($before_id, $after_id); $cursor_parts = explode('.', $cursor_id, 2); $task_id = $cursor_parts[0]; $group_id = idx($cursor_parts, 1); $cursor = $this->loadCursorObject($task_id); if (!$cursor) { // We may loop if we have a cursor and don't build a paging clause; fail // instead. throw new PhabricatorEmptyQueryException(); } $columns = array(); switch ($this->groupBy) { case self::GROUP_NONE: break; case self::GROUP_PRIORITY: $columns[] = array( 'table' => 'task', 'column' => 'priority', 'value' => (int)$group_id, 'type' => 'int', ); break; case self::GROUP_OWNER: $columns[] = array( 'table' => 'task', 'column' => 'ownerOrdering', 'value' => strlen($group_id), 'type' => 'null', ); if ($group_id) { $paging_users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($group_id)) ->execute(); if (!$paging_users) { return null; } $columns[] = array( 'table' => 'task', 'column' => 'ownerOrdering', 'value' => head($paging_users)->getUsername(), 'type' => 'string', 'reverse' => true, ); } break; case self::GROUP_STATUS: $columns[] = array( 'table' => 'task', 'column' => 'status', 'value' => $group_id, 'type' => 'string', ); break; case self::GROUP_PROJECT: $columns[] = array( 'table' => 'projectGroupName', 'column' => 'indexedObjectName', 'value' => strlen($group_id), 'type' => 'null', ); if ($group_id) { $paging_projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($group_id)) ->execute(); if (!$paging_projects) { return null; } $columns[] = array( 'table' => 'projectGroupName', 'column' => 'indexedObjectName', 'value' => head($paging_projects)->getName(), 'type' => 'string', 'reverse' => true, ); } break; default: throw new Exception("Unknown group query '{$this->groupBy}'!"); } $app_columns = $this->buildApplicationSearchPagination($conn_r, $cursor); if ($app_columns) { $columns = array_merge($columns, $app_columns); } else { switch ($this->orderBy) { case self::ORDER_PRIORITY: if ($this->groupBy != self::GROUP_PRIORITY) { $columns[] = array( 'table' => 'task', 'column' => 'priority', 'value' => (int)$cursor->getPriority(), 'type' => 'int', ); } $columns[] = array( 'table' => 'task', 'column' => 'subpriority', 'value' => $cursor->getSubpriority(), 'type' => 'float', ); $columns[] = array( 'table' => 'task', 'column' => 'dateModified', 'value' => (int)$cursor->getDateModified(), 'type' => 'int', ); break; case self::ORDER_CREATED: // This just uses the ID column, below. break; case self::ORDER_MODIFIED: $columns[] = array( 'table' => 'task', 'column' => 'dateModified', 'value' => (int)$cursor->getDateModified(), 'type' => 'int', ); break; case self::ORDER_TITLE: $columns[] = array( 'table' => 'task', 'column' => 'title', 'value' => $cursor->getTitle(), 'type' => 'string', ); break; default: throw new Exception("Unknown order query '{$this->orderBy}'!"); } } $columns[] = array( 'table' => 'task', 'column' => 'id', 'value' => $cursor->getID(), 'type' => 'int', ); return $this->buildPagingClauseFromMultipleColumns( $conn_r, $columns, array( 'reversed' => (bool)($before_id xor $this->getReversePaging()), )); } - protected function getApplicationSearchObjectPHIDColumn() { - return 'task.phid'; + protected function getPrimaryTableAlias() { + return 'task'; } public function getQueryApplicationClass() { return 'PhabricatorManiphestApplication'; } public function setReversePaging($reverse_paging) { $this->reversePaging = $reverse_paging; return $this; } protected function getReversePaging() { return $this->reversePaging; } } diff --git a/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php index 3983e362cc..d2ac2f17a6 100644 --- a/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php +++ b/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php @@ -1,129 +1,125 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAddresses(array $addresses) { $this->addresses = $addresses; return $this; } public function withAddressPrefix($prefix) { $this->addressPrefix = $prefix; return $this; } public function withApplicationPHIDs(array $phids) { $this->applicationPHIDs = $phids; return $this; } protected function loadPage() { $table = new PhabricatorMetaMTAApplicationEmail(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T appemail %Q %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildApplicationSearchGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $app_emails) { $app_emails_map = mgroup($app_emails, 'getApplicationPHID'); $applications = id(new PhabricatorApplicationQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array_keys($app_emails_map)) ->execute(); $applications = mpull($applications, null, 'getPHID'); foreach ($app_emails_map as $app_phid => $app_emails_group) { foreach ($app_emails_group as $app_email) { $application = idx($applications, $app_phid); if (!$application) { unset($app_emails[$app_phid]); continue; } $app_email->attachApplication($application); } } return $app_emails; } private function buildWhereClause($conn_r) { $where = array(); if ($this->addresses !== null) { $where[] = qsprintf( $conn_r, 'appemail.address IN (%Ls)', $this->addresses); } if ($this->addressPrefix !== null) { $where[] = qsprintf( $conn_r, 'appemail.address LIKE %>', $this->addressPrefix); } if ($this->applicationPHIDs !== null) { $where[] = qsprintf( $conn_r, 'appemail.applicationPHID IN (%Ls)', $this->applicationPHIDs); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'appemail.phid IN (%Ls)', $this->phids); } if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'appemail.id IN (%Ld)', $this->ids); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } - protected function getPagingColumn() { - return 'appemail.id'; - } - - protected function getApplicationSearchObjectPHIDColumn() { - return 'appemail.phid'; + protected function getPrimaryTableAlias() { + return 'appemail'; } public function getQueryApplicationClass() { return 'PhabricatorMetaMTAApplication'; } } diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index f76ab9e5aa..f79957dfa2 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -1,303 +1,299 @@ 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 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 needPrimaryEmail($need) { $this->needPrimaryEmail = $need; return $this; } public function needProfile($need) { $this->needProfile = $need; return $this; } public function needProfileImage($need) { $this->needProfileImage = $need; return $this; } public function needStatus($need) { $this->needStatus = $need; return $this; } protected function loadPage() { $table = new PhabricatorUser(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T user %Q %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinsClause($conn_r), $this->buildWhereClause($conn_r), $this->buildApplicationSearchGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); if ($this->needPrimaryEmail) { $table->putInSet(new LiskDAOSet()); } return $table->loadAllFromArray($data); } 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 = new PhabricatorUserProfile(); $profile->setUserPHID($user_phid); } $user->attachUserProfile($profile); } } if ($this->needProfileImage) { $user_profile_file_phids = mpull($users, 'getProfileImagePHID'); $user_profile_file_phids = array_filter($user_profile_file_phids); if ($user_profile_file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($user_profile_file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } foreach ($users as $user) { $image_phid = $user->getProfileImagePHID(); if (isset($files[$image_phid])) { $profile_image_uri = $files[$image_phid]->getBestURI(); } else { $profile_image_uri = PhabricatorUser::getDefaultProfileImageURI(); } $user->attachProfileImageURI($profile_image_uri); } } if ($this->needStatus) { $user_list = mpull($users, null, 'getPHID'); $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses( array_keys($user_list)); foreach ($user_list as $phid => $user) { $status = idx($statuses, $phid); if ($status) { $user->attachStatus($status); } } } return $users; } private function buildJoinsClause($conn_r) { $joins = array(); if ($this->emails) { $email_table = new PhabricatorUserEmail(); $joins[] = qsprintf( $conn_r, 'JOIN %T email ON email.userPHID = user.PHID', $email_table->getTableName()); } $joins[] = $this->buildApplicationSearchJoinClause($conn_r); $joins = implode(' ', $joins); return $joins; } private function buildWhereClause($conn_r) { $where = array(); if ($this->usernames !== null) { $where[] = qsprintf( $conn_r, 'user.userName IN (%Ls)', $this->usernames); } if ($this->emails !== null) { $where[] = qsprintf( $conn_r, 'email.address IN (%Ls)', $this->emails); } if ($this->realnames !== null) { $where[] = qsprintf( $conn_r, 'user.realName IN (%Ls)', $this->realnames); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'user.phid IN (%Ls)', $this->phids); } if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'user.id IN (%Ld)', $this->ids); } if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn_r, 'user.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn_r, 'user.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->isAdmin) { $where[] = qsprintf( $conn_r, 'user.isAdmin = 1'); } if ($this->isDisabled !== null) { $where[] = qsprintf( $conn_r, 'user.isDisabled = %d', (int)$this->isDisabled); } if ($this->isApproved !== null) { $where[] = qsprintf( $conn_r, 'user.isApproved = %d', (int)$this->isApproved); } if ($this->isSystemAgent) { $where[] = qsprintf( $conn_r, 'user.isSystemAgent = 1'); } if (strlen($this->nameLike)) { $where[] = qsprintf( $conn_r, 'user.username LIKE %~ OR user.realname LIKE %~', $this->nameLike, $this->nameLike); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } - protected function getPagingColumn() { - return 'user.id'; - } - - protected function getApplicationSearchObjectPHIDColumn() { - return 'user.phid'; + protected function getPrimaryTableAlias() { + return 'user'; } public function getQueryApplicationClass() { return 'PhabricatorPeopleApplication'; } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 486055691a..62db2b2aed 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,387 +1,387 @@ 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 withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withPhrictionSlugs(array $slugs) { $this->phrictionSlugs = $slugs; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withDatasourceQuery($string) { $this->datasourceQuery = $string; return $this; } public function withIcons(array $icons) { $this->icons = $icons; return $this; } public function withColors(array $colors) { $this->colors = $colors; return $this; } public function needMembers($need_members) { $this->needMembers = $need_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; } protected function getPagingColumn() { return 'name'; } protected function getPagingValue($result) { return $result->getName(); } protected function getReversePaging() { return true; } protected function loadPage() { $table = new PhabricatorProject(); $conn_r = $table->establishConnection('r'); // NOTE: Because visibility checks for projects depend on whether or not // the user is a project member, we always load their membership. If we're // loading all members anyway we can piggyback on that; otherwise we // do an explicit join. $select_clause = ''; if (!$this->needMembers) { $select_clause = ', vm.dst viewerIsMember'; } $data = queryfx_all( $conn_r, 'SELECT p.* %Q FROM %T p %Q %Q %Q %Q %Q', $select_clause, $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $projects = $table->loadAllFromArray($data); if ($projects) { $viewer_phid = $this->getViewer()->getPHID(); $project_phids = mpull($projects, 'getPHID'); $member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; $need_edge_types = array(); if ($this->needMembers) { $need_edge_types[] = $member_type; } else { foreach ($data as $row) { $projects[$row['id']]->setIsUserMember( $viewer_phid, ($row['viewerIsMember'] !== null)); } } if ($this->needWatchers) { $need_edge_types[] = $watcher_type; } if ($need_edge_types) { $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($project_phids) ->withEdgeTypes($need_edge_types) ->execute(); if ($this->needMembers) { foreach ($projects as $project) { $phid = $project->getPHID(); $project->attachMemberPHIDs( array_keys($edges[$phid][$member_type])); $project->setIsUserMember( $viewer_phid, isset($edges[$phid][$member_type][$viewer_phid])); } } if ($this->needWatchers) { foreach ($projects as $project) { $phid = $project->getPHID(); $project->attachWatcherPHIDs( array_keys($edges[$phid][$watcher_type])); $project->setIsUserWatcher( $viewer_phid, isset($edges[$phid][$watcher_type][$viewer_phid])); } } } } return $projects; } protected function didFilterPage(array $projects) { if ($this->needImages) { $default = null; $file_phids = mpull($projects, 'getProfileImagePHID'); $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); foreach ($projects as $project) { $file = idx($files, $project->getProfileImagePHID()); if (!$file) { if (!$default) { $default = PhabricatorFile::loadBuiltin( $this->getViewer(), 'project.png'); } $file = $default; } $project->attachProfileImageFile($file); } } if ($this->needSlugs) { $slugs = id(new PhabricatorProjectSlug()) ->loadAllWhere( 'projectPHID IN (%Ls)', mpull($projects, 'getPHID')); $slugs = mgroup($slugs, 'getProjectPHID'); foreach ($projects as $project) { $project_slugs = idx($slugs, $project->getPHID(), array()); $project->attachSlugs($project_slugs); } } return $projects; } private function buildWhereClause($conn_r) { $where = array(); 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( "Unknown project status '{$this->status}'!"); } $where[] = qsprintf( $conn_r, 'status IN (%Ld)', $filter); } if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn_r, 'e.dst IN (%Ls)', $this->memberPHIDs); } if ($this->slugs !== null) { $where[] = qsprintf( $conn_r, 'slug.slug IN (%Ls)', $this->slugs); } if ($this->phrictionSlugs !== null) { $where[] = qsprintf( $conn_r, 'phrictionSlug IN (%Ls)', $this->phrictionSlugs); } if ($this->names !== null) { $where[] = qsprintf( $conn_r, 'name IN (%Ls)', $this->names); } if ($this->icons !== null) { $where[] = qsprintf( $conn_r, 'icon IN (%Ls)', $this->icons); } if ($this->colors !== null) { $where[] = qsprintf( $conn_r, 'color IN (%Ls)', $this->colors); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function buildGroupClause($conn_r) { if ($this->memberPHIDs || $this->datasourceQuery) { return 'GROUP BY p.id'; } else { return $this->buildApplicationSearchGroupClause($conn_r); } } private function buildJoinClause($conn_r) { $joins = array(); if (!$this->needMembers !== null) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $this->getViewer()->getPHID()); } if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T e ON e.src = p.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST); } if ($this->slugs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T slug on slug.projectPHID = p.phid', id(new PhabricatorProjectSlug())->getTableName()); } if ($this->datasourceQuery !== null) { $tokens = PhabricatorTypeaheadDatasource::tokenizeString( $this->datasourceQuery); if (!$tokens) { throw new PhabricatorEmptyQueryException(); } $likes = array(); foreach ($tokens as $token) { $likes[] = qsprintf($conn_r, 'token.token LIKE %>', $token); } $joins[] = qsprintf( $conn_r, 'JOIN %T token ON token.projectID = p.id AND (%Q)', PhabricatorProject::TABLE_DATASOURCE_TOKEN, '('.implode(') OR (', $likes).')'); } $joins[] = $this->buildApplicationSearchJoinClause($conn_r); return implode(' ', $joins); } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } - protected function getApplicationSearchObjectPHIDColumn() { - return 'p.phid'; + protected function getPrimaryTableAlias() { + return 'p'; } } diff --git a/src/applications/slowvote/query/PhabricatorSlowvoteQuery.php b/src/applications/slowvote/query/PhabricatorSlowvoteQuery.php index f1ec5358f4..d17672b5e3 100644 --- a/src/applications/slowvote/query/PhabricatorSlowvoteQuery.php +++ b/src/applications/slowvote/query/PhabricatorSlowvoteQuery.php @@ -1,185 +1,185 @@ ids = $ids; return $this; } public function withPHIDs($phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs($author_phids) { $this->authorPHIDs = $author_phids; return $this; } public function withVotesByViewer($with_vote) { $this->withVotesByViewer = $with_vote; return $this; } public function withIsClosed($with_closed) { $this->isClosed = $with_closed; return $this; } public function needOptions($need_options) { $this->needOptions = $need_options; return $this; } public function needChoices($need_choices) { $this->needChoices = $need_choices; return $this; } public function needViewerChoices($need_viewer_choices) { $this->needViewerChoices = $need_viewer_choices; return $this; } protected function loadPage() { $table = new PhabricatorSlowvotePoll(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT p.* FROM %T p %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinsClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $polls) { assert_instances_of($polls, 'PhabricatorSlowvotePoll'); $ids = mpull($polls, 'getID'); $viewer = $this->getViewer(); if ($this->needOptions) { $options = id(new PhabricatorSlowvoteOption())->loadAllWhere( 'pollID IN (%Ld)', $ids); $options = mgroup($options, 'getPollID'); foreach ($polls as $poll) { $poll->attachOptions(idx($options, $poll->getID(), array())); } } if ($this->needChoices) { $choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( 'pollID IN (%Ld)', $ids); $choices = mgroup($choices, 'getPollID'); foreach ($polls as $poll) { $poll->attachChoices(idx($choices, $poll->getID(), array())); } // If we need the viewer's choices, we can just fill them from the data // we already loaded. if ($this->needViewerChoices) { foreach ($polls as $poll) { $poll->attachViewerChoices( $viewer, idx( mgroup($poll->getChoices(), 'getAuthorPHID'), $viewer->getPHID(), array())); } } } else if ($this->needViewerChoices) { $choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( 'pollID IN (%Ld) AND authorPHID = %s', $ids, $viewer->getPHID()); $choices = mgroup($choices, 'getPollID'); foreach ($polls as $poll) { $poll->attachViewerChoices( $viewer, idx($choices, $poll->getID(), array())); } } return $polls; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'p.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'p.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs) { $where[] = qsprintf( $conn_r, 'p.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->isClosed !== null) { $where[] = qsprintf( $conn_r, 'p.isClosed = %d', (int)$this->isClosed); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if ($this->withVotesByViewer) { $joins[] = qsprintf( $conn_r, 'JOIN %T vv ON vv.pollID = p.id AND vv.authorPHID = %s', id(new PhabricatorSlowvoteChoice())->getTableName(), $this->getViewer()->getPHID()); } return implode(' ', $joins); } - protected function getPagingColumn() { - return 'p.id'; + protected function getPrimaryTableAlias() { + return 'p'; } public function getQueryApplicationClass() { return 'PhabricatorSlowvoteApplication'; } } diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 2f89890058..af8b492bab 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -1,902 +1,925 @@ getID(); } protected function getReversePaging() { return false; } protected function nextPage(array $page) { // See getPagingViewer() for a description of this flag. $this->internalPaging = true; if ($this->beforeID) { $this->beforeID = $this->getPagingValue(last($page)); } else { $this->afterID = $this->getPagingValue(last($page)); } } final public function setAfterID($object_id) { $this->afterID = $object_id; return $this; } final protected function getAfterID() { return $this->afterID; } final public function setBeforeID($object_id) { $this->beforeID = $object_id; return $this; } final protected function getBeforeID() { return $this->beforeID; } /** * Get the viewer for making cursor paging queries. * * NOTE: You should ONLY use this viewer to load cursor objects while * building paging queries. * * Cursor paging can happen in two ways. First, the user can request a page * like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we * can fall back to implicit paging if we filter some results out of a * result list because the user can't see them and need to go fetch some more * results to generate a large enough result list. * * In the first case, want to use the viewer's policies to load the object. * This prevents an attacker from figuring out information about an object * they can't see by executing queries like `/stuff/?after=33&order=name`, * which would otherwise give them a hint about the name of the object. * Generally, if a user can't see an object, they can't use it to page. * * In the second case, we need to load the object whether the user can see * it or not, because we need to examine new results. For example, if a user * loads `/stuff/` and we run a query for the first 100 items that they can * see, but the first 100 rows in the database aren't visible, we need to * be able to issue a query for the next 100 results. If we can't load the * cursor object, we'll fail or issue the same query over and over again. * So, generally, internal paging must bypass policy controls. * * This method returns the appropriate viewer, based on the context in which * the paging is occuring. * * @return PhabricatorUser Viewer for executing paging queries. */ final protected function getPagingViewer() { if ($this->internalPaging) { return PhabricatorUser::getOmnipotentUser(); } else { return $this->getViewer(); } } final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) { if ($this->getRawResultLimit()) { return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit()); } else { return ''; } } final protected function didLoadResults(array $results) { if ($this->beforeID) { $results = array_reverse($results, $preserve_keys = true); } return $results; } final public function executeWithCursorPager(AphrontCursorPagerView $pager) { $this->setLimit($pager->getPageSize() + 1); if ($pager->getAfterID()) { $this->setAfterID($pager->getAfterID()); } else if ($pager->getBeforeID()) { $this->setBeforeID($pager->getBeforeID()); } $results = $this->execute(); $sliced_results = $pager->sliceResults($results); if ($sliced_results) { if ($pager->getBeforeID() || (count($results) > $pager->getPageSize())) { $pager->setNextPageID($this->getPagingValue(last($sliced_results))); } if ($pager->getAfterID() || ($pager->getBeforeID() && (count($results) > $pager->getPageSize()))) { $pager->setPrevPageID($this->getPagingValue(head($sliced_results))); } } return $sliced_results; } + /** + * 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; + } + + /* -( Paging )------------------------------------------------------------- */ protected function buildPagingClause(AphrontDatabaseConnection $conn) { $orderable = $this->getOrderableColumns(); // TODO: Remove this once subqueries modernize. if (!$orderable) { if ($this->beforeID) { return qsprintf( $conn, '%Q %Q %s', $this->getPagingColumn(), $this->getReversePaging() ? '<' : '>', $this->beforeID); } else if ($this->afterID) { return qsprintf( $conn, '%Q %Q %s', $this->getPagingColumn(), $this->getReversePaging() ? '>' : '<', $this->afterID); } else { return null; } } $vector = $this->getOrderVector(); if ($this->beforeID !== null) { $cursor = $this->beforeID; $reversed = true; } else if ($this->afterID !== null) { $cursor = $this->afterID; $reversed = false; } else { // No paging is being applied to this query so we do not need to // construct a paging clause. return ''; } $keys = array(); foreach ($vector as $order) { $keys[] = $order->getOrderKey(); } $value_map = $this->getPagingValueMap($cursor, $keys); $columns = array(); foreach ($vector as $order) { $key = $order->getOrderKey(); if (!array_key_exists($key, $value_map)) { throw new Exception( pht( 'Query "%s" failed to return a value from getPagingValueMap() '. 'for column "%s".', get_class($this), $key)); } $column = $orderable[$key]; $column['value'] = $value_map[$key]; $columns[] = $column; } return $this->buildPagingClauseFromMultipleColumns( $conn, $columns, array( 'reversed' => $reversed, )); } protected function getPagingValueMap($cursor, array $keys) { // TODO: This is a hack to make this work with existing classes for now. return array( 'id' => $cursor, ); } /** * 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 Column description dictionaries. * @param map Additional constuction options. * @return string Query clause. */ final protected function buildPagingClauseFromMultipleColumns( AphrontDatabaseConnection $conn, array $columns, array $options) { foreach ($columns as $column) { PhutilTypeSpec::checkMap( $column, array( 'table' => 'optional string|null', 'column' => 'string', 'value' => 'wild', 'type' => 'string', 'reverse' => 'optional bool', 'unique' => 'optional bool', )); } PhutilTypeSpec::checkMap( $options, array( 'reversed' => 'optional bool', )); $is_query_reversed = idx($options, 'reversed', false); $clauses = array(); $accumulated = array(); $last_key = last_key($columns); foreach ($columns as $key => $column) { $type = $column['type']; switch ($type) { case 'null': $value = qsprintf($conn, '%d', ($column['value'] ? 0 : 1)); break; case 'int': $value = qsprintf($conn, '%d', $column['value']); break; case 'float': $value = qsprintf($conn, '%f', $column['value']); break; case 'string': $value = qsprintf($conn, '%s', $column['value']); break; default: throw new Exception("Unknown column type '{$type}'!"); } $is_column_reversed = idx($column, 'reverse', false); $reverse = ($is_query_reversed xor $is_column_reversed); $clause = $accumulated; $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); } if ($type == 'null') { $field = qsprintf($conn, '(%Q IS NULL)', $field); } $clause[] = qsprintf( $conn, '%Q %Q %Q', $field, $reverse ? '>' : '<', $value); $clauses[] = '('.implode(') AND (', $clause).')'; $accumulated[] = qsprintf( $conn, '%Q = %Q', $field, $value); } return '('.implode(') OR (', $clauses).')'; } /* -( Result Ordering )---------------------------------------------------- */ /** * @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; } /** * @task order */ private function getOrderVector() { if (!$this->orderVector) { $vector = $this->getDefaultOrderVector(); $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() { // TODO: Remove this once all subclasses move off the old stuff. if ($this->getPagingColumn() !== 'id') { // This class has bad old custom logic around paging, so return nothing // here. This deactivates the new order code. return array(); } return array( 'id' => array( - 'table' => null, + 'table' => $this->getPrimaryTableAlias(), 'column' => 'id', 'reverse' => false, 'type' => 'int', 'unique' => true, ), ); } /** * @task order */ final protected function buildOrderClause(AphrontDatabaseConnection $conn) { $orderable = $this->getOrderableColumns(); // TODO: Remove this once all subclasses move off the old stuff. We'll // only enter this block for code using older ordering mechanisms. New // code should expose an orderable column list. if (!$orderable) { if ($this->beforeID) { return qsprintf( $conn, 'ORDER BY %Q %Q', $this->getPagingColumn(), $this->getReversePaging() ? 'DESC' : 'ASC'); } else { return qsprintf( $conn, 'ORDER BY %Q %Q', $this->getPagingColumn(), $this->getReversePaging() ? 'ASC' : 'DESC'); } } $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); } /** * @task order */ protected function formatOrderClause( AphrontDatabaseConnection $conn, array $parts) { $is_query_reversed = false; if ($this->getReversePaging()) { $is_query_reversed = !$is_query_reversed; } if ($this->getBeforeID()) { $is_query_reversed = !$is_query_reversed; } $sql = array(); foreach ($parts as $key => $part) { $is_column_reversed = !empty($part['reverse']); $descending = true; if ($is_query_reversed) { $descending = !$descending; } if ($is_column_reversed) { $descending = !$descending; } $table = idx($part, 'table'); $column = $part['column']; if ($descending) { if ($table !== null) { $sql[] = qsprintf($conn, '%T.%T DESC', $table, $column); } else { $sql[] = qsprintf($conn, '%T DESC', $column); } } else { if ($table !== null) { $sql[] = qsprintf($conn, '%T.%T ASC', $table, $column); } else { $sql[] = qsprintf($conn, '%T ASC', $column); } } } return qsprintf($conn, 'ORDER BY %Q', implode(', ', $sql)); } /* -( Application Search )------------------------------------------------- */ /** * Constrain the query with an ApplicationSearch index, requiring field values * contain at least one of the values in a set. * * This constraint can build the most common types of queries, like: * * - Find users with shirt sizes "X" or "XL". * - Find shoes with size "13". * * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. * @param string|list One or more values to filter by. * @return this * @task appsearch */ public function withApplicationSearchContainsConstraint( PhabricatorCustomFieldIndexStorage $index, $value) { $this->applicationSearchConstraints[] = array( 'type' => $index->getIndexValueType(), 'cond' => '=', 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), 'value' => $value, ); return $this; } /** * Constrain the query with an ApplicationSearch index, requiring values * exist in a given range. * * This constraint is useful for expressing date ranges: * * - Find events between July 1st and July 7th. * * The ends of the range are inclusive, so a `$min` of `3` and a `$max` of * `5` will match fields with values `3`, `4`, or `5`. Providing `null` for * either end of the range will leave that end of the constraint open. * * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. * @param int|null Minimum permissible value, inclusive. * @param int|null Maximum permissible value, inclusive. * @return this * @task appsearch */ public function withApplicationSearchRangeConstraint( PhabricatorCustomFieldIndexStorage $index, $min, $max) { $index_type = $index->getIndexValueType(); if ($index_type != 'int') { throw new Exception( pht( 'Attempting to apply a range constraint to a field with index type '. '"%s", expected type "%s".', $index_type, 'int')); } $this->applicationSearchConstraints[] = array( 'type' => $index->getIndexValueType(), 'cond' => 'range', 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), 'value' => array($min, $max), ); return $this; } /** * Order the results by an ApplicationSearch index. * * @param PhabricatorCustomField Field to which the index belongs. * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. * @param bool True to sort ascending. * @return this * @task appsearch */ public function withApplicationSearchOrder( PhabricatorCustomField $field, PhabricatorCustomFieldIndexStorage $index, $ascending) { $this->applicationSearchOrders[] = array( 'key' => $field->getFieldKey(), 'type' => $index->getIndexValueType(), 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), 'ascending' => $ascending, ); return $this; } /** * Get the name of the query's primary object PHID column, for constructing - * JOIN clauses. Normally (and by default) this is just `"phid"`, but if the - * query construction requires a table alias it may be something like - * `"task.phid"`. + * 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. * * @return string Column name. * @task appsearch */ protected function getApplicationSearchObjectPHIDColumn() { - return 'phid'; + if ($this->getPrimaryTableAlias()) { + $prefix = $this->getPrimaryTableAlias().'.'; + } else { + $prefix = ''; + } + + return $prefix.'phid'; } /** * Determine if the JOINs built by ApplicationSearch might cause each primary * object to return multiple result rows. Generally, this means the query * needs an extra GROUP BY clause. * * @return bool True if the query may return multiple rows for each object. * @task appsearch */ protected function getApplicationSearchMayJoinMultipleRows() { foreach ($this->applicationSearchConstraints as $constraint) { $type = $constraint['type']; $value = $constraint['value']; $cond = $constraint['cond']; switch ($cond) { case '=': switch ($type) { case 'string': case 'int': if (count((array)$value) > 1) { return true; } break; default: throw new Exception(pht('Unknown index type "%s"!', $type)); } break; case 'range': // NOTE: It's possible to write a custom field where multiple rows // match a range constraint, but we don't currently ship any in the // upstream and I can't immediately come up with cases where this // would make sense. break; default: throw new Exception(pht('Unknown constraint condition "%s"!', $cond)); } } return false; } /** * Construct a GROUP BY clause appropriate for ApplicationSearch constraints. * * @param AphrontDatabaseConnection Connection executing the query. * @return string Group clause. * @task appsearch */ protected function buildApplicationSearchGroupClause( AphrontDatabaseConnection $conn_r) { if ($this->getApplicationSearchMayJoinMultipleRows()) { return qsprintf( $conn_r, 'GROUP BY %Q', $this->getApplicationSearchObjectPHIDColumn()); } else { return ''; } } /** * Construct a JOIN clause appropriate for applying ApplicationSearch * constraints. * * @param AphrontDatabaseConnection Connection executing the query. * @return string Join clause. * @task appsearch */ protected function buildApplicationSearchJoinClause( AphrontDatabaseConnection $conn_r) { $joins = array(); foreach ($this->applicationSearchConstraints as $key => $constraint) { $table = $constraint['table']; $alias = 'appsearch_'.$key; $index = $constraint['index']; $cond = $constraint['cond']; $phid_column = $this->getApplicationSearchObjectPHIDColumn(); switch ($cond) { case '=': $type = $constraint['type']; switch ($type) { case 'string': $constraint_clause = qsprintf( $conn_r, '%T.indexValue IN (%Ls)', $alias, (array)$constraint['value']); break; case 'int': $constraint_clause = qsprintf( $conn_r, '%T.indexValue IN (%Ld)', $alias, (array)$constraint['value']); break; default: throw new Exception(pht('Unknown index type "%s"!', $type)); } $joins[] = qsprintf( $conn_r, 'JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s AND (%Q)', $table, $alias, $alias, $phid_column, $alias, $index, $constraint_clause); break; case 'range': list($min, $max) = $constraint['value']; if (($min === null) && ($max === null)) { // If there's no actual range constraint, just move on. break; } if ($min === null) { $constraint_clause = qsprintf( $conn_r, '%T.indexValue <= %d', $alias, $max); } else if ($max === null) { $constraint_clause = qsprintf( $conn_r, '%T.indexValue >= %d', $alias, $min); } else { $constraint_clause = qsprintf( $conn_r, '%T.indexValue BETWEEN %d AND %d', $alias, $min, $max); } $joins[] = qsprintf( $conn_r, 'JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s AND (%Q)', $table, $alias, $alias, $phid_column, $alias, $index, $constraint_clause); break; default: throw new Exception(pht('Unknown constraint condition "%s"!', $cond)); } } foreach ($this->applicationSearchOrders as $key => $order) { $table = $order['table']; $alias = 'appsearch_order_'.$key; $index = $order['index']; $phid_column = $this->getApplicationSearchObjectPHIDColumn(); $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s', $table, $alias, $alias, $phid_column, $alias, $index); } return implode(' ', $joins); } protected function buildApplicationSearchOrders( AphrontDatabaseConnection $conn_r, $reverse) { $orders = array(); foreach ($this->applicationSearchOrders as $key => $order) { $alias = 'appsearch_order_'.$key; if ($order['ascending'] xor $reverse) { $orders[] = qsprintf($conn_r, '%T.indexValue ASC', $alias); } else { $orders[] = qsprintf($conn_r, '%T.indexValue DESC', $alias); } } return $orders; } protected function buildApplicationSearchPagination( AphrontDatabaseConnection $conn_r, $cursor) { // We have to get the current field values on the cursor object. $fields = PhabricatorCustomField::getObjectFields( $cursor, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->setViewer($this->getViewer()); $fields->readFieldsFromStorage($cursor); $fields = mpull($fields->getFields(), null, 'getFieldKey'); $columns = array(); foreach ($this->applicationSearchOrders as $key => $order) { $alias = 'appsearch_order_'.$key; $field = idx($fields, $order['key']); $columns[] = array( 'name' => $alias.'.indexValue', 'value' => $field->getValueForStorage(), 'type' => $order['type'], ); } return $columns; } }