diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php index d961b2e4ab..85217d1247 100644 --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -1,224 +1,224 @@ 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 withServiceClasses(array $classes) { $this->serviceClasses = $classes; return $this; } public function withDevicePHIDs(array $phids) { $this->devicePHIDs = $phids; return $this; } public function withLocked($locked) { $this->locked = $locked; return $this; } public function withNamePrefix($prefix) { $this->namePrefix = $prefix; return $this; } public function withNameSuffix($suffix) { $this->nameSuffix = $suffix; return $this; } public function needBindings($need_bindings) { $this->needBindings = $need_bindings; return $this; } protected function loadPage() { $table = new AlmanacService(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT service.* FROM %T service %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - protected function buildJoinClause($conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if ($this->devicePHIDs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T binding ON service.phid = binding.servicePHID', id(new AlmanacBinding())->getTableName()); } return implode(' ', $joins); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'service.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'service.phid IN (%Ls)', $this->phids); } if ($this->names !== null) { $hashes = array(); foreach ($this->names as $name) { $hashes[] = PhabricatorHash::digestForIndex($name); } $where[] = qsprintf( $conn_r, 'service.nameIndex IN (%Ls)', $hashes); } if ($this->serviceClasses !== null) { $where[] = qsprintf( $conn_r, 'service.serviceClass IN (%Ls)', $this->serviceClasses); } if ($this->devicePHIDs !== null) { $where[] = qsprintf( $conn_r, 'binding.devicePHID IN (%Ls)', $this->devicePHIDs); } if ($this->locked !== null) { $where[] = qsprintf( $conn_r, 'service.isLocked = %d', (int)$this->locked); } if ($this->namePrefix !== null) { $where[] = qsprintf( $conn_r, 'service.name LIKE %>', $this->namePrefix); } if ($this->nameSuffix !== null) { $where[] = qsprintf( $conn_r, 'service.name LIKE %<', $this->nameSuffix); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } protected function willFilterPage(array $services) { $service_types = AlmanacServiceType::getAllServiceTypes(); foreach ($services as $key => $service) { $service_class = $service->getServiceClass(); $service_type = idx($service_types, $service_class); if (!$service_type) { $this->didRejectResult($service); unset($services[$key]); continue; } $service->attachServiceType($service_type); } 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) ->execute(); $bindings = mgroup($bindings, 'getServicePHID'); foreach ($services as $service) { $service_bindings = idx($bindings, $service->getPHID(), array()); $service->attachBindings($service_bindings); } } return parent::didFilterPage($services); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => 'service', 'column' => 'name', 'type' => 'string', 'unique' => true, 'reverse' => true, ), ); } protected function getValueMap($cursor, array $keys) { $service = $this->loadCursorObject($cursor); return array( 'id' => $service->getID(), 'name' => $service->getServiceName(), ); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Service Name'), ), ) + parent::getBuiltinOrders(); } } diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index c191a763b4..b7be434fa3 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -1,383 +1,383 @@ needFilePHIDs = $need_file_phids; return $this; } public function needParticipantCache($participant_cache) { $this->needParticipantCache = $participant_cache; return $this; } public function needParticipants($need) { $this->needParticipants = $need; return $this; } public function needWidgetData($need_widget_data) { $this->needWidgetData = $need_widget_data; return $this; } public function needTransactions($need_transactions) { $this->needTransactions = $need_transactions; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withParticipantPHIDs(array $phids) { $this->participantPHIDs = $phids; return $this; } public function withIsRoom($bool) { $this->isRoom = $bool; return $this; } public function setAfterTransactionID($id) { $this->afterTransactionID = $id; return $this; } public function setBeforeTransactionID($id) { $this->beforeTransactionID = $id; return $this; } public function setTransactionLimit($transaction_limit) { $this->transactionLimit = $transaction_limit; return $this; } public function getTransactionLimit() { return $this->transactionLimit; } protected function loadPage() { $table = new ConpherenceThread(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT conpherence_thread.* FROM %T conpherence_thread %Q %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $conpherences = $table->loadAllFromArray($data); if ($conpherences) { $conpherences = mpull($conpherences, null, 'getPHID'); $this->loadParticipantsAndInitHandles($conpherences); if ($this->needParticipantCache) { $this->loadCoreHandles($conpherences, 'getRecentParticipantPHIDs'); } if ($this->needWidgetData || $this->needParticipants) { $this->loadCoreHandles($conpherences, 'getParticipantPHIDs'); } if ($this->needTransactions) { $this->loadTransactionsAndHandles($conpherences); } if ($this->needFilePHIDs || $this->needWidgetData) { $this->loadFilePHIDs($conpherences); } if ($this->needWidgetData) { $this->loadWidgetData($conpherences); } } return $conpherences; } private function buildGroupClause($conn_r) { if ($this->participantPHIDs !== null) { return 'GROUP BY conpherence_thread.id'; } else { return $this->buildApplicationSearchGroupClause($conn_r); } } - private function buildJoinClause($conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if ($this->participantPHIDs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T p ON p.conpherencePHID = conpherence_thread.phid', id(new ConpherenceParticipant())->getTableName()); } $viewer = $this->getViewer(); if ($this->shouldJoinForViewer($viewer)) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T v ON v.conpherencePHID = conpherence_thread.phid '. 'AND v.participantPHID = %s', id(new ConpherenceParticipant())->getTableName(), $viewer->getPHID()); } $joins[] = $this->buildApplicationSearchJoinClause($conn_r); return implode(' ', $joins); } private function shouldJoinForViewer(PhabricatorUser $viewer) { if ($viewer->isLoggedIn() && $this->ids === null && $this->phids === null) { return true; } return false; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'conpherence_thread.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'conpherence_thread.phid IN (%Ls)', $this->phids); } if ($this->participantPHIDs !== null) { $where[] = qsprintf( $conn_r, 'p.participantPHID IN (%Ls)', $this->participantPHIDs); } if ($this->isRoom !== null) { $where[] = qsprintf( $conn_r, 'conpherence_thread.isRoom = %d', (int)$this->isRoom); } $viewer = $this->getViewer(); if ($this->shouldJoinForViewer($viewer)) { $where[] = qsprintf( $conn_r, 'conpherence_thread.isRoom = 1 OR v.participantPHID IS NOT NULL'); } else if ($this->phids === null && $this->ids === null) { $where[] = qsprintf( $conn_r, 'conpherence_thread.isRoom = 1'); } return $this->formatWhereClause($where); } private function loadParticipantsAndInitHandles(array $conpherences) { $participants = id(new ConpherenceParticipant()) ->loadAllWhere('conpherencePHID IN (%Ls)', array_keys($conpherences)); $map = mgroup($participants, 'getConpherencePHID'); foreach ($conpherences as $current_conpherence) { $conpherence_phid = $current_conpherence->getPHID(); $conpherence_participants = idx( $map, $conpherence_phid, array()); $conpherence_participants = mpull( $conpherence_participants, null, 'getParticipantPHID'); $current_conpherence->attachParticipants($conpherence_participants); $current_conpherence->attachHandles(array()); } return $this; } private function loadCoreHandles( array $conpherences, $method) { $handle_phids = array(); foreach ($conpherences as $conpherence) { $handle_phids[$conpherence->getPHID()] = $conpherence->$method(); } $flat_phids = array_mergev($handle_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($flat_phids) ->execute(); foreach ($handle_phids as $conpherence_phid => $phids) { $conpherence = $conpherences[$conpherence_phid]; $conpherence->attachHandles( $conpherence->getHandles() + array_select_keys($handles, $phids)); } return $this; } private function loadTransactionsAndHandles(array $conpherences) { $query = id(new ConpherenceTransactionQuery()) ->setViewer($this->getViewer()) ->withObjectPHIDs(array_keys($conpherences)) ->needHandles(true); // We have to flip these for the underyling query class. The semantics of // paging are tricky business. if ($this->afterTransactionID) { $query->setBeforeID($this->afterTransactionID); } else if ($this->beforeTransactionID) { $query->setAfterID($this->beforeTransactionID); } if ($this->getTransactionLimit()) { // fetch an extra for "show older" scenarios $query->setLimit($this->getTransactionLimit() + 1); } $transactions = $query->execute(); $transactions = mgroup($transactions, 'getObjectPHID'); foreach ($conpherences as $phid => $conpherence) { $current_transactions = idx($transactions, $phid, array()); $handles = array(); foreach ($current_transactions as $transaction) { $handles += $transaction->getHandles(); } $conpherence->attachHandles($conpherence->getHandles() + $handles); $conpherence->attachTransactions($current_transactions); } return $this; } private function loadFilePHIDs(array $conpherences) { $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST; $file_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array_keys($conpherences)) ->withEdgeTypes(array($edge_type)) ->execute(); foreach ($file_edges as $conpherence_phid => $data) { $conpherence = $conpherences[$conpherence_phid]; $conpherence->attachFilePHIDs(array_keys($data[$edge_type])); } return $this; } private function loadWidgetData(array $conpherences) { $participant_phids = array(); $file_phids = array(); foreach ($conpherences as $conpherence) { $participant_phids[] = array_keys($conpherence->getParticipants()); $file_phids[] = $conpherence->getFilePHIDs(); } $participant_phids = array_mergev($participant_phids); $file_phids = array_mergev($file_phids); $epochs = CalendarTimeUtil::getCalendarEventEpochs( $this->getViewer()); $start_epoch = $epochs['start_epoch']; $end_epoch = $epochs['end_epoch']; $statuses = id(new PhabricatorCalendarEventQuery()) ->setViewer($this->getViewer()) ->withInvitedPHIDs($participant_phids) ->withDateRange($start_epoch, $end_epoch) ->execute(); $statuses = mgroup($statuses, 'getUserPHID'); // attached files $files = array(); $file_author_phids = array(); $authors = array(); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $file_author_phids = mpull($files, 'getAuthorPHID', 'getPHID'); $authors = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($file_author_phids) ->execute(); $authors = mpull($authors, null, 'getPHID'); } foreach ($conpherences as $phid => $conpherence) { $participant_phids = array_keys($conpherence->getParticipants()); $statuses = array_select_keys($statuses, $participant_phids); $statuses = array_mergev($statuses); $statuses = msort($statuses, 'getDateFrom'); $conpherence_files = array(); $files_authors = array(); foreach ($conpherence->getFilePHIDs() as $curr_phid) { $curr_file = idx($files, $curr_phid); if (!$curr_file) { // this file was deleted or user doesn't have permission to see it // this is generally weird continue; } $conpherence_files[$curr_phid] = $curr_file; // some files don't have authors so be careful $current_author = null; $current_author_phid = idx($file_author_phids, $curr_phid); if ($current_author_phid) { $current_author = $authors[$current_author_phid]; } $files_authors[$curr_phid] = $current_author; } $widget_data = array( 'statuses' => $statuses, 'files' => $conpherence_files, 'files_authors' => $files_authors, ); $conpherence->attachWidgetData($widget_data); } return $this; } public function getQueryApplicationClass() { return 'PhabricatorConpherenceApplication'; } } diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index 8fd9d0c73e..e6f67e44c8 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -1,566 +1,566 @@ 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) { $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 underyling 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; } /** * 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 = $repository_ids; return $this; } public function needCommitData($need) { $this->needCommitData = $need; return $this; } public function needAuditRequests($need) { $this->needAuditRequests = $need; return $this; } /** * Returns true if we should join the audit table, either because we're * interested in the information if it's available or because matching rows * must always have it. */ private function shouldJoinAudits() { return $this->auditStatus || $this->rowsMustHaveAudits(); } /** * Return true if we should `JOIN` (vs `LEFT JOIN`) the audit table, because * matching commits will always have audit rows. */ private function rowsMustHaveAudits() { return $this->auditIDs || $this->auditorPHIDs || $this->auditAwaitingUser; } public function withAuditIDs(array $ids) { $this->auditIDs = $ids; return $this; } public function withAuditorPHIDs(array $auditor_phids) { $this->auditorPHIDs = $auditor_phids; return $this; } public function withAuditAwaitingUser(PhabricatorUser $user) { $this->auditAwaitingUser = $user; return $this; } public function withAuditStatus($status) { $this->auditStatus = $status; return $this; } public function getIdentifierMap() { if ($this->identifierMap === null) { throw new Exception( 'You must execute() the query before accessing the identifier map.'); } return $this->identifierMap; } protected function getPrimaryTableAlias() { return 'commit'; } protected function willExecute() { if ($this->identifierMap === null) { $this->identifierMap = array(); } } protected function loadPage() { $table = new PhabricatorRepositoryCommit(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT commit.* FROM %T commit %Q %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } 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 { unset($commits[$key]); continue; } // Build the identifierMap if ($this->identifiers !== null) { $ids = array_fuse($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) { 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); } } // TODO: This should just be `needAuditRequests`, not `shouldJoinAudits()`, // but leave that for a future diff. if ($this->needAuditRequests || $this->shouldJoinAudits()) { $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); } } } return $commits; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); 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->ids !== null) { $where[] = qsprintf( $conn_r, 'commit.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'commit.phid IN (%Ls)', $this->phids); } if ($this->repositoryIDs !== null) { $where[] = qsprintf( $conn_r, 'commit.repositoryID IN (%Ld)', $this->repositoryIDs); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn_r, 'commit.authorPHID IN (%Ls)', $this->authorPHIDs); } 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->getCallsign(); } } if ($repo === null) { if (strlen($commit_identifier) < $min_unqualified) { continue; } $bare[] = $commit_identifier; } else { $refs[] = array( 'callsign' => $repo, 'identifier' => $commit_identifier, ); } } $sql = array(); foreach ($bare as $identifier) { $sql[] = qsprintf( $conn_r, '(commit.commitIdentifier LIKE %> AND '. 'LENGTH(commit.commitIdentifier) = 40)', $identifier); } if ($refs) { $callsigns = ipull($refs, 'callsign'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIdentifiers($callsigns); $repos->execute(); $repos = $repos->getIdentifierMap(); foreach ($refs as $key => $ref) { $repo = idx($repos, $ref['callsign']); if (!$repo) { continue; } if ($repo->isSVN()) { if (!ctype_digit($ref['identifier'])) { continue; } $sql[] = qsprintf( $conn_r, '(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; } $sql[] = qsprintf( $conn_r, '(commit.repositoryID = %d AND commit.commitIdentifier LIKE %>)', $repo->getID(), $ref['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[] = '('.implode(' OR ', $sql).')'; } if ($this->auditIDs !== null) { $where[] = qsprintf( $conn_r, 'audit.id IN (%Ld)', $this->auditIDs); } if ($this->auditorPHIDs !== null) { $where[] = qsprintf( $conn_r, 'audit.auditorPHID IN (%Ls)', $this->auditorPHIDs); } if ($this->auditAwaitingUser) { $awaiting_user_phid = $this->auditAwaitingUser->getPHID(); // Exclude package and project audits associated with commits where // the user is the author. $where[] = qsprintf( $conn_r, '(commit.authorPHID IS NULL OR commit.authorPHID != %s) OR (audit.auditorPHID = %s)', $awaiting_user_phid, $awaiting_user_phid); } $status = $this->auditStatus; if ($status !== null) { switch ($status) { case self::AUDIT_STATUS_PARTIAL: $where[] = qsprintf( $conn_r, 'commit.auditStatus = %d', PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED); break; case self::AUDIT_STATUS_ACCEPTED: $where[] = qsprintf( $conn_r, 'commit.auditStatus = %d', PhabricatorAuditCommitStatusConstants::FULLY_AUDITED); break; case self::AUDIT_STATUS_CONCERN: $where[] = qsprintf( $conn_r, 'audit.auditStatus = %s', PhabricatorAuditStatusConstants::CONCERNED); break; case self::AUDIT_STATUS_OPEN: $where[] = qsprintf( $conn_r, 'audit.auditStatus in (%Ls)', PhabricatorAuditStatusConstants::getOpenStatusConstants()); if ($this->auditAwaitingUser) { $where[] = qsprintf( $conn_r, 'awaiting.auditStatus IS NULL OR awaiting.auditStatus != %s', PhabricatorAuditStatusConstants::RESIGNED); } break; case self::AUDIT_STATUS_ANY: break; default: $valid = array( self::AUDIT_STATUS_ANY, self::AUDIT_STATUS_OPEN, self::AUDIT_STATUS_CONCERN, self::AUDIT_STATUS_ACCEPTED, self::AUDIT_STATUS_PARTIAL, ); throw new Exception( "Unknown audit status '{$status}'! Valid statuses are: ". implode(', ', $valid)); } } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($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 buildJoinClause($conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); $audit_request = new PhabricatorRepositoryAuditRequest(); if ($this->shouldJoinAudits()) { $joins[] = qsprintf( $conn_r, '%Q %T audit ON commit.phid = audit.commitPHID', ($this->rowsMustHaveAudits() ? 'JOIN' : 'LEFT JOIN'), $audit_request->getTableName()); } if ($this->auditAwaitingUser) { // Join the request table on the awaiting user's requests, so we can // filter out package and project requests which the user has resigned // from. $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T awaiting ON audit.commitPHID = awaiting.commitPHID AND awaiting.auditorPHID = %s', $audit_request->getTableName(), $this->auditAwaitingUser->getPHID()); } if ($joins) { return implode(' ', $joins); } else { return ''; } } private function buildGroupClause(AphrontDatabaseConnection $conn_r) { $should_group = $this->shouldJoinAudits(); // TODO: Currently, the audit table is missing a unique key, so we may // require a GROUP BY if we perform this join. See T1768. This can be // removed once the table has the key. if ($this->auditAwaitingUser) { $should_group = true; } if ($should_group) { return 'GROUP BY commit.id'; } else { return ''; } } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } } diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php index 424658b342..29be87c428 100644 --- a/src/applications/feed/query/PhabricatorFeedQuery.php +++ b/src/applications/feed/query/PhabricatorFeedQuery.php @@ -1,126 +1,126 @@ filterPHIDs = $phids; return $this; } public function withChronologicalKeys(array $keys) { $this->chronologicalKeys = $keys; return $this; } protected function loadPage() { $story_table = new PhabricatorFeedStoryData(); $conn = $story_table->establishConnection('r'); $data = queryfx_all( $conn, 'SELECT story.* FROM %T story %Q %Q %Q %Q %Q', $story_table->getTableName(), $this->buildJoinClause($conn), $this->buildWhereClause($conn), $this->buildGroupClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $data; } protected function willFilterPage(array $data) { return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer()); } - private function buildJoinClause(AphrontDatabaseConnection $conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { // NOTE: We perform this join unconditionally (even if we have no filter // PHIDs) to omit rows which have no story references. These story data // rows are notifications or realtime alerts. $ref_table = new PhabricatorFeedStoryReference(); return qsprintf( $conn_r, 'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey', $ref_table->getTableName()); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->filterPHIDs) { $where[] = qsprintf( $conn_r, 'ref.objectPHID IN (%Ls)', $this->filterPHIDs); } if ($this->chronologicalKeys) { // NOTE: We want to use integers in the query so we can take advantage // of keys, but can't use %d on 32-bit systems. Make sure all the keys // are integers and then format them raw. $keys = $this->chronologicalKeys; foreach ($keys as $key) { if (!ctype_digit($key)) { throw new Exception("Key '{$key}' is not a valid chronological key!"); } } $where[] = qsprintf( $conn_r, 'ref.chronologicalKey IN (%Q)', implode(', ', $keys)); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function buildGroupClause(AphrontDatabaseConnection $conn_r) { if ($this->filterPHIDs) { return qsprintf($conn_r, 'GROUP BY ref.chronologicalKey'); } else { return qsprintf($conn_r, 'GROUP BY story.chronologicalKey'); } } protected function getDefaultOrderVector() { return array('key'); } public function getOrderableColumns() { $table = ($this->filterPHIDs ? 'ref' : 'story'); return array( 'key' => array( 'table' => $table, 'column' => 'chronologicalKey', 'type' => 'int', 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { return array( 'key' => $cursor, ); } protected function getResultCursor($item) { if ($item instanceof PhabricatorFeedStory) { return $item->getChronologicalKey(); } return $item['chronologicalKey']; } public function getQueryApplicationClass() { return 'PhabricatorFeedApplication'; } } diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php index a15be0c0bb..aa4e0be16c 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) { + protected 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); } protected 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 getPrimaryTableAlias() { return 'f'; } public function getQueryApplicationClass() { return 'PhabricatorFilesApplication'; } } diff --git a/src/applications/legalpad/query/LegalpadDocumentQuery.php b/src/applications/legalpad/query/LegalpadDocumentQuery.php index e2e43e0e28..4a5f2c8663 100644 --- a/src/applications/legalpad/query/LegalpadDocumentQuery.php +++ b/src/applications/legalpad/query/LegalpadDocumentQuery.php @@ -1,278 +1,278 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withCreatorPHIDs(array $phids) { $this->creatorPHIDs = $phids; return $this; } public function withContributorPHIDs(array $phids) { $this->contributorPHIDs = $phids; return $this; } public function withSignerPHIDs(array $phids) { $this->signerPHIDs = $phids; return $this; } public function withSignatureRequired($bool) { $this->signatureRequired = $bool; return $this; } public function needDocumentBodies($need_bodies) { $this->needDocumentBodies = $need_bodies; return $this; } public function needContributors($need_contributors) { $this->needContributors = $need_contributors; return $this; } public function needSignatures($need_signatures) { $this->needSignatures = $need_signatures; 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 needViewerSignatures($need) { $this->needViewerSignatures = $need; return $this; } protected function loadPage() { $table = new LegalpadDocument(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT d.* FROM %T d %Q %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildGroupClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $documents = $table->loadAllFromArray($data); return $documents; } protected function willFilterPage(array $documents) { if ($this->needDocumentBodies) { $documents = $this->loadDocumentBodies($documents); } if ($this->needContributors) { $documents = $this->loadContributors($documents); } if ($this->needSignatures) { $documents = $this->loadSignatures($documents); } if ($this->needViewerSignatures) { if ($documents) { if ($this->getViewer()->getPHID()) { $signatures = id(new LegalpadDocumentSignatureQuery()) ->setViewer($this->getViewer()) ->withSignerPHIDs(array($this->getViewer()->getPHID())) ->withDocumentPHIDs(mpull($documents, 'getPHID')) ->execute(); $signatures = mpull($signatures, null, 'getDocumentPHID'); } else { $signatures = array(); } foreach ($documents as $document) { $signature = idx($signatures, $document->getPHID()); $document->attachUserSignature( $this->getViewer()->getPHID(), $signature); } } } return $documents; } - private function buildJoinClause($conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if ($this->contributorPHIDs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN edge contributor ON contributor.src = d.phid AND contributor.type = %d', PhabricatorObjectHasContributorEdgeType::EDGECONST); } if ($this->signerPHIDs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T signer ON signer.documentPHID = d.phid AND signer.signerPHID IN (%Ls)', id(new LegalpadDocumentSignature())->getTableName(), $this->signerPHIDs); } return implode(' ', $joins); } private function buildGroupClause(AphrontDatabaseConnection $conn_r) { if ($this->contributorPHIDs || $this->signerPHIDs) { return 'GROUP BY d.id'; } else { return ''; } } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'd.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'd.phid IN (%Ls)', $this->phids); } if ($this->creatorPHIDs !== null) { $where[] = qsprintf( $conn_r, 'd.creatorPHID IN (%Ls)', $this->creatorPHIDs); } if ($this->dateCreatedAfter !== null) { $where[] = qsprintf( $conn_r, 'd.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore !== null) { $where[] = qsprintf( $conn_r, 'd.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->contributorPHIDs !== null) { $where[] = qsprintf( $conn_r, 'contributor.dst IN (%Ls)', $this->contributorPHIDs); } if ($this->signatureRequired !== null) { $where[] = qsprintf( $conn_r, 'd.requireSignature = %d', $this->signatureRequired); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function loadDocumentBodies(array $documents) { $body_phids = mpull($documents, 'getDocumentBodyPHID'); $bodies = id(new LegalpadDocumentBody())->loadAllWhere( 'phid IN (%Ls)', $body_phids); $bodies = mpull($bodies, null, 'getPHID'); foreach ($documents as $document) { $body = idx($bodies, $document->getDocumentBodyPHID()); $document->attachDocumentBody($body); } return $documents; } private function loadContributors(array $documents) { $document_map = mpull($documents, null, 'getPHID'); $edge_type = PhabricatorObjectHasContributorEdgeType::EDGECONST; $contributor_data = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array_keys($document_map)) ->withEdgeTypes(array($edge_type)) ->execute(); foreach ($document_map as $document_phid => $document) { $data = $contributor_data[$document_phid]; $contributors = array_keys(idx($data, $edge_type, array())); $document->attachContributors($contributors); } return $documents; } private function loadSignatures(array $documents) { $document_map = mpull($documents, null, 'getPHID'); $signatures = id(new LegalpadDocumentSignatureQuery()) ->setViewer($this->getViewer()) ->withDocumentPHIDs(array_keys($document_map)) ->execute(); $signatures = mgroup($signatures, 'getDocumentPHID'); foreach ($documents as $document) { $sigs = idx($signatures, $document->getPHID(), array()); $document->attachSignatures($sigs); } return $documents; } public function getQueryApplicationClass() { return 'PhabricatorLegalpadApplication'; } } diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php index 64640c3ab8..3bbb3b8fe2 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php @@ -1,86 +1,86 @@ ownerPHIDs = $phids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } protected function loadPage() { $table = new PhabricatorOwnersPackage(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT p.* FROM %T p %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } - private function buildJoinClause(AphrontDatabaseConnection $conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if ($this->ownerPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T o ON o.packageID = p.id', id(new PhabricatorOwnersOwner())->getTableName()); } return implode(' ', $joins); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->phids) { $where[] = qsprintf( $conn_r, 'p.phid IN (%Ls)', $this->phids); } if ($this->ownerPHIDs) { $base_phids = $this->ownerPHIDs; $query = new PhabricatorProjectQuery(); $query->setViewer($this->getViewer()); $query->withMemberPHIDs($base_phids); $projects = $query->execute(); $project_phids = mpull($projects, 'getPHID'); $all_phids = array_merge($base_phids, $project_phids); $where[] = qsprintf( $conn_r, 'o.userPHID IN (%Ls)', $all_phids); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorOwnersApplication'; } } diff --git a/src/applications/phortune/query/PhortuneAccountQuery.php b/src/applications/phortune/query/PhortuneAccountQuery.php index a567aa2d38..c91e5f0111 100644 --- a/src/applications/phortune/query/PhortuneAccountQuery.php +++ b/src/applications/phortune/query/PhortuneAccountQuery.php @@ -1,123 +1,123 @@ setViewer($user) ->withMemberPHIDs(array($user->getPHID())) ->execute(); if (!$accounts) { $accounts = array( PhortuneAccount::createNewAccount($user, $content_source), ); } $accounts = mpull($accounts, null, 'getPHID'); return $accounts; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } protected function loadPage() { $table = new PhortuneAccount(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT a.* FROM %T a %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $accounts) { $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($accounts, 'getPHID')) ->withEdgeTypes(array(PhortuneAccountHasMemberEdgeType::EDGECONST)); $query->execute(); foreach ($accounts as $account) { $member_phids = $query->getDestinationPHIDs(array($account->getPHID())); $member_phids = array_reverse($member_phids); $account->attachMemberPHIDs($member_phids); } return $accounts; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); $where[] = $this->buildPagingClause($conn); if ($this->ids) { $where[] = qsprintf( $conn, 'a.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn, 'a.phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs) { $where[] = qsprintf( $conn, 'm.dst IN (%Ls)', $this->memberPHIDs); } return $this->formatWhereClause($where); } - private function buildJoinClause(AphrontDatabaseConnection $conn) { + protected function buildJoinClause(AphrontDatabaseConnection $conn) { $joins = array(); if ($this->memberPHIDs) { $joins[] = qsprintf( $conn, 'LEFT JOIN %T m ON a.phid = m.src AND m.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhortuneAccountHasMemberEdgeType::EDGECONST); } return implode(' ', $joins); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/query/PhortuneMerchantQuery.php b/src/applications/phortune/query/PhortuneMerchantQuery.php index 41a6f1004d..aa730393ff 100644 --- a/src/applications/phortune/query/PhortuneMerchantQuery.php +++ b/src/applications/phortune/query/PhortuneMerchantQuery.php @@ -1,103 +1,103 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } protected function loadPage() { $table = new PhortuneMerchant(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT m.* FROM %T m %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $merchants) { $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($merchants, 'getPHID')) ->withEdgeTypes(array(PhortuneMerchantHasMemberEdgeType::EDGECONST)); $query->execute(); foreach ($merchants as $merchant) { $member_phids = $query->getDestinationPHIDs(array($merchant->getPHID())); $member_phids = array_reverse($member_phids); $merchant->attachMemberPHIDs($member_phids); } return $merchants; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); 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); } $where[] = $this->buildPagingClause($conn); return $this->formatWhereClause($where); } - private function buildJoinClause(AphrontDatabaseConnection $conn) { + protected function buildJoinClause(AphrontDatabaseConnection $conn) { $joins = array(); if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, 'LEFT JOIN %T e ON m.phid = e.src AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhortuneMerchantHasMemberEdgeType::EDGECONST); } return implode(' ', $joins); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index 83684cfb3c..b4e80af936 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -1,339 +1,339 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withDepths(array $depths) { $this->depths = $depths; return $this; } public function withSlugPrefix($slug_prefix) { $this->slugPrefix = $slug_prefix; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function needContent($need_content) { $this->needContent = $need_content; return $this; } public function setOrder($order) { switch ($order) { case self::ORDER_CREATED: $this->setOrderVector(array('id')); break; case self::ORDER_UPDATED: $this->setOrderVector(array('updated')); break; case self::ORDER_HIERARCHY: $this->setOrderVector(array('depth', 'title', 'updated')); break; default: throw new Exception(pht('Unknown order "%s".', $order)); } return $this; } protected function loadPage() { $table = new PhrictionDocument(); $conn_r = $table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT d.* FROM %T d %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $documents = $table->loadAllFromArray($rows); if ($documents) { $ancestor_slugs = array(); foreach ($documents as $key => $document) { $document_slug = $document->getSlug(); foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) { $ancestor_slugs[$ancestor][] = $key; } } if ($ancestor_slugs) { $ancestors = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE slug IN (%Ls)', $document->getTableName(), array_keys($ancestor_slugs)); $ancestors = $table->loadAllFromArray($ancestors); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestor_slugs as $ancestor_slug => $document_keys) { $ancestor = idx($ancestors, $ancestor_slug); foreach ($document_keys as $document_key) { $documents[$document_key]->attachAncestor( $ancestor_slug, $ancestor); } } } } return $documents; } protected function willFilterPage(array $documents) { // To view a Phriction document, you must also be able to view all of the // ancestor documents. Filter out documents which have ancestors that are // not visible. $document_map = array(); foreach ($documents as $document) { $document_map[$document->getSlug()] = $document; foreach ($document->getAncestors() as $key => $ancestor) { if ($ancestor) { $document_map[$key] = $ancestor; } } } $filtered_map = $this->applyPolicyFilter( $document_map, array(PhabricatorPolicyCapability::CAN_VIEW)); // Filter all of the documents where a parent is not visible. foreach ($documents as $document_key => $document) { // If the document itself is not visible, filter it. if (!isset($filtered_map[$document->getSlug()])) { $this->didRejectResult($documents[$document_key]); unset($documents[$document_key]); continue; } // If an ancestor exists but is not visible, filter the document. foreach ($document->getAncestors() as $ancestor_key => $ancestor) { if (!$ancestor) { continue; } if (!isset($filtered_map[$ancestor_key])) { $this->didRejectResult($documents[$document_key]); unset($documents[$document_key]); break; } } } if (!$documents) { return $documents; } if ($this->needContent) { $contents = id(new PhrictionContent())->loadAllWhere( 'id IN (%Ld)', mpull($documents, 'getContentID')); foreach ($documents as $key => $document) { $content_id = $document->getContentID(); if (empty($contents[$content_id])) { unset($documents[$key]); continue; } $document->attachContent($contents[$content_id]); } } return $documents; } - private function buildJoinClause(AphrontDatabaseConnection $conn) { + protected function buildJoinClause(AphrontDatabaseConnection $conn) { $join = ''; if ($this->getOrderVector()->containsKey('updated')) { $content_dao = new PhrictionContent(); $join = qsprintf( $conn, 'JOIN %T c ON d.contentID = c.id', $content_dao->getTableName()); } return $join; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn, 'd.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn, 'd.phid IN (%Ls)', $this->phids); } if ($this->slugs) { $where[] = qsprintf( $conn, 'd.slug IN (%Ls)', $this->slugs); } if ($this->statuses) { $where[] = qsprintf( $conn, 'd.status IN (%Ld)', $this->statuses); } if ($this->slugPrefix) { $where[] = qsprintf( $conn, 'd.slug LIKE %>', $this->slugPrefix); } if ($this->depths) { $where[] = qsprintf( $conn, 'd.depth IN (%Ld)', $this->depths); } switch ($this->status) { case self::STATUS_OPEN: $where[] = qsprintf( $conn, 'd.status NOT IN (%Ld)', array( PhrictionDocumentStatus::STATUS_DELETED, PhrictionDocumentStatus::STATUS_MOVED, PhrictionDocumentStatus::STATUS_STUB, )); break; case self::STATUS_NONSTUB: $where[] = qsprintf( $conn, 'd.status NOT IN (%Ld)', array( PhrictionDocumentStatus::STATUS_MOVED, PhrictionDocumentStatus::STATUS_STUB, )); break; case self::STATUS_ANY: break; default: throw new Exception("Unknown status '{$this->status}'!"); } $where[] = $this->buildPagingClause($conn); return $this->formatWhereClause($where); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'depth' => array( 'table' => 'd', 'column' => 'depth', 'reverse' => true, 'type' => 'int', ), 'title' => array( 'table' => 'c', 'column' => 'title', 'reverse' => true, 'type' => 'string', ), 'updated' => array( 'table' => 'd', 'column' => 'contentID', 'type' => 'int', 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $document = $this->loadCursorObject($cursor); $map = array( 'id' => $document->getID(), 'depth' => $document->getDepth(), 'updated' => $document->getContentID(), ); foreach ($keys as $key) { switch ($key) { case 'title': $map[$key] = $document->getContent()->getTitle(); break; } } return $map; } protected function willExecuteCursorQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { $vector = $this->getOrderVector(); if ($vector->containsKey('title')) { $query->needContent(true); } } public function getQueryApplicationClass() { return 'PhabricatorPhrictionApplication'; } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index f743be7295..655d1f0850 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,393 +1,393 @@ 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 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 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 getDefaultOrderVector() { return array('name'); } public function getOrderableColumns() { return array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'reverse' => true, 'type' => 'string', 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $project = $this->loadCursorObject($cursor); return array( 'name' => $project->getName(), ); } 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; } protected function buildWhereClause(AphrontDatabaseConnection $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->nameTokens) { return 'GROUP BY p.id'; } else { return $this->buildApplicationSearchGroupClause($conn_r); } } - private function buildJoinClause($conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $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->nameTokens !== null) { foreach ($this->nameTokens as $key => $token) { $token_table = 'token_'.$key; $joins[] = qsprintf( $conn_r, 'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>', PhabricatorProject::TABLE_DATASOURCE_TOKEN, $token_table, $token_table, $token_table, $token); } } $joins[] = $this->buildApplicationSearchJoinClause($conn_r); return implode(' ', $joins); } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } protected function getPrimaryTableAlias() { return 'p'; } } diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php index eb9441aa8f..34cab99afa 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php @@ -1,233 +1,233 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withVersionBetween($min, $max) { $this->versionMin = $min; $this->versionMax = $max; return $this; } public function withNextEventBetween($min, $max) { $this->nextEpochMin = $min; $this->nextEpochMax = $max; return $this; } public function needEvents($need_events) { $this->needEvents = $need_events; return $this; } /** * Set the result order. * * Note that using `ORDER_EXECUTION` will also filter results to include only * triggers which have been scheduled to execute. You should not use this * ordering when querying for specific triggers, e.g. by ID or PHID. * * @param const Result order. * @return this */ public function setOrder($order) { $this->order = $order; return $this; } protected function nextPage(array $page) { // NOTE: We don't implement paging because we don't currently ever need // it and paging ORDER_EXCUTION is a hassle. throw new PhutilMethodNotImplementedException(); } protected function loadPage() { $task_table = new PhabricatorWorkerTrigger(); $conn_r = $task_table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT t.* FROM %T t %Q %Q %Q %Q', $task_table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $triggers = $task_table->loadAllFromArray($rows); if ($triggers) { if ($this->needEvents) { $ids = mpull($triggers, 'getID'); $events = id(new PhabricatorWorkerTriggerEvent())->loadAllWhere( 'triggerID IN (%Ld)', $ids); $events = mpull($events, null, 'getTriggerID'); foreach ($triggers as $key => $trigger) { $event = idx($events, $trigger->getID()); $trigger->attachEvent($event); } } foreach ($triggers as $key => $trigger) { $clock_class = $trigger->getClockClass(); if (!is_subclass_of($clock_class, 'PhabricatorTriggerClock')) { unset($triggers[$key]); continue; } try { $argv = array($trigger->getClockProperties()); $clock = newv($clock_class, $argv); } catch (Exception $ex) { unset($triggers[$key]); continue; } $trigger->attachClock($clock); } foreach ($triggers as $key => $trigger) { $action_class = $trigger->getActionClass(); if (!is_subclass_of($action_class, 'PhabricatorTriggerAction')) { unset($triggers[$key]); continue; } try { $argv = array($trigger->getActionProperties()); $action = newv($action_class, $argv); } catch (Exception $ex) { unset($triggers[$key]); continue; } $trigger->attachAction($action); } } return $triggers; } - private function buildJoinClause(AphrontDatabaseConnection $conn_r) { + protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if (($this->nextEpochMin !== null) || ($this->nextEpochMax !== null) || ($this->order == PhabricatorWorkerTriggerQuery::ORDER_EXECUTION)) { $joins[] = qsprintf( $conn_r, 'JOIN %T e ON e.triggerID = t.id', id(new PhabricatorWorkerTriggerEvent())->getTableName()); } return implode(' ', $joins); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 't.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 't.phid IN (%Ls)', $this->phids); } if ($this->versionMin !== null) { $where[] = qsprintf( $conn_r, 't.triggerVersion >= %d', $this->versionMin); } if ($this->versionMax !== null) { $where[] = qsprintf( $conn_r, 't.triggerVersion <= %d', $this->versionMax); } if ($this->nextEpochMin !== null) { $where[] = qsprintf( $conn_r, 'e.nextEventEpoch >= %d', $this->nextEpochMin); } if ($this->nextEpochMax !== null) { $where[] = qsprintf( $conn_r, 'e.nextEventEpoch <= %d', $this->nextEpochMax); } return $this->formatWhereClause($where); } private function buildOrderClause(AphrontDatabaseConnection $conn_r) { switch ($this->order) { case self::ORDER_ID: return qsprintf( $conn_r, 'ORDER BY id DESC'); case self::ORDER_EXECUTION: return qsprintf( $conn_r, 'ORDER BY e.nextEventEpoch ASC, e.id ASC'); case self::ORDER_VERSION: return qsprintf( $conn_r, 'ORDER BY t.triggerVersion ASC'); default: throw new Exception( pht( 'Unsupported order "%s".', $this->order)); } } } diff --git a/src/infrastructure/query/PhabricatorQuery.php b/src/infrastructure/query/PhabricatorQuery.php index 1662e16ac5..f57c43c0ee 100644 --- a/src/infrastructure/query/PhabricatorQuery.php +++ b/src/infrastructure/query/PhabricatorQuery.php @@ -1,17 +1,122 @@ flattenSubclause($parts); + if (!$parts) { + return ''; + } + + return 'WHERE '.$this->formatWhereSubclause($parts); + } + + + /** + * @task format + */ + protected function formatWhereSubclause(array $parts) { + $parts = $this->flattenSubclause($parts); + if (!$parts) { + return null; + } + + return '('.implode(') AND (', $parts).')'; + } + + + /** + * @task format + */ + protected function formatSelectClause(array $parts) { + $parts = $this->flattenSubclause($parts); + if (!$parts) { + throw new Exception(pht('Can not build empty select clause!')); + } + + return 'SELECT '.$this->formatSelectSubclause($parts); + } + + + /** + * @task format + */ + protected function formatSelectSubclause(array $parts) { + $parts = $this->flattenSubclause($parts); + if (!$parts) { + return null; + } + return implode(', ', $parts); + } + + + /** + * @task format + */ + protected function formatJoinClause(array $parts) { + $parts = $this->flattenSubclause($parts); + if (!$parts) { + return ''; + } + + return implode(' ', $parts); + } + + + /** + * @task format + */ + protected function formatHavingClause(array $parts) { + $parts = $this->flattenSubclause($parts); if (!$parts) { return ''; } - return 'WHERE ('.implode(') AND (', $parts).')'; + return 'HAVING '.$this->formatHavingSubclause($parts); + } + + + /** + * @task format + */ + protected function formatHavingSubclause(array $parts) { + $parts = $this->flattenSubclause($parts); + if (!$parts) { + return null; + } + + return '('.implode(') AND (', $parts).')'; + } + + + /** + * @task format + */ + private function flattenSubclause(array $parts) { + $result = array(); + foreach ($parts as $part) { + if (is_array($part)) { + foreach ($this->flattenSubclause($part) as $subpart) { + $result[] = $subpart; + } + } else if (strlen($part)) { + $result[] = $part; + } + } + return $result; } } diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index d532d1cf0f..347fb89315 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -1,1148 +1,1221 @@ getResultCursor(head($page)), $this->getResultCursor(last($page)), ); } protected function getResultCursor($object) { if (!is_object($object)) { throw new Exception( pht( 'Expected object, got "%s".', gettype($object))); } return $object->getID(); } protected function nextPage(array $page) { // See getPagingViewer() for a description of this flag. $this->internalPaging = true; if ($this->beforeID !== null) { $page = array_reverse($page, $preserve_keys = true); list($before, $after) = $this->getPageCursors($page); $this->beforeID = $before; } else { list($before, $after) = $this->getPageCursors($page); $this->afterID = $after; } } final 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) { $limit = $pager->getPageSize(); $this->setLimit($limit + 1); if ($pager->getAfterID()) { $this->setAfterID($pager->getAfterID()); } else if ($pager->getBeforeID()) { $this->setBeforeID($pager->getBeforeID()); } $results = $this->execute(); $count = count($results); $sliced_results = $pager->sliceResults($results); if ($sliced_results) { list($before, $after) = $this->getPageCursors($sliced_results); if ($pager->getBeforeID() || ($count > $limit)) { $pager->setNextPageID($after); } if ($pager->getAfterID() || ($pager->getBeforeID() && ($count > $limit))) { $pager->setPrevPageID($before); } } 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; } protected function newResultObject() { return null; } /* -( Building Query Clauses )--------------------------------------------- */ + /** + * @task clauses + */ + protected function buildSelectClause(AphrontDatabaseConnection $conn) { + $parts = $this->buildSelectClauseParts($conn); + return $this->formatSelectClause($parts); + } + + + /** + * @task clauses + */ + protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { + $select = array(); + + $alias = $this->getPrimaryTableAlias(); + if ($alias) { + $select[] = qsprintf($conn, '%T.*', $alias); + } else { + $select[] = '*'; + } + + return $select; + } + + + /** + * @task clauses + */ + protected function buildJoinClause(AphrontDatabaseConnection $conn) { + $joins = $this->buildJoinClauseParts($conn); + return $this->formatJoinClause($joins); + } + + + /** + * @task clauses + */ + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = array(); + return $joins; + } + + /** * @task clauses */ protected function buildWhereClause(AphrontDatabaseConnection $conn) { - throw new PhutilMethodNotImplementedException(); + $where = $this->buildWhereClauseParts($conn); + return $this->formatWhereClause($where); + } + + + /** + * @task clauses + */ + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = array(); + $where[] = $this->buildPagingClause($conn); + return $where; + } + + + /** + * @task clauses + */ + protected function buildHavingClause(AphrontDatabaseConnection $conn) { + $having = $this->buildHavingClauseParts($conn); + return $this->formatHavingClause($having); + } + + + /** + * @task clauses + */ + protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) { + $having = array(); + return $having; } /* -( Paging )------------------------------------------------------------- */ /** * @task paging */ protected function buildPagingClause(AphrontDatabaseConnection $conn) { $orderable = $this->getOrderableColumns(); $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, )); } /** * @task paging */ protected function getPagingValueMap($cursor, array $keys) { return array( 'id' => $cursor, ); } /** * @task paging */ protected function loadCursorObject($cursor) { $query = newv(get_class($this), array()) ->setViewer($this->getPagingViewer()) ->withIDs(array((int)$cursor)); $this->willExecuteCursorQuery($query); $object = $query->executeOne(); if (!$object) { throw new Exception( pht( 'Cursor "%s" does not identify a valid object.', $cursor)); } return $object; } /** * @task paging */ protected function willExecuteCursorQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { return; } /** * Simplifies the task of constructing a paging clause across multiple * columns. In the general case, this looks like: * * 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. * @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 ? '>' : '<', $value); } if ($parts) { if (count($parts) > 1) { $clause[] = '('.implode(') OR (', $parts).')'; } else { $clause[] = head($parts); } } if ($clause) { if (count($clause) > 1) { $clauses[] = '('.implode(') AND (', $clause).')'; } else { $clauses[] = head($clause); } } if ($value === null) { $accumulated[] = qsprintf( $conn, '%Q IS NULL', $field); } else { $accumulated[] = qsprintf( $conn, '%Q = %Q', $field, $value); } } return '('.implode(') OR (', $clauses).')'; } /* -( 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) { $orders = $this->getBuiltinOrders(); if (empty($orders[$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($orders)))); } $this->builtinOrder = $order; $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`): The actual order vector to use. * - `name` (`string`): Human-readable order name. * * @return map 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; } $key = $field->getFieldKey(); $digest = $field->getFieldIndex(); $full_key = 'custom:'.$key; $orders[$full_key] = array( 'vector' => array($full_key, 'id'), 'name' => $field->getFieldName(), ); } } return $orders; } /** * 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 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(); } $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() { $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; } $key = $field->getFieldKey(); $digest = $field->getFieldIndex(); $full_key = 'custom:'.$key; $columns[$full_key] = array( 'table' => 'appsearch_order_'.$digest, 'column' => 'indexValue', 'type' => $index->getIndexValueType(), 'null' => 'tail', ); } } return $columns; } /** * @task order */ final protected function buildOrderClause(AphrontDatabaseConnection $conn) { $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); } /** * @task order */ protected function formatOrderClause( AphrontDatabaseConnection $conn, array $parts) { $is_query_reversed = false; 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 ($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 %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 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() { 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']; $index = $order['index']; $alias = 'appsearch_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); } /* -( 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)); } }