@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 1060 lines 29 kB view raw
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 * @extends PhabricatorCursorPagedPolicyAwareQuery<ManiphestTask> 8 */ 9final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { 10 11 private $taskIDs; 12 private $taskPHIDs; 13 private $authorPHIDs; 14 private $ownerPHIDs; 15 private $noOwner; 16 private $anyOwner; 17 private $subscriberPHIDs; 18 private $dateCreatedAfter; 19 private $dateCreatedBefore; 20 private $dateModifiedAfter; 21 private $dateModifiedBefore; 22 private $bridgedObjectPHIDs; 23 private $hasOpenParents; 24 private $hasOpenSubtasks; 25 private $parentTaskIDs; 26 private $subtaskIDs; 27 private $subtypes; 28 private $closedEpochMin; 29 private $closedEpochMax; 30 private $closerPHIDs; 31 private $columnPHIDs; 32 private $specificGroupByProjectPHID; 33 34 private $status = 'status-any'; 35 const STATUS_ANY = 'status-any'; 36 const STATUS_OPEN = 'status-open'; 37 const STATUS_CLOSED = 'status-closed'; 38 const STATUS_RESOLVED = 'status-resolved'; 39 const STATUS_WONTFIX = 'status-wontfix'; 40 const STATUS_INVALID = 'status-invalid'; 41 const STATUS_SPITE = 'status-spite'; 42 const STATUS_DUPLICATE = 'status-duplicate'; 43 44 private $statuses; 45 private $priorities; 46 private $subpriorities; 47 48 private $groupBy = 'group-none'; 49 const GROUP_NONE = 'group-none'; 50 const GROUP_PRIORITY = 'group-priority'; 51 const GROUP_OWNER = 'group-owner'; 52 const GROUP_STATUS = 'group-status'; 53 const GROUP_PROJECT = 'group-project'; 54 55 const ORDER_PRIORITY = 'order-priority'; 56 const ORDER_CREATED = 'order-created'; 57 const ORDER_MODIFIED = 'order-modified'; 58 const ORDER_TITLE = 'order-title'; 59 60 private $needSubscriberPHIDs; 61 private $needProjectPHIDs; 62 63 public function withAuthors(array $authors) { 64 $this->authorPHIDs = $authors; 65 return $this; 66 } 67 68 public function withIDs(array $ids) { 69 $this->taskIDs = $ids; 70 return $this; 71 } 72 73 public function withPHIDs(array $phids) { 74 $this->taskPHIDs = $phids; 75 return $this; 76 } 77 78 public function withOwners(array $owners) { 79 if ($owners === array()) { 80 throw new Exception(pht('Empty withOwners() constraint is not valid.')); 81 } 82 83 $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; 84 $any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN; 85 86 foreach ($owners as $k => $phid) { 87 if ($phid === $no_owner || $phid === null) { 88 $this->noOwner = true; 89 unset($owners[$k]); 90 break; 91 } 92 if ($phid === $any_owner) { 93 $this->anyOwner = true; 94 unset($owners[$k]); 95 break; 96 } 97 } 98 99 if ($owners) { 100 $this->ownerPHIDs = $owners; 101 } 102 103 return $this; 104 } 105 106 public function withStatus($status) { 107 $this->status = $status; 108 return $this; 109 } 110 111 public function withStatuses(array $statuses) { 112 $this->statuses = $statuses; 113 return $this; 114 } 115 116 public function withPriorities(array $priorities) { 117 $this->priorities = $priorities; 118 return $this; 119 } 120 121 public function withSubpriorities(array $subpriorities) { 122 $this->subpriorities = $subpriorities; 123 return $this; 124 } 125 126 public function withSubscribers(array $subscribers) { 127 $this->subscriberPHIDs = $subscribers; 128 return $this; 129 } 130 131 public function setGroupBy($group) { 132 $this->groupBy = $group; 133 134 switch ($this->groupBy) { 135 case self::GROUP_NONE: 136 $vector = array(); 137 break; 138 case self::GROUP_PRIORITY: 139 $vector = array('priority'); 140 break; 141 case self::GROUP_OWNER: 142 $vector = array('owner'); 143 break; 144 case self::GROUP_STATUS: 145 $vector = array('status'); 146 break; 147 case self::GROUP_PROJECT: 148 $vector = array('project'); 149 break; 150 } 151 152 $this->setGroupVector($vector); 153 154 return $this; 155 } 156 157 public function withOpenSubtasks($value) { 158 $this->hasOpenSubtasks = $value; 159 return $this; 160 } 161 162 public function withOpenParents($value) { 163 $this->hasOpenParents = $value; 164 return $this; 165 } 166 167 public function withParentTaskIDs(array $ids) { 168 $this->parentTaskIDs = $ids; 169 return $this; 170 } 171 172 public function withSubtaskIDs(array $ids) { 173 $this->subtaskIDs = $ids; 174 return $this; 175 } 176 177 public function withDateCreatedBefore($date_created_before) { 178 $this->dateCreatedBefore = $date_created_before; 179 return $this; 180 } 181 182 public function withDateCreatedAfter($date_created_after) { 183 $this->dateCreatedAfter = $date_created_after; 184 return $this; 185 } 186 187 public function withDateModifiedBefore($date_modified_before) { 188 $this->dateModifiedBefore = $date_modified_before; 189 return $this; 190 } 191 192 public function withDateModifiedAfter($date_modified_after) { 193 $this->dateModifiedAfter = $date_modified_after; 194 return $this; 195 } 196 197 public function withClosedEpochBetween($min, $max) { 198 $this->closedEpochMin = $min; 199 $this->closedEpochMax = $max; 200 return $this; 201 } 202 203 public function withCloserPHIDs(array $phids) { 204 $this->closerPHIDs = $phids; 205 return $this; 206 } 207 208 public function needSubscriberPHIDs($bool) { 209 $this->needSubscriberPHIDs = $bool; 210 return $this; 211 } 212 213 public function needProjectPHIDs($bool) { 214 $this->needProjectPHIDs = $bool; 215 return $this; 216 } 217 218 public function withBridgedObjectPHIDs(array $phids) { 219 $this->bridgedObjectPHIDs = $phids; 220 return $this; 221 } 222 223 public function withSubtypes(array $subtypes) { 224 $this->subtypes = $subtypes; 225 return $this; 226 } 227 228 public function withColumnPHIDs(array $column_phids) { 229 $this->columnPHIDs = $column_phids; 230 return $this; 231 } 232 233 public function withSpecificGroupByProjectPHID($project_phid) { 234 $this->specificGroupByProjectPHID = $project_phid; 235 return $this; 236 } 237 238 public function newResultObject() { 239 return new ManiphestTask(); 240 } 241 242 protected function loadPage() { 243 $task_dao = new ManiphestTask(); 244 $conn = $task_dao->establishConnection('r'); 245 246 $where = $this->buildWhereClause($conn); 247 248 $group_column = qsprintf($conn, ''); 249 switch ($this->groupBy) { 250 case self::GROUP_PROJECT: 251 $group_column = qsprintf( 252 $conn, 253 ', projectGroupName.indexedObjectPHID projectGroupPHID'); 254 break; 255 } 256 257 $rows = queryfx_all( 258 $conn, 259 '%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', 260 $this->buildSelectClause($conn), 261 $group_column, 262 $task_dao->getTableName(), 263 $this->buildJoinClause($conn), 264 $where, 265 $this->buildGroupClause($conn), 266 $this->buildHavingClause($conn), 267 $this->buildOrderClause($conn), 268 $this->buildLimitClause($conn)); 269 270 switch ($this->groupBy) { 271 case self::GROUP_PROJECT: 272 $data = ipull($rows, null, 'id'); 273 break; 274 default: 275 $data = $rows; 276 break; 277 } 278 279 $data = $this->didLoadRawRows($data); 280 $tasks = $task_dao->loadAllFromArray($data); 281 282 switch ($this->groupBy) { 283 case self::GROUP_PROJECT: 284 $results = array(); 285 foreach ($rows as $row) { 286 $task = clone $tasks[$row['id']]; 287 $task->attachGroupByProjectPHID($row['projectGroupPHID']); 288 $results[] = $task; 289 } 290 $tasks = $results; 291 break; 292 } 293 294 return $tasks; 295 } 296 297 protected function willFilterPage(array $tasks) { 298 if ($this->groupBy == self::GROUP_PROJECT) { 299 // We should only return project groups which the user can actually see. 300 $project_phids = mpull($tasks, 'getGroupByProjectPHID'); 301 $projects = id(new PhabricatorProjectQuery()) 302 ->setViewer($this->getViewer()) 303 ->withPHIDs($project_phids) 304 ->execute(); 305 $projects = mpull($projects, null, 'getPHID'); 306 307 foreach ($tasks as $key => $task) { 308 if (!$task->getGroupByProjectPHID()) { 309 // This task is either not tagged with any projects, or only tagged 310 // with projects which we're ignoring because they're being queried 311 // for explicitly. 312 continue; 313 } 314 315 if (empty($projects[$task->getGroupByProjectPHID()])) { 316 unset($tasks[$key]); 317 } 318 } 319 } 320 321 return $tasks; 322 } 323 324 protected function didFilterPage(array $tasks) { 325 $phids = mpull($tasks, 'getPHID'); 326 327 if ($this->needProjectPHIDs) { 328 $edge_query = id(new PhabricatorEdgeQuery()) 329 ->withSourcePHIDs($phids) 330 ->withEdgeTypes( 331 array( 332 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 333 )); 334 $edge_query->execute(); 335 336 foreach ($tasks as $task) { 337 $project_phids = $edge_query->getDestinationPHIDs( 338 array($task->getPHID())); 339 $task->attachProjectPHIDs($project_phids); 340 } 341 } 342 343 if ($this->needSubscriberPHIDs) { 344 $subscriber_sets = id(new PhabricatorSubscribersQuery()) 345 ->withObjectPHIDs($phids) 346 ->execute(); 347 foreach ($tasks as $task) { 348 $subscribers = idx($subscriber_sets, $task->getPHID(), array()); 349 $task->attachSubscriberPHIDs($subscribers); 350 } 351 } 352 353 return $tasks; 354 } 355 356 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 357 $where = parent::buildWhereClauseParts($conn); 358 359 $where[] = $this->buildStatusWhereClause($conn); 360 $where[] = $this->buildOwnerWhereClause($conn); 361 362 if ($this->taskIDs !== null) { 363 if (!ctype_digit(implode('', $this->taskIDs))) { 364 throw new PhutilSearchQueryCompilerSyntaxException( 365 pht('Task IDs must be integer numbers.')); 366 } 367 $where[] = qsprintf( 368 $conn, 369 'task.id in (%Ld)', 370 $this->taskIDs); 371 } 372 373 if ($this->taskPHIDs !== null) { 374 $where[] = qsprintf( 375 $conn, 376 'task.phid in (%Ls)', 377 $this->taskPHIDs); 378 } 379 380 if ($this->statuses !== null) { 381 $where[] = qsprintf( 382 $conn, 383 'task.status IN (%Ls)', 384 $this->statuses); 385 } 386 387 if ($this->authorPHIDs !== null) { 388 $where[] = qsprintf( 389 $conn, 390 'task.authorPHID in (%Ls)', 391 $this->authorPHIDs); 392 } 393 394 if ($this->dateCreatedAfter) { 395 $where[] = qsprintf( 396 $conn, 397 'task.dateCreated >= %d', 398 $this->dateCreatedAfter); 399 } 400 401 if ($this->dateCreatedBefore) { 402 $where[] = qsprintf( 403 $conn, 404 'task.dateCreated <= %d', 405 $this->dateCreatedBefore); 406 } 407 408 if ($this->dateModifiedAfter) { 409 $where[] = qsprintf( 410 $conn, 411 'task.dateModified >= %d', 412 $this->dateModifiedAfter); 413 } 414 415 if ($this->dateModifiedBefore) { 416 $where[] = qsprintf( 417 $conn, 418 'task.dateModified <= %d', 419 $this->dateModifiedBefore); 420 } 421 422 if ($this->closedEpochMin !== null) { 423 $where[] = qsprintf( 424 $conn, 425 'task.closedEpoch >= %d', 426 $this->closedEpochMin); 427 } 428 429 if ($this->closedEpochMax !== null) { 430 $where[] = qsprintf( 431 $conn, 432 'task.closedEpoch <= %d', 433 $this->closedEpochMax); 434 } 435 436 if ($this->closerPHIDs !== null) { 437 $where[] = qsprintf( 438 $conn, 439 'task.closerPHID IN (%Ls)', 440 $this->closerPHIDs); 441 } 442 443 if ($this->priorities !== null) { 444 $where[] = qsprintf( 445 $conn, 446 'task.priority IN (%Ld)', 447 $this->priorities); 448 } 449 450 if ($this->bridgedObjectPHIDs !== null) { 451 $where[] = qsprintf( 452 $conn, 453 'task.bridgedObjectPHID IN (%Ls)', 454 $this->bridgedObjectPHIDs); 455 } 456 457 if ($this->subtypes !== null) { 458 $where[] = qsprintf( 459 $conn, 460 'task.subtype IN (%Ls)', 461 $this->subtypes); 462 } 463 464 465 if ($this->columnPHIDs !== null) { 466 $viewer = $this->getViewer(); 467 468 $columns = id(new PhabricatorProjectColumnQuery()) 469 ->setParentQuery($this) 470 ->setViewer($viewer) 471 ->withPHIDs($this->columnPHIDs) 472 ->execute(); 473 if (!$columns) { 474 throw new PhabricatorEmptyQueryException(); 475 } 476 477 // We must do board layout before we move forward because the column 478 // positions may not yet exist otherwise. An example is that newly 479 // created tasks may not yet be positioned in the backlog column. 480 481 $projects = mpull($columns, 'getProject'); 482 $projects = mpull($projects, null, 'getPHID'); 483 484 // The board layout engine needs to know about every object that it's 485 // going to be asked to do layout for. For now, we're just doing layout 486 // on every object on the boards. In the future, we could do layout on a 487 // smaller set of objects by using the constraints on this Query. For 488 // example, if the caller is only asking for open tasks, we only need 489 // to do layout on open tasks. 490 491 // This fetches too many objects (every type of object tagged with the 492 // project, not just tasks). We could narrow it by querying the edge 493 // table on the Maniphest side, but there's currently no way to build 494 // that query with EdgeQuery. 495 $edge_query = id(new PhabricatorEdgeQuery()) 496 ->withSourcePHIDs(array_keys($projects)) 497 ->withEdgeTypes( 498 array( 499 PhabricatorProjectProjectHasObjectEdgeType::EDGECONST, 500 )); 501 502 $edge_query->execute(); 503 $all_phids = $edge_query->getDestinationPHIDs(); 504 505 // Since we overfetched PHIDs, filter out any non-tasks we got back. 506 foreach ($all_phids as $key => $phid) { 507 if (phid_get_type($phid) !== ManiphestTaskPHIDType::TYPECONST) { 508 unset($all_phids[$key]); 509 } 510 } 511 512 // If there are no tasks on the relevant boards, this query can't 513 // possibly hit anything so we're all done. 514 $task_phids = array_fuse($all_phids); 515 if (!$task_phids) { 516 throw new PhabricatorEmptyQueryException(); 517 } 518 519 // We know everything we need to know, so perform board layout. 520 $engine = id(new PhabricatorBoardLayoutEngine()) 521 ->setViewer($viewer) 522 ->setFetchAllBoards(true) 523 ->setBoardPHIDs(array_keys($projects)) 524 ->setObjectPHIDs($task_phids) 525 ->executeLayout(); 526 527 // Find the tasks that are in the constraint columns after board layout 528 // completes. 529 $select_phids = array(); 530 foreach ($columns as $column) { 531 $in_column = $engine->getColumnObjectPHIDs( 532 $column->getProjectPHID(), 533 $column->getPHID()); 534 foreach ($in_column as $phid) { 535 $select_phids[$phid] = $phid; 536 } 537 } 538 539 if (!$select_phids) { 540 throw new PhabricatorEmptyQueryException(); 541 } 542 543 $where[] = qsprintf( 544 $conn, 545 'task.phid IN (%Ls)', 546 $select_phids); 547 } 548 549 if ($this->specificGroupByProjectPHID !== null) { 550 $where[] = qsprintf( 551 $conn, 552 'projectGroupName.indexedObjectPHID = %s', 553 $this->specificGroupByProjectPHID); 554 } 555 556 return $where; 557 } 558 559 private function buildStatusWhereClause(AphrontDatabaseConnection $conn) { 560 static $map = array( 561 self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 562 self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 563 self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 564 self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 565 self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE, 566 ); 567 568 switch ($this->status) { 569 case self::STATUS_ANY: 570 return null; 571 case self::STATUS_OPEN: 572 return qsprintf( 573 $conn, 574 'task.status IN (%Ls)', 575 ManiphestTaskStatus::getOpenStatusConstants()); 576 case self::STATUS_CLOSED: 577 return qsprintf( 578 $conn, 579 'task.status IN (%Ls)', 580 ManiphestTaskStatus::getClosedStatusConstants()); 581 default: 582 $constant = idx($map, $this->status); 583 if (!$constant) { 584 throw new Exception(pht("Unknown status query '%s'!", $this->status)); 585 } 586 return qsprintf( 587 $conn, 588 'task.status = %s', 589 $constant); 590 } 591 } 592 593 private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) { 594 $subclause = array(); 595 596 if ($this->noOwner) { 597 $subclause[] = qsprintf( 598 $conn, 599 'task.ownerPHID IS NULL'); 600 } 601 602 if ($this->anyOwner) { 603 $subclause[] = qsprintf( 604 $conn, 605 'task.ownerPHID IS NOT NULL'); 606 } 607 608 if ($this->ownerPHIDs !== null) { 609 $subclause[] = qsprintf( 610 $conn, 611 'task.ownerPHID IN (%Ls)', 612 $this->ownerPHIDs); 613 } 614 615 if (!$subclause) { 616 return qsprintf($conn, ''); 617 } 618 619 return qsprintf($conn, '%LO', $subclause); 620 } 621 622 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { 623 $open_statuses = ManiphestTaskStatus::getOpenStatusConstants(); 624 $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; 625 $task_table = $this->newResultObject()->getTableName(); 626 627 $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; 628 $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; 629 630 $joins = array(); 631 if ($this->hasOpenParents !== null) { 632 if ($this->hasOpenParents) { 633 $join_type = qsprintf($conn, 'JOIN'); 634 } else { 635 $join_type = qsprintf($conn, 'LEFT JOIN'); 636 } 637 638 $joins[] = qsprintf( 639 $conn, 640 '%Q %T e_parent 641 ON e_parent.src = task.phid 642 AND e_parent.type = %d 643 %Q %T parent 644 ON e_parent.dst = parent.phid 645 AND parent.status IN (%Ls)', 646 $join_type, 647 $edge_table, 648 $parent_type, 649 $join_type, 650 $task_table, 651 $open_statuses); 652 } 653 654 if ($this->hasOpenSubtasks !== null) { 655 if ($this->hasOpenSubtasks) { 656 $join_type = qsprintf($conn, 'JOIN'); 657 } else { 658 $join_type = qsprintf($conn, 'LEFT JOIN'); 659 } 660 661 $joins[] = qsprintf( 662 $conn, 663 '%Q %T e_subtask 664 ON e_subtask.src = task.phid 665 AND e_subtask.type = %d 666 %Q %T subtask 667 ON e_subtask.dst = subtask.phid 668 AND subtask.status IN (%Ls)', 669 $join_type, 670 $edge_table, 671 $subtask_type, 672 $join_type, 673 $task_table, 674 $open_statuses); 675 } 676 677 if ($this->subscriberPHIDs !== null) { 678 $joins[] = qsprintf( 679 $conn, 680 'JOIN %T e_ccs ON e_ccs.src = task.phid '. 681 'AND e_ccs.type = %s '. 682 'AND e_ccs.dst in (%Ls)', 683 PhabricatorEdgeConfig::TABLE_NAME_EDGE, 684 PhabricatorObjectHasSubscriberEdgeType::EDGECONST, 685 $this->subscriberPHIDs); 686 } 687 688 switch ($this->groupBy) { 689 case self::GROUP_PROJECT: 690 $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs(); 691 if ($ignore_group_phids) { 692 $joins[] = qsprintf( 693 $conn, 694 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src 695 AND projectGroup.type = %d 696 AND projectGroup.dst NOT IN (%Ls)', 697 $edge_table, 698 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 699 $ignore_group_phids); 700 } else { 701 $joins[] = qsprintf( 702 $conn, 703 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src 704 AND projectGroup.type = %d', 705 $edge_table, 706 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); 707 } 708 $joins[] = qsprintf( 709 $conn, 710 'LEFT JOIN %T projectGroupName 711 ON projectGroup.dst = projectGroupName.indexedObjectPHID', 712 id(new ManiphestNameIndex())->getTableName()); 713 break; 714 } 715 716 if ($this->parentTaskIDs !== null) { 717 $joins[] = qsprintf( 718 $conn, 719 'JOIN %T e_has_parent 720 ON e_has_parent.src = task.phid 721 AND e_has_parent.type = %d 722 JOIN %T has_parent 723 ON e_has_parent.dst = has_parent.phid 724 AND has_parent.id IN (%Ld)', 725 $edge_table, 726 $parent_type, 727 $task_table, 728 $this->parentTaskIDs); 729 } 730 731 if ($this->subtaskIDs !== null) { 732 $joins[] = qsprintf( 733 $conn, 734 'JOIN %T e_has_subtask 735 ON e_has_subtask.src = task.phid 736 AND e_has_subtask.type = %d 737 JOIN %T has_subtask 738 ON e_has_subtask.dst = has_subtask.phid 739 AND has_subtask.id IN (%Ld)', 740 $edge_table, 741 $subtask_type, 742 $task_table, 743 $this->subtaskIDs); 744 } 745 746 $joins[] = parent::buildJoinClauseParts($conn); 747 748 return $joins; 749 } 750 751 protected function buildGroupClause(AphrontDatabaseConnection $conn) { 752 $joined_multiple_rows = 753 ($this->hasOpenParents !== null) || 754 ($this->hasOpenSubtasks !== null) || 755 ($this->parentTaskIDs !== null) || 756 ($this->subtaskIDs !== null) || 757 $this->shouldGroupQueryResultRows(); 758 759 $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); 760 761 // If we're joining multiple rows, we need to group the results by the 762 // task IDs. 763 if ($joined_multiple_rows) { 764 if ($joined_project_name) { 765 return qsprintf($conn, 'GROUP BY task.phid, projectGroup.dst'); 766 } else { 767 return qsprintf($conn, 'GROUP BY task.phid'); 768 } 769 } 770 771 return qsprintf($conn, ''); 772 } 773 774 775 protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) { 776 $having = parent::buildHavingClauseParts($conn); 777 778 if ($this->hasOpenParents !== null) { 779 if (!$this->hasOpenParents) { 780 $having[] = qsprintf( 781 $conn, 782 'COUNT(parent.phid) = 0'); 783 } 784 } 785 786 if ($this->hasOpenSubtasks !== null) { 787 if (!$this->hasOpenSubtasks) { 788 $having[] = qsprintf( 789 $conn, 790 'COUNT(subtask.phid) = 0'); 791 } 792 } 793 794 return $having; 795 } 796 797 798 /** 799 * Return project PHIDs which we should ignore when grouping tasks by 800 * project. For example, if a user issues a query like: 801 * 802 * Tasks tagged with all projects: Frontend, Bugs 803 * 804 * ...then we don't show "Frontend" or "Bugs" groups in the result set, since 805 * they're meaningless as all results are in both groups. 806 * 807 * Similarly, for queries like: 808 * 809 * Tasks tagged with any projects: Public Relations 810 * 811 * ...we ignore the single project, as every result is in that project. (In 812 * the case that there are several "any" projects, we do not ignore them.) 813 * 814 * @return list<string> Project PHIDs which should be ignored in query 815 * construction. 816 */ 817 private function getIgnoreGroupedProjectPHIDs() { 818 // Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't 819 // impact the results, but we might end up with a better query plan. 820 // Investigate this on real data? This is likely very rare. 821 822 $edge_types = array( 823 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 824 ); 825 826 $phids = array(); 827 828 $phids[] = $this->getEdgeLogicValues( 829 $edge_types, 830 array( 831 PhabricatorQueryConstraint::OPERATOR_AND, 832 )); 833 834 $any = $this->getEdgeLogicValues( 835 $edge_types, 836 array( 837 PhabricatorQueryConstraint::OPERATOR_OR, 838 )); 839 if (count($any) == 1) { 840 $phids[] = $any; 841 } 842 843 return array_mergev($phids); 844 } 845 846 public function getBuiltinOrders() { 847 $orders = array( 848 'priority' => array( 849 'vector' => array('priority', 'id'), 850 'name' => pht('Priority'), 851 'aliases' => array(self::ORDER_PRIORITY), 852 ), 853 'updated' => array( 854 'vector' => array('updated', 'id'), 855 'name' => pht('Date Updated (Latest First)'), 856 'aliases' => array(self::ORDER_MODIFIED), 857 ), 858 'outdated' => array( 859 'vector' => array('-updated', '-id'), 860 'name' => pht('Date Updated (Oldest First)'), 861 ), 862 'closed' => array( 863 'vector' => array('closed', 'id'), 864 'name' => pht('Date Closed (Latest First)'), 865 ), 866 'title' => array( 867 'vector' => array('title', 'id'), 868 'name' => pht('Title'), 869 'aliases' => array(self::ORDER_TITLE), 870 ), 871 ) + parent::getBuiltinOrders(); 872 873 // Alias the "newest" builtin to the historical key for it. 874 $orders['newest']['aliases'][] = self::ORDER_CREATED; 875 876 $orders = array_select_keys( 877 $orders, 878 array( 879 'priority', 880 'updated', 881 'outdated', 882 'newest', 883 'oldest', 884 'closed', 885 'title', 886 )) + $orders; 887 888 return $orders; 889 } 890 891 public function getOrderableColumns() { 892 return parent::getOrderableColumns() + array( 893 'priority' => array( 894 'table' => 'task', 895 'column' => 'priority', 896 'type' => 'int', 897 ), 898 'owner' => array( 899 'table' => 'task', 900 'column' => 'ownerOrdering', 901 'null' => 'head', 902 'reverse' => true, 903 'type' => 'string', 904 ), 905 'status' => array( 906 'table' => 'task', 907 'column' => 'status', 908 'type' => 'string', 909 'reverse' => true, 910 ), 911 'project' => array( 912 'table' => 'projectGroupName', 913 'column' => 'indexedObjectName', 914 'type' => 'string', 915 'null' => 'head', 916 'reverse' => true, 917 ), 918 'title' => array( 919 'table' => 'task', 920 'column' => 'title', 921 'type' => 'string', 922 'reverse' => true, 923 ), 924 'updated' => array( 925 'table' => 'task', 926 'column' => 'dateModified', 927 'type' => 'int', 928 ), 929 'closed' => array( 930 'table' => 'task', 931 'column' => 'closedEpoch', 932 'type' => 'int', 933 'null' => 'tail', 934 ), 935 ); 936 } 937 938 protected function newPagingMapFromCursorObject( 939 PhabricatorQueryCursor $cursor, 940 array $keys) { 941 942 $task = $cursor->getObject(); 943 944 $map = array( 945 'id' => (int)$task->getID(), 946 'priority' => (int)$task->getPriority(), 947 'owner' => $task->getOwnerOrdering(), 948 'status' => $task->getStatus(), 949 'title' => $task->getTitle(), 950 'updated' => (int)$task->getDateModified(), 951 'closed' => $task->getClosedEpoch(), 952 ); 953 954 if (isset($keys['project'])) { 955 $value = null; 956 957 $group_phid = $task->getGroupByProjectPHID(); 958 if ($group_phid) { 959 $paging_projects = id(new PhabricatorProjectQuery()) 960 ->setViewer($this->getViewer()) 961 ->withPHIDs(array($group_phid)) 962 ->execute(); 963 if ($paging_projects) { 964 $value = head($paging_projects)->getName(); 965 } 966 } 967 968 $map['project'] = $value; 969 } 970 971 foreach ($keys as $key) { 972 if ($this->isCustomFieldOrderKey($key)) { 973 $map += $this->getPagingValueMapForCustomFields($task); 974 break; 975 } 976 } 977 978 return $map; 979 } 980 981 protected function newExternalCursorStringForResult($object) { 982 $id = $object->getID(); 983 984 if ($this->groupBy == self::GROUP_PROJECT) { 985 return rtrim($id.'.'.$object->getGroupByProjectPHID(), '.'); 986 } 987 988 return $id; 989 } 990 991 protected function newInternalCursorFromExternalCursor($cursor) { 992 list($task_id, $group_phid) = $this->parseCursor($cursor); 993 994 $cursor_object = parent::newInternalCursorFromExternalCursor($cursor); 995 996 if ($group_phid !== null) { 997 $project = id(new PhabricatorProjectQuery()) 998 ->setViewer($this->getViewer()) 999 ->withPHIDs(array($group_phid)) 1000 ->execute(); 1001 1002 if (!$project) { 1003 $this->throwCursorException( 1004 pht( 1005 'Group PHID ("%s") component of cursor ("%s") is not valid.', 1006 $group_phid, 1007 $cursor)); 1008 } 1009 1010 $cursor_object->getObject()->attachGroupByProjectPHID($group_phid); 1011 } 1012 1013 return $cursor_object; 1014 } 1015 1016 protected function applyExternalCursorConstraintsToQuery( 1017 PhabricatorCursorPagedPolicyAwareQuery $subquery, 1018 $cursor) { 1019 list($task_id, $group_phid) = $this->parseCursor($cursor); 1020 1021 $subquery->withIDs(array($task_id)); 1022 1023 if ($group_phid) { 1024 $subquery->setGroupBy(self::GROUP_PROJECT); 1025 1026 // The subquery needs to return exactly one result. If a task is in 1027 // several projects, the query may naturally return several results. 1028 // Specify that we want only the particular instance of the task in 1029 // the specified project. 1030 $subquery->withSpecificGroupByProjectPHID($group_phid); 1031 } 1032 } 1033 1034 1035 private function parseCursor($cursor) { 1036 // Split a "123.PHID-PROJ-abcd" cursor into a "Task ID" part and a 1037 // "Project PHID" part. 1038 1039 $parts = explode('.', $cursor, 2); 1040 1041 if (count($parts) < 2) { 1042 $parts[] = null; 1043 } 1044 1045 if (!phutil_nonempty_string($parts[1])) { 1046 $parts[1] = null; 1047 } 1048 1049 return $parts; 1050 } 1051 1052 protected function getPrimaryTableAlias() { 1053 return 'task'; 1054 } 1055 1056 public function getQueryApplicationClass() { 1057 return PhabricatorManiphestApplication::class; 1058 } 1059 1060}