@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Service "Group by: Project" in Maniphest out of a local index

Summary:
See discussion in D6955. Currently, the logic for "Group by: Project" is roughly:

- Load every possible result.
- Lots of in-process garbage.

Instead, use the new local project name index (from D6957) to service this query more reasonably. Basically:

- Join a table which has keyed project names.
- Order by that table.

Test Plan: {F58033}

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Differential Revision: https://secure.phabricator.com/D6958

+163 -171
+11 -16
src/applications/maniphest/controller/ManiphestTaskListController.php
··· 631 631 } 632 632 break; 633 633 case 'project': 634 - $grouped = array(); 635 - foreach ($query->getGroupByProjectResults() as $project => $tasks) { 636 - foreach ($tasks as $task) { 637 - $group = 'No Project'; 638 - if ($project && isset($handles[$project])) { 639 - $group = $handles[$project]->getName(); 634 + $data = mgroup($data, 'getGroupByProjectPHID'); 635 + 636 + $out = array(); 637 + foreach ($data as $phid => $tasks) { 638 + $name = pht('No Project'); 639 + if ($phid) { 640 + $handle = idx($handles, $phid); 641 + if ($handle) { 642 + $name = $handles[$phid]->getFullName(); 640 643 } 641 - $grouped[$group][$task->getID()] = $task; 642 644 } 645 + $out[$name] = $tasks; 643 646 } 644 - $data = $grouped; 645 - ksort($data); 646 - 647 - // Move "No Project" to the end of the list. 648 - if (isset($data['No Project'])) { 649 - $noproject = $data['No Project']; 650 - unset($data['No Project']); 651 - $data += array('No Project' => $noproject); 652 - } 647 + $data = $out; 653 648 break; 654 649 default: 655 650 $data = array(
+142 -155
src/applications/maniphest/query/ManiphestTaskQuery.php
··· 64 64 65 65 private $rowCount = null; 66 66 67 - private $groupByProjectResults = null; // See comment at bottom for details 68 - 69 67 public function withAuthors(array $authors) { 70 68 $this->authorPHIDs = $authors; 71 69 return $this; ··· 171 169 return $this->rowCount; 172 170 } 173 171 174 - public function getGroupByProjectResults() { 175 - return $this->groupByProjectResults; 176 - } 177 - 178 172 public function withAnyProjects(array $projects) { 179 173 $this->anyProjectPHIDs = $projects; 180 174 return $this; ··· 247 241 } 248 242 249 243 $where = $this->formatWhereClause($where); 250 - 251 - $join = array(); 252 - $join[] = $this->buildProjectJoinClause($conn); 253 - $join[] = $this->buildAnyProjectJoinClause($conn); 254 - $join[] = $this->buildXProjectJoinClause($conn); 255 - $join[] = $this->buildSubscriberJoinClause($conn); 256 - 257 - $join = array_filter($join); 258 - if ($join) { 259 - $join = implode(' ', $join); 260 - } else { 261 - $join = ''; 262 - } 263 244 264 245 $having = ''; 265 246 $count = ''; 266 - $group = ''; 267 - 268 - if (count($this->projectPHIDs) > 1 || count($this->anyProjectPHIDs) > 1) { 269 - // If we're joining multiple rows, we need to group the results by the 270 - // task IDs. 271 - $group = 'GROUP BY task.id'; 272 - } else { 273 - $group = ''; 274 - } 275 247 276 248 if (count($this->projectPHIDs) > 1) { 277 249 // We want to treat the query as an intersection query, not a union ··· 292 264 $this->setLimit(self::DEFAULT_PAGE_SIZE); 293 265 } 294 266 295 - if ($this->groupBy == self::GROUP_PROJECT) { 296 - $this->setLimit(PHP_INT_MAX); 297 - $this->setOffset(0); 267 + $group_column = ''; 268 + switch ($this->groupBy) { 269 + case self::GROUP_PROJECT: 270 + $group_column = qsprintf( 271 + $conn, 272 + ', projectGroupName.indexedObjectPHID projectGroupPHID'); 273 + break; 298 274 } 299 275 300 - $data = queryfx_all( 276 + $rows = queryfx_all( 301 277 $conn, 302 - 'SELECT %Q * %Q FROM %T task %Q %Q %Q %Q %Q %Q', 278 + 'SELECT %Q task.* %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', 303 279 $calc, 304 280 $count, 281 + $group_column, 305 282 $task_dao->getTableName(), 306 - $join, 283 + $this->buildJoinsClause($conn), 307 284 $where, 308 - $group, 285 + $this->buildGroupClause($conn), 309 286 $having, 310 287 $order, 311 288 $this->buildLimitClause($conn)); ··· 319 296 $this->rowCount = null; 320 297 } 321 298 299 + switch ($this->groupBy) { 300 + case self::GROUP_PROJECT: 301 + $data = ipull($rows, null, 'id'); 302 + break; 303 + default: 304 + $data = $rows; 305 + break; 306 + } 307 + 322 308 $tasks = $task_dao->loadAllFromArray($data); 323 309 324 - if ($this->groupBy == self::GROUP_PROJECT) { 325 - $tasks = $this->applyGroupByProject($tasks); 310 + switch ($this->groupBy) { 311 + case self::GROUP_PROJECT: 312 + $results = array(); 313 + foreach ($rows as $row) { 314 + $task = clone $tasks[$row['id']]; 315 + $task->attachGroupByProjectPHID($row['projectGroupPHID']); 316 + $results[] = $task; 317 + } 318 + $tasks = $results; 319 + break; 326 320 } 327 321 328 322 return $tasks; ··· 514 508 return '('.implode(') OR (', $parts).')'; 515 509 } 516 510 517 - private function buildProjectJoinClause(AphrontDatabaseConnection $conn) { 518 - if (!$this->projectPHIDs && !$this->includeNoProject) { 519 - return null; 520 - } 521 - 522 - $project_dao = new ManiphestTaskProject(); 523 - return qsprintf( 524 - $conn, 525 - '%Q JOIN %T project ON project.taskPHID = task.phid', 526 - ($this->includeNoProject ? 'LEFT' : ''), 527 - $project_dao->getTableName()); 528 - } 529 - 530 511 private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) { 531 512 if (!$this->anyProjectPHIDs) { 532 513 return null; ··· 559 540 $any_user_project_phids); 560 541 } 561 542 562 - private function buildAnyProjectJoinClause(AphrontDatabaseConnection $conn) { 563 - if (!$this->anyProjectPHIDs && !$this->anyUserProjectPHIDs) { 564 - return null; 565 - } 566 - 567 - $project_dao = new ManiphestTaskProject(); 568 - return qsprintf( 569 - $conn, 570 - 'JOIN %T anyproject ON anyproject.taskPHID = task.phid', 571 - $project_dao->getTableName()); 572 - } 573 - 574 543 private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) { 575 544 if (!$this->xprojectPHIDs) { 576 545 return null; ··· 581 550 'xproject.projectPHID IS NULL'); 582 551 } 583 552 584 - private function buildXProjectJoinClause(AphrontDatabaseConnection $conn) { 585 - if (!$this->xprojectPHIDs) { 586 - return null; 587 - } 588 - 589 - $project_dao = new ManiphestTaskProject(); 590 - return qsprintf( 591 - $conn, 592 - 'LEFT JOIN %T xproject ON xproject.taskPHID = task.phid 593 - AND xproject.projectPHID IN (%Ls)', 594 - $project_dao->getTableName(), 595 - $this->xprojectPHIDs); 596 - } 597 - 598 - private function buildSubscriberJoinClause(AphrontDatabaseConnection $conn) { 599 - if (!$this->subscriberPHIDs) { 600 - return null; 601 - } 602 - 603 - $subscriber_dao = new ManiphestTaskSubscriber(); 604 - return qsprintf( 605 - $conn, 606 - 'JOIN %T subscriber ON subscriber.taskPHID = task.phid', 607 - $subscriber_dao->getTableName()); 608 - } 609 - 610 553 private function buildCustomOrderClause(AphrontDatabaseConnection $conn) { 611 554 $order = array(); 612 555 ··· 623 566 $order[] = 'status'; 624 567 break; 625 568 case self::GROUP_PROJECT: 626 - // NOTE: We have to load the entire result set and apply this grouping 627 - // in the PHP process for now. 569 + $order[] = '<group.project>'; 628 570 break; 629 571 default: 630 572 throw new Exception("Unknown group query '{$this->groupBy}'!"); ··· 662 604 case 'title': 663 605 $order[$k] = "task.{$column} ASC"; 664 606 break; 607 + case '<group.project>': 608 + // Put "No Project" at the end of the list. 609 + $order[$k] = 610 + 'projectGroupName.indexedObjectName IS NULL ASC, '. 611 + 'projectGroupName.indexedObjectName ASC'; 612 + break; 665 613 default: 666 614 $order[$k] = "task.{$column} DESC"; 667 615 break; ··· 671 619 return 'ORDER BY '.implode(', ', $order); 672 620 } 673 621 622 + private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { 623 + $project_dao = new ManiphestTaskProject(); 674 624 675 - /** 676 - * To get paging to work for "group by project", we need to do a bunch of 677 - * server-side magic since there's currently no way to sort by project name on 678 - * the database. 679 - * 680 - * As a consequence of this, moreover, because the list we return from here 681 - * may include a single task multiple times (once for each project it's in), 682 - * sorting gets screwed up in the controller unless we tell it which project 683 - * to put the task in each time it appears. Hence the magic field 684 - * groupByProjectResults. 685 - * 686 - * TODO: Move this all to the database. 687 - */ 688 - private function applyGroupByProject(array $tasks) { 689 - assert_instances_of($tasks, 'ManiphestTask'); 625 + $joins = array(); 626 + 627 + if ($this->projectPHIDs || $this->includeNoProject) { 628 + $joins[] = qsprintf( 629 + $conn_r, 630 + '%Q JOIN %T project ON project.taskPHID = task.phid', 631 + ($this->includeNoProject ? 'LEFT' : ''), 632 + $project_dao->getTableName()); 633 + } 690 634 691 - $project_phids = array(); 692 - foreach ($tasks as $task) { 693 - foreach ($task->getProjectPHIDs() as $phid) { 694 - $project_phids[$phid] = true; 695 - } 635 + if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) { 636 + $joins[] = qsprintf( 637 + $conn_r, 638 + 'JOIN %T anyproject ON anyproject.taskPHID = task.phid', 639 + $project_dao->getTableName()); 696 640 } 697 641 698 - $handles = id(new PhabricatorHandleQuery()) 699 - ->setViewer($this->getViewer()) 700 - ->withPHIDs(array_keys($project_phids)) 701 - ->execute(); 642 + if ($this->xprojectPHIDs) { 643 + $joins[] = qsprintf( 644 + $conn_r, 645 + 'LEFT JOIN %T xproject ON xproject.taskPHID = task.phid 646 + AND xproject.projectPHID IN (%Ls)', 647 + $project_dao->getTableName(), 648 + $this->xprojectPHIDs); 649 + } 702 650 703 - $max = 1; 704 - foreach ($handles as $handle) { 705 - $max = max($max, strlen($handle->getName())); 651 + if ($this->subscriberPHIDs) { 652 + $subscriber_dao = new ManiphestTaskSubscriber(); 653 + $joins[] = qsprintf( 654 + $conn_r, 655 + 'JOIN %T subscriber ON subscriber.taskPHID = task.phid', 656 + $subscriber_dao->getTableName()); 706 657 } 707 658 708 - $items = array(); 709 - $ii = 0; 710 - foreach ($tasks as $key => $task) { 711 - $phids = $task->getProjectPHIDs(); 712 - if ($this->projectPHIDs) { 713 - $phids = array_diff($phids, $this->projectPHIDs); 714 - } 715 - if ($phids) { 716 - foreach ($phids as $phid) { 717 - $items[] = array( 718 - 'key' => $key, 719 - 'proj' => $phid, 720 - 'seq' => sprintf( 721 - '%'.$max.'s%09d', 722 - $handles[$phid]->getName(), 723 - $ii), 724 - ); 659 + switch ($this->groupBy) { 660 + case self::GROUP_PROJECT: 661 + $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs(); 662 + if ($ignore_group_phids) { 663 + $joins[] = qsprintf( 664 + $conn_r, 665 + 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.taskPHID 666 + AND projectGroup.projectPHID NOT IN (%Ls)', 667 + $project_dao->getTableName(), 668 + $ignore_group_phids); 669 + } else { 670 + $joins[] = qsprintf( 671 + $conn_r, 672 + 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.taskPHID', 673 + $project_dao->getTableName()); 725 674 } 675 + $joins[] = qsprintf( 676 + $conn_r, 677 + 'LEFT JOIN %T projectGroupName 678 + ON projectGroup.projectPHID = projectGroupName.indexedObjectPHID', 679 + id(new ManiphestNameIndex())->getTableName()); 680 + break; 681 + } 682 + 683 + return implode(' ', $joins); 684 + } 685 + 686 + private function buildGroupClause(AphrontDatabaseConnection $conn_r) { 687 + $joined_multiple_project_rows = (count($this->projectPHIDs) > 1) || 688 + (count($this->anyProjectPHIDs) > 1); 689 + 690 + $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); 691 + 692 + // If we're joining multiple rows, we need to group the results by the 693 + // task IDs. 694 + if ($joined_multiple_project_rows) { 695 + if ($joined_project_name) { 696 + return 'GROUP BY task.id, projectGroup.projectPHID'; 726 697 } else { 727 - // Sort "no project" tasks first. 728 - $items[] = array( 729 - 'key' => $key, 730 - 'proj' => null, 731 - 'seq' => sprintf( 732 - '%'.$max.'s%09d', 733 - '', 734 - $ii), 735 - ); 698 + return 'GROUP BY task.id'; 736 699 } 737 - ++$ii; 700 + } else { 701 + return ''; 738 702 } 703 + } 739 704 740 - $items = isort($items, 'seq'); 741 - $items = array_slice( 742 - $items, 743 - nonempty($this->getOffset()), 744 - nonempty($this->getLimit(), self::DEFAULT_PAGE_SIZE)); 705 + /** 706 + * Return project PHIDs which we should ignore when grouping tasks by 707 + * project. For example, if a user issues a query like: 708 + * 709 + * Tasks in all projects: Frontend, Bugs 710 + * 711 + * ...then we don't show "Frontend" or "Bugs" groups in the result set, since 712 + * they're meaningless as all results are in both groups. 713 + * 714 + * Similarly, for queries like: 715 + * 716 + * Tasks in any projects: Public Relations 717 + * 718 + * ...we ignore the single project, as every result is in that project. (In 719 + * the case that there are several "any" projects, we do not ignore them.) 720 + * 721 + * @return list<phid> Project PHIDs which should be ignored in query 722 + * construction. 723 + */ 724 + private function getIgnoreGroupedProjectPHIDs() { 725 + $phids = array(); 745 726 746 - $result = array(); 747 - $projects = array(); 748 - foreach ($items as $item) { 749 - $result[] = $projects[$item['proj']][] = $tasks[$item['key']]; 727 + if ($this->projectPHIDs) { 728 + $phids[] = $this->projectPHIDs; 750 729 } 751 - $this->groupByProjectResults = $projects; 752 730 753 - return $result; 731 + if (count($this->anyProjectPHIDs) == 1) { 732 + $phids[] = $this->anyProjectPHIDs; 733 + } 734 + 735 + // Maybe we should also exclude the "excludeProjectPHIDs"? It won't 736 + // impact the results, but we might end up with a better query plan. 737 + // Investigate this on real data? This is likely very rare. 738 + 739 + return array_mergev($phids); 754 740 } 741 + 755 742 756 743 }
+10
src/applications/maniphest/storage/ManiphestTask.php
··· 36 36 37 37 private $auxiliaryAttributes = self::ATTACHABLE; 38 38 private $auxiliaryDirty = array(); 39 + private $groupByProjectPHID = self::ATTACHABLE; 39 40 40 41 public function getConfiguration() { 41 42 return array( ··· 113 114 $this->originalTitle = $title; 114 115 } 115 116 return $this; 117 + } 118 + 119 + public function attachGroupByProjectPHID($phid) { 120 + $this->groupByProjectPHID = $phid; 121 + return $this; 122 + } 123 + 124 + public function getGroupByProjectPHID() { 125 + return $this->assertAttached($this->groupByProjectPHID); 116 126 } 117 127 118 128 public function attachAuxiliaryAttributes(array $attrs) {