@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<?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}