[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/maniphest/query/ -> ManiphestTaskQuery.php (source)

   1  <?php
   2  
   3  /**
   4   * Query tasks by specific criteria. This class uses the higher-performance
   5   * but less-general Maniphest indexes to satisfy queries.
   6   */
   7  final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
   8  
   9    private $taskIDs             = array();
  10    private $taskPHIDs           = array();
  11    private $authorPHIDs         = array();
  12    private $ownerPHIDs          = array();
  13    private $includeUnowned      = null;
  14    private $projectPHIDs        = array();
  15    private $xprojectPHIDs       = array();
  16    private $subscriberPHIDs     = array();
  17    private $anyProjectPHIDs     = array();
  18    private $anyUserProjectPHIDs = array();
  19    private $includeNoProject    = null;
  20    private $dateCreatedAfter;
  21    private $dateCreatedBefore;
  22    private $dateModifiedAfter;
  23    private $dateModifiedBefore;
  24  
  25    private $fullTextSearch   = '';
  26  
  27    private $status           = 'status-any';
  28    const STATUS_ANY          = 'status-any';
  29    const STATUS_OPEN         = 'status-open';
  30    const STATUS_CLOSED       = 'status-closed';
  31    const STATUS_RESOLVED     = 'status-resolved';
  32    const STATUS_WONTFIX      = 'status-wontfix';
  33    const STATUS_INVALID      = 'status-invalid';
  34    const STATUS_SPITE        = 'status-spite';
  35    const STATUS_DUPLICATE    = 'status-duplicate';
  36  
  37    private $statuses;
  38    private $priorities;
  39  
  40    private $groupBy          = 'group-none';
  41    const GROUP_NONE          = 'group-none';
  42    const GROUP_PRIORITY      = 'group-priority';
  43    const GROUP_OWNER         = 'group-owner';
  44    const GROUP_STATUS        = 'group-status';
  45    const GROUP_PROJECT       = 'group-project';
  46  
  47    private $orderBy          = 'order-modified';
  48    const ORDER_PRIORITY      = 'order-priority';
  49    const ORDER_CREATED       = 'order-created';
  50    const ORDER_MODIFIED      = 'order-modified';
  51    const ORDER_TITLE         = 'order-title';
  52  
  53    const DEFAULT_PAGE_SIZE   = 1000;
  54  
  55    public function withAuthors(array $authors) {
  56      $this->authorPHIDs = $authors;
  57      return $this;
  58    }
  59  
  60    public function withIDs(array $ids) {
  61      $this->taskIDs = $ids;
  62      return $this;
  63    }
  64  
  65    public function withPHIDs(array $phids) {
  66      $this->taskPHIDs = $phids;
  67      return $this;
  68    }
  69  
  70    public function withOwners(array $owners) {
  71      $this->includeUnowned = false;
  72      foreach ($owners as $k => $phid) {
  73        if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS || $phid === null) {
  74          $this->includeUnowned = true;
  75          unset($owners[$k]);
  76          break;
  77        }
  78      }
  79      $this->ownerPHIDs = $owners;
  80      return $this;
  81    }
  82  
  83    public function withAllProjects(array $projects) {
  84      $this->includeNoProject = false;
  85      foreach ($projects as $k => $phid) {
  86        if ($phid == ManiphestTaskOwner::PROJECT_NO_PROJECT) {
  87          $this->includeNoProject = true;
  88          unset($projects[$k]);
  89        }
  90      }
  91      $this->projectPHIDs = $projects;
  92      return $this;
  93    }
  94  
  95    /**
  96     * Add an additional "all projects" constraint to existing filters.
  97     *
  98     * This is used by boards to supplement queries.
  99     *
 100     * @param list<phid> List of project PHIDs to add to any existing constraint.
 101     * @return this
 102     */
 103    public function addWithAllProjects(array $projects) {
 104      if ($this->projectPHIDs === null) {
 105        $this->projectPHIDs = array();
 106      }
 107  
 108      return $this->withAllProjects(array_merge($this->projectPHIDs, $projects));
 109    }
 110  
 111    public function withoutProjects(array $projects) {
 112      $this->xprojectPHIDs = $projects;
 113      return $this;
 114    }
 115  
 116    public function withStatus($status) {
 117      $this->status = $status;
 118      return $this;
 119    }
 120  
 121    public function withStatuses(array $statuses) {
 122      $this->statuses = $statuses;
 123      return $this;
 124    }
 125  
 126    public function withPriorities(array $priorities) {
 127      $this->priorities = $priorities;
 128      return $this;
 129    }
 130  
 131    public function withSubscribers(array $subscribers) {
 132      $this->subscriberPHIDs = $subscribers;
 133      return $this;
 134    }
 135  
 136    public function withFullTextSearch($fulltext_search) {
 137      $this->fullTextSearch = $fulltext_search;
 138      return $this;
 139    }
 140  
 141    public function setGroupBy($group) {
 142      $this->groupBy = $group;
 143      return $this;
 144    }
 145  
 146    public function setOrderBy($order) {
 147      $this->orderBy = $order;
 148      return $this;
 149    }
 150  
 151    public function withAnyProjects(array $projects) {
 152      $this->anyProjectPHIDs = $projects;
 153      return $this;
 154    }
 155  
 156    public function withAnyUserProjects(array $users) {
 157      $this->anyUserProjectPHIDs = $users;
 158      return $this;
 159    }
 160  
 161    public function withDateCreatedBefore($date_created_before) {
 162      $this->dateCreatedBefore = $date_created_before;
 163      return $this;
 164    }
 165  
 166    public function withDateCreatedAfter($date_created_after) {
 167      $this->dateCreatedAfter = $date_created_after;
 168      return $this;
 169    }
 170  
 171    public function withDateModifiedBefore($date_modified_before) {
 172      $this->dateModifiedBefore = $date_modified_before;
 173      return $this;
 174    }
 175  
 176    public function withDateModifiedAfter($date_modified_after) {
 177      $this->dateModifiedAfter = $date_modified_after;
 178      return $this;
 179    }
 180  
 181    public function loadPage() {
 182      // TODO: (T603) It is possible for a user to find the PHID of a project
 183      // they can't see, then query for tasks in that project and deduce the
 184      // identity of unknown/invisible projects. Before we allow the user to
 185      // execute a project-based PHID query, we should verify that they
 186      // can see the project.
 187  
 188      $task_dao = new ManiphestTask();
 189      $conn = $task_dao->establishConnection('r');
 190  
 191      $where = array();
 192      $where[] = $this->buildTaskIDsWhereClause($conn);
 193      $where[] = $this->buildTaskPHIDsWhereClause($conn);
 194      $where[] = $this->buildStatusWhereClause($conn);
 195      $where[] = $this->buildStatusesWhereClause($conn);
 196      $where[] = $this->buildPrioritiesWhereClause($conn);
 197      $where[] = $this->buildAuthorWhereClause($conn);
 198      $where[] = $this->buildOwnerWhereClause($conn);
 199      $where[] = $this->buildSubscriberWhereClause($conn);
 200      $where[] = $this->buildProjectWhereClause($conn);
 201      $where[] = $this->buildAnyProjectWhereClause($conn);
 202      $where[] = $this->buildAnyUserProjectWhereClause($conn);
 203      $where[] = $this->buildXProjectWhereClause($conn);
 204      $where[] = $this->buildFullTextWhereClause($conn);
 205  
 206      if ($this->dateCreatedAfter) {
 207        $where[] = qsprintf(
 208          $conn,
 209          'task.dateCreated >= %d',
 210          $this->dateCreatedAfter);
 211      }
 212  
 213      if ($this->dateCreatedBefore) {
 214        $where[] = qsprintf(
 215          $conn,
 216          'task.dateCreated <= %d',
 217          $this->dateCreatedBefore);
 218      }
 219  
 220      if ($this->dateModifiedAfter) {
 221        $where[] = qsprintf(
 222          $conn,
 223          'task.dateModified >= %d',
 224          $this->dateModifiedAfter);
 225      }
 226  
 227      if ($this->dateModifiedBefore) {
 228        $where[] = qsprintf(
 229          $conn,
 230          'task.dateModified <= %d',
 231          $this->dateModifiedBefore);
 232      }
 233  
 234      $where[] = $this->buildPagingClause($conn);
 235  
 236      $where = $this->formatWhereClause($where);
 237  
 238      $having = '';
 239      $count = '';
 240  
 241      if (count($this->projectPHIDs) > 1) {
 242        // We want to treat the query as an intersection query, not a union
 243        // query. We sum the project count and require it be the same as the
 244        // number of projects we're searching for.
 245  
 246        $count = ', COUNT(project.dst) projectCount';
 247        $having = qsprintf(
 248          $conn,
 249          'HAVING projectCount = %d',
 250          count($this->projectPHIDs));
 251      }
 252  
 253      $order = $this->buildCustomOrderClause($conn);
 254  
 255      // TODO: Clean up this nonstandardness.
 256      if (!$this->getLimit()) {
 257        $this->setLimit(self::DEFAULT_PAGE_SIZE);
 258      }
 259  
 260      $group_column = '';
 261      switch ($this->groupBy) {
 262        case self::GROUP_PROJECT:
 263          $group_column = qsprintf(
 264            $conn,
 265            ', projectGroupName.indexedObjectPHID projectGroupPHID');
 266          break;
 267      }
 268  
 269      $rows = queryfx_all(
 270        $conn,
 271        'SELECT task.* %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
 272        $count,
 273        $group_column,
 274        $task_dao->getTableName(),
 275        $this->buildJoinsClause($conn),
 276        $where,
 277        $this->buildGroupClause($conn),
 278        $having,
 279        $order,
 280        $this->buildLimitClause($conn));
 281  
 282      switch ($this->groupBy) {
 283        case self::GROUP_PROJECT:
 284          $data = ipull($rows, null, 'id');
 285          break;
 286        default:
 287          $data = $rows;
 288          break;
 289      }
 290  
 291      $tasks = $task_dao->loadAllFromArray($data);
 292  
 293      switch ($this->groupBy) {
 294        case self::GROUP_PROJECT:
 295          $results = array();
 296          foreach ($rows as $row) {
 297            $task = clone $tasks[$row['id']];
 298            $task->attachGroupByProjectPHID($row['projectGroupPHID']);
 299            $results[] = $task;
 300          }
 301          $tasks = $results;
 302          break;
 303      }
 304  
 305      return $tasks;
 306    }
 307  
 308    protected function willFilterPage(array $tasks) {
 309      if ($this->groupBy == self::GROUP_PROJECT) {
 310        // We should only return project groups which the user can actually see.
 311        $project_phids = mpull($tasks, 'getGroupByProjectPHID');
 312        $projects = id(new PhabricatorProjectQuery())
 313          ->setViewer($this->getViewer())
 314          ->withPHIDs($project_phids)
 315          ->execute();
 316        $projects = mpull($projects, null, 'getPHID');
 317  
 318        foreach ($tasks as $key => $task) {
 319          if (!$task->getGroupByProjectPHID()) {
 320            // This task is either not in any projects, or only in projects
 321            // which we're ignoring because they're being queried for explicitly.
 322            continue;
 323          }
 324  
 325          if (empty($projects[$task->getGroupByProjectPHID()])) {
 326            unset($tasks[$key]);
 327          }
 328        }
 329      }
 330  
 331      return $tasks;
 332    }
 333  
 334    protected function didFilterPage(array $tasks) {
 335      // TODO: Eventually, we should make this optional and introduce a
 336      // needProjectPHIDs() method, but for now there's a lot of code which
 337      // assumes the data is always populated.
 338  
 339      $edge_query = id(new PhabricatorEdgeQuery())
 340        ->withSourcePHIDs(mpull($tasks, 'getPHID'))
 341        ->withEdgeTypes(
 342          array(
 343            PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
 344          ));
 345      $edge_query->execute();
 346  
 347      foreach ($tasks as $task) {
 348        $phids = $edge_query->getDestinationPHIDs(array($task->getPHID()));
 349        $task->attachProjectPHIDs($phids);
 350      }
 351  
 352      return $tasks;
 353    }
 354  
 355    private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) {
 356      if (!$this->taskIDs) {
 357        return null;
 358      }
 359  
 360      return qsprintf(
 361        $conn,
 362        'id in (%Ld)',
 363        $this->taskIDs);
 364    }
 365  
 366    private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) {
 367      if (!$this->taskPHIDs) {
 368        return null;
 369      }
 370  
 371      return qsprintf(
 372        $conn,
 373        'phid in (%Ls)',
 374        $this->taskPHIDs);
 375    }
 376  
 377    private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
 378      static $map = array(
 379        self::STATUS_RESOLVED   => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
 380        self::STATUS_WONTFIX    => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
 381        self::STATUS_INVALID    => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
 382        self::STATUS_SPITE      => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
 383        self::STATUS_DUPLICATE  => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
 384      );
 385  
 386      switch ($this->status) {
 387        case self::STATUS_ANY:
 388          return null;
 389        case self::STATUS_OPEN:
 390          return qsprintf(
 391            $conn,
 392            'status IN (%Ls)',
 393            ManiphestTaskStatus::getOpenStatusConstants());
 394        case self::STATUS_CLOSED:
 395          return qsprintf(
 396            $conn,
 397            'status IN (%Ls)',
 398            ManiphestTaskStatus::getClosedStatusConstants());
 399        default:
 400          $constant = idx($map, $this->status);
 401          if (!$constant) {
 402            throw new Exception("Unknown status query '{$this->status}'!");
 403          }
 404          return qsprintf(
 405            $conn,
 406            'status = %s',
 407            $constant);
 408      }
 409    }
 410  
 411    private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) {
 412      if ($this->statuses) {
 413        return qsprintf(
 414          $conn,
 415          'status IN (%Ls)',
 416          $this->statuses);
 417      }
 418      return null;
 419    }
 420  
 421    private function buildPrioritiesWhereClause(AphrontDatabaseConnection $conn) {
 422      if ($this->priorities) {
 423        return qsprintf(
 424          $conn,
 425          'priority IN (%Ld)',
 426          $this->priorities);
 427      }
 428  
 429      return null;
 430    }
 431  
 432    private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) {
 433      if (!$this->authorPHIDs) {
 434        return null;
 435      }
 436  
 437      return qsprintf(
 438        $conn,
 439        'authorPHID in (%Ls)',
 440        $this->authorPHIDs);
 441    }
 442  
 443    private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
 444      if (!$this->ownerPHIDs) {
 445        if ($this->includeUnowned === null) {
 446          return null;
 447        } else if ($this->includeUnowned) {
 448          return qsprintf(
 449            $conn,
 450            'ownerPHID IS NULL');
 451        } else {
 452          return qsprintf(
 453            $conn,
 454            'ownerPHID IS NOT NULL');
 455        }
 456      }
 457  
 458      if ($this->includeUnowned) {
 459        return qsprintf(
 460          $conn,
 461          'ownerPHID IN (%Ls) OR ownerPHID IS NULL',
 462          $this->ownerPHIDs);
 463      } else {
 464        return qsprintf(
 465          $conn,
 466          'ownerPHID IN (%Ls)',
 467          $this->ownerPHIDs);
 468      }
 469    }
 470  
 471    private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
 472      if (!strlen($this->fullTextSearch)) {
 473        return null;
 474      }
 475  
 476      // In doing a fulltext search, we first find all the PHIDs that match the
 477      // fulltext search, and then use that to limit the rest of the search
 478      $fulltext_query = id(new PhabricatorSavedQuery())
 479        ->setEngineClassName('PhabricatorSearchApplicationSearchEngine')
 480        ->setParameter('query', $this->fullTextSearch);
 481  
 482      // NOTE: Setting this to something larger than 2^53 will raise errors in
 483      // ElasticSearch, and billions of results won't fit in memory anyway.
 484      $fulltext_query->setParameter('limit', 100000);
 485      $fulltext_query->setParameter('type', ManiphestTaskPHIDType::TYPECONST);
 486  
 487      $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
 488      $fulltext_results = $engine->executeSearch($fulltext_query);
 489  
 490      if (empty($fulltext_results)) {
 491        $fulltext_results = array(null);
 492      }
 493  
 494      return qsprintf(
 495        $conn,
 496        'phid IN (%Ls)',
 497        $fulltext_results);
 498    }
 499  
 500    private function buildSubscriberWhereClause(AphrontDatabaseConnection $conn) {
 501      if (!$this->subscriberPHIDs) {
 502        return null;
 503      }
 504  
 505      return qsprintf(
 506        $conn,
 507        'subscriber.subscriberPHID IN (%Ls)',
 508        $this->subscriberPHIDs);
 509    }
 510  
 511    private function buildProjectWhereClause(AphrontDatabaseConnection $conn) {
 512      if (!$this->projectPHIDs && !$this->includeNoProject) {
 513        return null;
 514      }
 515  
 516      $parts = array();
 517      if ($this->projectPHIDs) {
 518        $parts[] = qsprintf(
 519          $conn,
 520          'project.dst in (%Ls)',
 521          $this->projectPHIDs);
 522      }
 523      if ($this->includeNoProject) {
 524        $parts[] = qsprintf(
 525          $conn,
 526          'project.dst IS NULL');
 527      }
 528  
 529      return '('.implode(') OR (', $parts).')';
 530    }
 531  
 532    private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) {
 533      if (!$this->anyProjectPHIDs) {
 534        return null;
 535      }
 536  
 537      return qsprintf(
 538        $conn,
 539        'anyproject.dst IN (%Ls)',
 540        $this->anyProjectPHIDs);
 541    }
 542  
 543    private function buildAnyUserProjectWhereClause(
 544      AphrontDatabaseConnection $conn) {
 545      if (!$this->anyUserProjectPHIDs) {
 546        return null;
 547      }
 548  
 549      $projects = id(new PhabricatorProjectQuery())
 550        ->setViewer($this->getViewer())
 551        ->withMemberPHIDs($this->anyUserProjectPHIDs)
 552        ->execute();
 553      $any_user_project_phids = mpull($projects, 'getPHID');
 554      if (!$any_user_project_phids) {
 555        throw new PhabricatorEmptyQueryException();
 556      }
 557  
 558      return qsprintf(
 559        $conn,
 560        'anyproject.dst IN (%Ls)',
 561        $any_user_project_phids);
 562    }
 563  
 564    private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) {
 565      if (!$this->xprojectPHIDs) {
 566        return null;
 567      }
 568  
 569      return qsprintf(
 570        $conn,
 571        'xproject.dst IS NULL');
 572    }
 573  
 574    private function buildCustomOrderClause(AphrontDatabaseConnection $conn) {
 575      $reverse = ($this->getBeforeID() xor $this->getReversePaging());
 576  
 577      $order = array();
 578  
 579      switch ($this->groupBy) {
 580        case self::GROUP_NONE:
 581          break;
 582        case self::GROUP_PRIORITY:
 583          $order[] = 'priority';
 584          break;
 585        case self::GROUP_OWNER:
 586          $order[] = 'ownerOrdering';
 587          break;
 588        case self::GROUP_STATUS:
 589          $order[] = 'status';
 590          break;
 591        case self::GROUP_PROJECT:
 592          $order[] = '<group.project>';
 593          break;
 594        default:
 595          throw new Exception("Unknown group query '{$this->groupBy}'!");
 596      }
 597  
 598      $app_order = $this->buildApplicationSearchOrders($conn, $reverse);
 599  
 600      if (!$app_order) {
 601        switch ($this->orderBy) {
 602          case self::ORDER_PRIORITY:
 603            $order[] = 'priority';
 604            $order[] = 'subpriority';
 605            $order[] = 'dateModified';
 606            break;
 607          case self::ORDER_CREATED:
 608            $order[] = 'id';
 609            break;
 610          case self::ORDER_MODIFIED:
 611            $order[] = 'dateModified';
 612            break;
 613          case self::ORDER_TITLE:
 614            $order[] = 'title';
 615            break;
 616          default:
 617            throw new Exception("Unknown order query '{$this->orderBy}'!");
 618        }
 619      }
 620  
 621      $order = array_unique($order);
 622  
 623      if (empty($order) && empty($app_order)) {
 624        return null;
 625      }
 626  
 627      foreach ($order as $k => $column) {
 628        switch ($column) {
 629          case 'subpriority':
 630          case 'ownerOrdering':
 631          case 'title':
 632            if ($reverse) {
 633              $order[$k] = "task.{$column} DESC";
 634            } else {
 635              $order[$k] = "task.{$column} ASC";
 636            }
 637            break;
 638          case '<group.project>':
 639            // Put "No Project" at the end of the list.
 640            if ($reverse) {
 641              $order[$k] =
 642                'projectGroupName.indexedObjectName IS NULL DESC, '.
 643                'projectGroupName.indexedObjectName DESC';
 644            } else {
 645              $order[$k] =
 646                'projectGroupName.indexedObjectName IS NULL ASC, '.
 647                'projectGroupName.indexedObjectName ASC';
 648            }
 649            break;
 650          default:
 651            if ($reverse) {
 652              $order[$k] = "task.{$column} ASC";
 653            } else {
 654              $order[$k] = "task.{$column} DESC";
 655            }
 656            break;
 657        }
 658      }
 659  
 660      if ($app_order) {
 661        foreach ($app_order as $order_by) {
 662          $order[] = $order_by;
 663        }
 664  
 665        if ($reverse) {
 666          $order[] = 'task.id ASC';
 667        } else {
 668          $order[] = 'task.id DESC';
 669        }
 670      }
 671  
 672      return 'ORDER BY '.implode(', ', $order);
 673    }
 674  
 675    private function buildJoinsClause(AphrontDatabaseConnection $conn_r) {
 676      $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
 677  
 678      $joins = array();
 679  
 680      if ($this->projectPHIDs || $this->includeNoProject) {
 681        $joins[] = qsprintf(
 682          $conn_r,
 683          '%Q JOIN %T project ON project.src = task.phid
 684            AND project.type = %d',
 685          ($this->includeNoProject ? 'LEFT' : ''),
 686          $edge_table,
 687          PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 688      }
 689  
 690      if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) {
 691        $joins[] = qsprintf(
 692          $conn_r,
 693          'JOIN %T anyproject ON anyproject.src = task.phid
 694            AND anyproject.type = %d',
 695          $edge_table,
 696          PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 697      }
 698  
 699      if ($this->xprojectPHIDs) {
 700        $joins[] = qsprintf(
 701          $conn_r,
 702          'LEFT JOIN %T xproject ON xproject.src = task.phid
 703            AND xproject.type = %d
 704            AND xproject.dst IN (%Ls)',
 705          $edge_table,
 706          PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
 707          $this->xprojectPHIDs);
 708      }
 709  
 710      if ($this->subscriberPHIDs) {
 711        $subscriber_dao = new ManiphestTaskSubscriber();
 712        $joins[] = qsprintf(
 713          $conn_r,
 714          'JOIN %T subscriber ON subscriber.taskPHID = task.phid',
 715          $subscriber_dao->getTableName());
 716      }
 717  
 718      switch ($this->groupBy) {
 719        case self::GROUP_PROJECT:
 720          $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
 721          if ($ignore_group_phids) {
 722            $joins[] = qsprintf(
 723              $conn_r,
 724              'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
 725                AND projectGroup.type = %d
 726                AND projectGroup.dst NOT IN (%Ls)',
 727              $edge_table,
 728              PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
 729              $ignore_group_phids);
 730          } else {
 731            $joins[] = qsprintf(
 732              $conn_r,
 733              'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
 734                AND projectGroup.type = %d',
 735              $edge_table,
 736              PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 737          }
 738          $joins[] = qsprintf(
 739            $conn_r,
 740            'LEFT JOIN %T projectGroupName
 741              ON projectGroup.dst = projectGroupName.indexedObjectPHID',
 742            id(new ManiphestNameIndex())->getTableName());
 743          break;
 744      }
 745  
 746      $joins[] = $this->buildApplicationSearchJoinClause($conn_r);
 747  
 748      return implode(' ', $joins);
 749    }
 750  
 751    private function buildGroupClause(AphrontDatabaseConnection $conn_r) {
 752      $joined_multiple_rows = (count($this->projectPHIDs) > 1) ||
 753                              (count($this->anyProjectPHIDs) > 1) ||
 754                              ($this->getApplicationSearchMayJoinMultipleRows());
 755  
 756      $joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
 757  
 758      // If we're joining multiple rows, we need to group the results by the
 759      // task IDs.
 760      if ($joined_multiple_rows) {
 761        if ($joined_project_name) {
 762          return 'GROUP BY task.phid, projectGroup.dst';
 763        } else {
 764          return 'GROUP BY task.phid';
 765        }
 766      } else {
 767        return '';
 768      }
 769    }
 770  
 771    /**
 772     * Return project PHIDs which we should ignore when grouping tasks by
 773     * project. For example, if a user issues a query like:
 774     *
 775     *   Tasks in all projects: Frontend, Bugs
 776     *
 777     * ...then we don't show "Frontend" or "Bugs" groups in the result set, since
 778     * they're meaningless as all results are in both groups.
 779     *
 780     * Similarly, for queries like:
 781     *
 782     *   Tasks in any projects: Public Relations
 783     *
 784     * ...we ignore the single project, as every result is in that project. (In
 785     * the case that there are several "any" projects, we do not ignore them.)
 786     *
 787     * @return list<phid> Project PHIDs which should be ignored in query
 788     *                    construction.
 789     */
 790    private function getIgnoreGroupedProjectPHIDs() {
 791      $phids = array();
 792  
 793      if ($this->projectPHIDs) {
 794        $phids[] = $this->projectPHIDs;
 795      }
 796  
 797      if (count($this->anyProjectPHIDs) == 1) {
 798        $phids[] = $this->anyProjectPHIDs;
 799      }
 800  
 801      // Maybe we should also exclude the "excludeProjectPHIDs"? It won't
 802      // impact the results, but we might end up with a better query plan.
 803      // Investigate this on real data? This is likely very rare.
 804  
 805      return array_mergev($phids);
 806    }
 807  
 808    private function loadCursorObject($id) {
 809      $results = id(new ManiphestTaskQuery())
 810        ->setViewer($this->getPagingViewer())
 811        ->withIDs(array((int)$id))
 812        ->execute();
 813      return head($results);
 814    }
 815  
 816    protected function getPagingValue($result) {
 817      $id = $result->getID();
 818  
 819      switch ($this->groupBy) {
 820        case self::GROUP_NONE:
 821          return $id;
 822        case self::GROUP_PRIORITY:
 823          return $id.'.'.$result->getPriority();
 824        case self::GROUP_OWNER:
 825          return rtrim($id.'.'.$result->getOwnerPHID(), '.');
 826        case self::GROUP_STATUS:
 827          return $id.'.'.$result->getStatus();
 828        case self::GROUP_PROJECT:
 829          return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
 830        default:
 831          throw new Exception("Unknown group query '{$this->groupBy}'!");
 832      }
 833    }
 834  
 835    protected function buildPagingClause(AphrontDatabaseConnection $conn_r) {
 836      $default = parent::buildPagingClause($conn_r);
 837  
 838      $before_id = $this->getBeforeID();
 839      $after_id = $this->getAfterID();
 840  
 841      if (!$before_id && !$after_id) {
 842        return $default;
 843      }
 844  
 845      $cursor_id = nonempty($before_id, $after_id);
 846      $cursor_parts = explode('.', $cursor_id, 2);
 847      $task_id = $cursor_parts[0];
 848      $group_id = idx($cursor_parts, 1);
 849  
 850      $cursor = $this->loadCursorObject($task_id);
 851      if (!$cursor) {
 852        return null;
 853      }
 854  
 855      $columns = array();
 856  
 857      switch ($this->groupBy) {
 858        case self::GROUP_NONE:
 859          break;
 860        case self::GROUP_PRIORITY:
 861          $columns[] = array(
 862            'name' => 'task.priority',
 863            'value' => (int)$group_id,
 864            'type' => 'int',
 865          );
 866          break;
 867        case self::GROUP_OWNER:
 868          $columns[] = array(
 869            'name' => '(task.ownerOrdering IS NULL)',
 870            'value' => (int)(strlen($group_id) ? 0 : 1),
 871            'type' => 'int',
 872          );
 873          if ($group_id) {
 874            $paging_users = id(new PhabricatorPeopleQuery())
 875              ->setViewer($this->getViewer())
 876              ->withPHIDs(array($group_id))
 877              ->execute();
 878            if (!$paging_users) {
 879              return null;
 880            }
 881            $columns[] = array(
 882              'name' => 'task.ownerOrdering',
 883              'value' => head($paging_users)->getUsername(),
 884              'type' => 'string',
 885              'reverse' => true,
 886            );
 887          }
 888          break;
 889        case self::GROUP_STATUS:
 890          $columns[] = array(
 891            'name' => 'task.status',
 892            'value' => $group_id,
 893            'type' => 'string',
 894          );
 895          break;
 896        case self::GROUP_PROJECT:
 897          $columns[] = array(
 898            'name' => '(projectGroupName.indexedObjectName IS NULL)',
 899            'value' => (int)(strlen($group_id) ? 0 : 1),
 900            'type' => 'int',
 901          );
 902          if ($group_id) {
 903            $paging_projects = id(new PhabricatorProjectQuery())
 904              ->setViewer($this->getViewer())
 905              ->withPHIDs(array($group_id))
 906              ->execute();
 907            if (!$paging_projects) {
 908              return null;
 909            }
 910            $columns[] = array(
 911              'name' => 'projectGroupName.indexedObjectName',
 912              'value' => head($paging_projects)->getName(),
 913              'type' => 'string',
 914              'reverse' => true,
 915            );
 916          }
 917          break;
 918        default:
 919          throw new Exception("Unknown group query '{$this->groupBy}'!");
 920      }
 921  
 922      $app_columns = $this->buildApplicationSearchPagination($conn_r, $cursor);
 923      if ($app_columns) {
 924        $columns = array_merge($columns, $app_columns);
 925        $columns[] = array(
 926          'name' => 'task.id',
 927          'value' => (int)$cursor->getID(),
 928          'type' => 'int',
 929        );
 930      } else {
 931        switch ($this->orderBy) {
 932          case self::ORDER_PRIORITY:
 933            if ($this->groupBy != self::GROUP_PRIORITY) {
 934              $columns[] = array(
 935                'name' => 'task.priority',
 936                'value' => (int)$cursor->getPriority(),
 937                'type' => 'int',
 938              );
 939            }
 940            $columns[] = array(
 941              'name' => 'task.subpriority',
 942              'value' => (int)$cursor->getSubpriority(),
 943              'type' => 'int',
 944              'reverse' => true,
 945            );
 946            $columns[] = array(
 947              'name' => 'task.dateModified',
 948              'value' => (int)$cursor->getDateModified(),
 949              'type' => 'int',
 950            );
 951            break;
 952          case self::ORDER_CREATED:
 953            $columns[] = array(
 954              'name' => 'task.id',
 955              'value' => (int)$cursor->getID(),
 956              'type' => 'int',
 957            );
 958            break;
 959          case self::ORDER_MODIFIED:
 960            $columns[] = array(
 961              'name' => 'task.dateModified',
 962              'value' => (int)$cursor->getDateModified(),
 963              'type' => 'int',
 964            );
 965            break;
 966          case self::ORDER_TITLE:
 967            $columns[] = array(
 968              'name' => 'task.title',
 969              'value' => $cursor->getTitle(),
 970              'type' => 'string',
 971            );
 972            $columns[] = array(
 973              'name' => 'task.id',
 974              'value' => $cursor->getID(),
 975              'type' => 'int',
 976            );
 977            break;
 978          default:
 979            throw new Exception("Unknown order query '{$this->orderBy}'!");
 980        }
 981      }
 982  
 983      return $this->buildPagingClauseFromMultipleColumns(
 984        $conn_r,
 985        $columns,
 986        array(
 987          'reversed' => (bool)($before_id xor $this->getReversePaging()),
 988        ));
 989    }
 990  
 991    protected function getApplicationSearchObjectPHIDColumn() {
 992      return 'task.phid';
 993    }
 994  
 995    public function getQueryApplicationClass() {
 996      return 'PhabricatorManiphestApplication';
 997    }
 998  
 999  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1