diff --git a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
index fdeb8fda8..467682923 100644
--- a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
+++ b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
@@ -1,215 +1,276 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 /**
  * Load object edges created by @{class:PhabricatorEdgeEditor}.
  *
  *   name=Querying Edges
  *   $src  = $earth_phid;
  *   $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
  *
  *   // Load the earth's satellites.
  *   $satellite_edges = id(new PhabricatorEdgeQuery())
  *     ->withSourcePHIDs(array($src))
  *     ->withEdgeTypes(array($type))
  *     ->execute();
  *
  * For more information on edges, see @{article:Using Edges}.
  *
  * @task config   Configuring the Query
  * @task exec     Executing the Query
  * @task internal Internal
  */
 final class PhabricatorEdgeQuery extends PhabricatorQuery {
 
   private $sourcePHIDs;
   private $edgeTypes;
+  private $resultSet;
 
   private $needEdgeData;
 
 
 /* -(  Configuring the Query  )---------------------------------------------- */
 
 
   /**
    * Find edges originating at one or more source PHIDs. You MUST provide this
    * to execute an edge query.
    *
    * @param list List of source PHIDs.
    * @return this
    *
    * @task config
    */
   public function withSourcePHIDs(array $source_phids) {
     $this->sourcePHIDs = $source_phids;
     return $this;
   }
 
 
   /**
    * Find edges of specific types.
    *
    * @param list List of PhabricatorEdgeConfig type constants.
    * @return this
    *
    * @task config
    */
   public function withEdgeTypes(array $types) {
     $this->edgeTypes = $types;
     return $this;
   }
 
 
   /**
    * When loading edges, also load edge data.
    *
    * @param bool True to load edge data.
    * @return this
    *
    * @task config
    */
   public function needEdgeData($need) {
     $this->needEdgeData = $need;
     return $this;
   }
 
 
 /* -(  Executing the Query  )------------------------------------------------ */
 
 
   /**
    * Convenience method for loading destination PHIDs with one source and one
    * edge type. Equivalent to building a full query, but simplifies a common
    * use case.
    *
    * @param phid  Source PHID.
    * @param const Edge type.
    * @return list<phid> List of destination PHIDs.
    */
   public static function loadDestinationPHIDs($src_phid, $edge_type) {
     $edges = id(new PhabricatorEdgeQuery())
       ->withSourcePHIDs(array($src_phid))
       ->withEdgeTypes(array($edge_type))
       ->execute();
     return array_keys($edges[$src_phid][$edge_type]);
   }
 
 
   /**
    * Load specified edges.
    *
    * @task exec
    */
   public function execute() {
     if (!$this->sourcePHIDs) {
       throw new Exception(
       "You must use withSourcePHIDs() to query edges.");
     }
 
     $sources = phid_group_by_type($this->sourcePHIDs);
 
     $result = array();
 
     // When a query specifies types, make sure we return data for all queried
     // types. This is mostly to make sure PhabricatorLiskDAO->attachEdges()
     // gets some data, so that getEdges() doesn't throw later.
     if ($this->edgeTypes) {
       foreach ($this->sourcePHIDs as $phid) {
         foreach ($this->edgeTypes as $type) {
           $result[$phid][$type] = array();
         }
       }
     }
 
     foreach ($sources as $type => $phids) {
       $conn_r = PhabricatorEdgeConfig::establishConnection($type, 'r');
 
       $where = $this->buildWhereClause($conn_r);
       $order = $this->buildOrderClause($conn_r);
 
       $edges = queryfx_all(
         $conn_r,
         'SELECT edge.* FROM %T edge %Q %Q',
         PhabricatorEdgeConfig::TABLE_NAME_EDGE,
         $where,
         $order);
 
       if ($this->needEdgeData) {
         $data_ids = array_filter(ipull($edges, 'dataID'));
         $data_map = array();
         if ($data_ids) {
           $data_rows = queryfx_all(
             $conn_r,
             'SELECT edgedata.* FROM %T edgedata WHERE id IN (%Ld)',
             PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
             $data_ids);
           foreach ($data_rows as $row) {
             $data_map[$row['id']] = idx(
               json_decode($row['data'], true),
               'data');
           }
         }
         foreach ($edges as $key => $edge) {
           $edges[$key]['data'] = idx($data_map, $edge['dataID']);
         }
       }
 
       foreach ($edges as $edge) {
         $result[$edge['src']][$edge['type']][$edge['dst']] = $edge;
       }
     }
 
+    $this->resultSet = $result;
     return $result;
   }
 
 
+  /**
+   * Convenience function for selecting edge destination PHIDs after calling
+   * execute().
+   *
+   * Returns a flat list of PHIDs matching the provided source PHID and type
+   * filters. By default, the filters are empty so all PHIDs will be returned.
+   * For example, if you're doing a batch query from several sources, you might
+   * write code like this:
+   *
+   *   $query = new PhabricatorEdgeQuery();
+   *   $query->withSourcePHIDs(mpull($objects, 'getPHID'));
+   *   $query->withEdgeTypes(array($some_type));
+   *   $query->execute();
+   *
+   *   // Gets all of the destinations.
+   *   $all_phids = $query->getDestinationPHIDs();
+   *   $handles = id(new PhabricatorObjectHandleData($all_phids))
+   *     ->loadHandles();
+   *
+   *   foreach ($objects as $object) {
+   *     // Get all of the destinations for the given object.
+   *     $dst_phids = $query->getDestinationPHIDs(array($object->getPHID()));
+   *     $object->attachHandles(array_select_keys($handles, $dst_phids));
+   *   }
+   *
+   * @param list? List of PHIDs to select, or empty to select all.
+   * @param list? List of edge types to select, or empty to select all.
+   * @return list<phid> List of matching destination PHIDs.
+   */
+  public function getDestinationPHIDs(
+    array $src_phids = array(),
+    array $types = array()) {
+    if ($this->resultSet === null) {
+      throw new Exception(
+        "You must execute() a query before you you can getDestinationPHIDs().");
+    }
+
+    $src_phids = array_fill_keys($src_phids, true);
+    $types = array_fill_keys($types, true);
+
+    $result_phids = array();
+    foreach ($this->resultSet as $src => $edges_by_type) {
+      if ($src_phids && empty($src_phids[$src])) {
+        continue;
+      }
+      foreach ($edges_by_type as $type => $edges_by_dst) {
+        if ($types && empty($types[$type])) {
+          continue;
+        }
+        foreach ($edges_by_dst as $dst => $edge) {
+          $result_phids[$dst] = true;
+        }
+      }
+    }
+
+    return array_keys($result_phids);
+  }
+
+
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   /**
    * @task internal
    */
   private function buildWhereClause($conn_r) {
     $where = array();
 
     if ($this->sourcePHIDs) {
       $where[] = qsprintf(
         $conn_r,
         'edge.src IN (%Ls)',
         $this->sourcePHIDs);
     }
 
     if ($this->edgeTypes) {
       $where[] = qsprintf(
         $conn_r,
         'edge.type IN (%Ls)',
         $this->edgeTypes);
     }
 
     return $this->formatWhereClause($where);
   }
 
 
   /**
    * @task internal
    */
   private function buildOrderClause($conn_r) {
     return 'ORDER BY edge.dateCreated DESC, edge.seq ASC';
   }
 
 }