4 * Add and remove edges between objects. You can use
5 * @{class:PhabricatorEdgeQuery} to load object edges. For more information
6 * on edges, see @{article:Using Edges}.
8 * Edges are not directly policy aware, and this editor makes low-level changes
9 * below the policy layer.
13 * $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
16 * id(new PhabricatorEdgeEditor())
17 * ->addEdge($src, $type, $dst)
20 * @task edit Editing Edges
21 * @task cycles Cycle Prevention
22 * @task internal Internals
24 final class PhabricatorEdgeEditor
extends Phobject
{
26 private $addEdges = array();
27 private $remEdges = array();
28 private $openTransactions = array();
31 /* -( Editing Edges )------------------------------------------------------ */
35 * Add a new edge (possibly also adding its inverse). Changes take effect when
36 * you call @{method:save}. If the edge already exists, it will not be
37 * overwritten, but if data is attached to the edge it will be updated.
38 * Removals queued with @{method:removeEdge} are executed before
39 * adds, so the effect of removing and adding the same edge is to overwrite
42 * The `$options` parameter accepts these values:
44 * - `data` Optional, data to write onto the edge.
45 * - `inverse_data` Optional, data to write on the inverse edge. If not
46 * provided, `data` will be written.
48 * @param phid Source object PHID.
49 * @param const Edge type constant.
50 * @param phid Destination object PHID.
51 * @param map Options map (see documentation).
56 public function addEdge($src, $type, $dst, array $options = array()) {
57 foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) {
58 $this->addEdges
[] = $spec;
65 * Remove an edge (possibly also removing its inverse). Changes take effect
66 * when you call @{method:save}. If an edge does not exist, the removal
67 * will be ignored. Edges are added after edges are removed, so the effect of
68 * a remove plus an add is to overwrite.
70 * @param phid Source object PHID.
71 * @param const Edge type constant.
72 * @param phid Destination object PHID.
77 public function removeEdge($src, $type, $dst) {
78 foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) {
79 $this->remEdges
[] = $spec;
86 * Apply edge additions and removals queued by @{method:addEdge} and
87 * @{method:removeEdge}. Note that transactions are opened, all additions and
88 * removals are executed, and then transactions are saved. Thus, in some cases
89 * it may be slightly more efficient to perform multiple edit operations
90 * (e.g., adds followed by removals) if their outcomes are not dependent,
91 * since transactions will not be held open as long.
96 public function save() {
98 $cycle_types = $this->getPreventCyclesEdgeTypes();
104 // NOTE: We write edge data first, before doing any transactions, since
105 // it's OK if we just leave it hanging out in space unattached to
107 $this->writeEdgeData();
109 // If we're going to perform cycle detection, lock the edge type before
112 $src_phids = ipull($this->addEdges
, 'src');
113 foreach ($cycle_types as $cycle_type) {
114 $key = 'edge.cycle:'.$cycle_type;
115 $locks[] = PhabricatorGlobalLock
::newLock($key)->lock(15);
122 // NOTE: Removes first, then adds, so that "remove + add" is a useful
123 // operation meaning "overwrite".
125 $this->executeRemoves();
126 $this->executeAdds();
128 foreach ($cycle_types as $cycle_type) {
129 $this->detectCycles($src_phids, $cycle_type);
132 $this->saveTransactions();
133 } catch (Exception
$ex) {
138 $this->killTransactions();
141 foreach ($locks as $lock) {
151 /* -( Internals )---------------------------------------------------------- */
155 * Build the specification for an edge operation, and possibly build its
160 private function buildEdgeSpecs($src, $type, $dst, array $options = array()) {
162 if (!empty($options['data'])) {
163 $data['data'] = $options['data'];
166 $src_type = phid_get_type($src);
167 $dst_type = phid_get_type($dst);
172 'src_type' => $src_type,
174 'dst_type' => $dst_type,
179 $type_obj = PhabricatorEdgeType
::getByConstant($type);
180 $inverse = $type_obj->getInverseEdgeConstant();
181 if ($inverse !== null) {
183 // If `inverse_data` is set, overwrite the edge data. Normally, just
184 // write the same data to the inverse edge.
185 if (array_key_exists('inverse_data', $options)) {
186 $data['data'] = $options['inverse_data'];
191 'src_type' => $dst_type,
193 'dst_type' => $src_type,
208 private function writeEdgeData() {
209 $adds = $this->addEdges
;
212 foreach ($adds as $key => $edge) {
214 $writes[] = array($key, $edge['src_type'], json_encode($edge['data']));
218 foreach ($writes as $write) {
219 list($key, $src_type, $data) = $write;
220 $conn_w = PhabricatorEdgeConfig
::establishConnection($src_type, 'w');
223 'INSERT INTO %T (data) VALUES (%s)',
224 PhabricatorEdgeConfig
::TABLE_NAME_EDGEDATA
,
226 $this->addEdges
[$key]['data_id'] = $conn_w->getInsertID();
236 private function executeAdds() {
237 $adds = $this->addEdges
;
238 $adds = igroup($adds, 'src_type');
240 // Assign stable sequence numbers to each edge, so we have a consistent
241 // ordering across edges by source and type.
242 foreach ($adds as $src_type => $edges) {
243 $edges_by_src = igroup($edges, 'src');
244 foreach ($edges_by_src as $src => $src_edges) {
246 foreach ($src_edges as $key => $edge) {
247 $src_edges[$key]['seq'] = $seq++
;
248 $src_edges[$key]['dateCreated'] = time();
250 $edges_by_src[$src] = $src_edges;
252 $adds[$src_type] = array_mergev($edges_by_src);
256 foreach ($adds as $src_type => $edges) {
257 $conn_w = PhabricatorEdgeConfig
::establishConnection($src_type, 'w');
259 foreach ($edges as $edge) {
262 '(%s, %d, %s, %d, %d, %nd)',
266 $edge['dateCreated'],
268 idx($edge, 'data_id'));
270 $inserts[] = array($conn_w, $sql);
273 foreach ($inserts as $insert) {
274 list($conn_w, $sql) = $insert;
275 $conn_w->openTransaction();
276 $this->openTransactions
[] = $conn_w;
278 foreach (PhabricatorLiskDAO
::chunkSQL($sql) as $chunk) {
281 'INSERT INTO %T (src, type, dst, dateCreated, seq, dataID)
282 VALUES %LQ ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)',
283 PhabricatorEdgeConfig
::TABLE_NAME_EDGE
,
291 * Remove queued edges.
295 private function executeRemoves() {
296 $rems = $this->remEdges
;
297 $rems = igroup($rems, 'src_type');
300 foreach ($rems as $src_type => $edges) {
301 $conn_w = PhabricatorEdgeConfig
::establishConnection($src_type, 'w');
303 foreach ($edges as $edge) {
306 '(src = %s AND type = %d AND dst = %s)',
311 $deletes[] = array($conn_w, $sql);
314 foreach ($deletes as $delete) {
315 list($conn_w, $sql) = $delete;
317 $conn_w->openTransaction();
318 $this->openTransactions
[] = $conn_w;
320 foreach (array_chunk($sql, 256) as $chunk) {
323 'DELETE FROM %T WHERE %LO',
324 PhabricatorEdgeConfig
::TABLE_NAME_EDGE
,
332 * Save open transactions.
336 private function saveTransactions() {
337 foreach ($this->openTransactions
as $key => $conn_w) {
338 $conn_w->saveTransaction();
339 unset($this->openTransactions
[$key]);
343 private function killTransactions() {
344 foreach ($this->openTransactions
as $key => $conn_w) {
345 $conn_w->killTransaction();
346 unset($this->openTransactions
[$key]);
351 /* -( Cycle Prevention )--------------------------------------------------- */
355 * Get a list of all edge types which are being added, and which we should
358 * @return list<const> List of edge types which should have cycles prevented.
361 private function getPreventCyclesEdgeTypes() {
362 $edge_types = array();
363 foreach ($this->addEdges
as $edge) {
364 $edge_types[$edge['type']] = true;
366 foreach ($edge_types as $type => $ignored) {
367 $type_obj = PhabricatorEdgeType
::getByConstant($type);
368 if (!$type_obj->shouldPreventCycles()) {
369 unset($edge_types[$type]);
372 return array_keys($edge_types);
377 * Detect graph cycles of a given edge type. If the edit introduces a cycle,
378 * a @{class:PhabricatorEdgeCycleException} is thrown with details.
383 private function detectCycles(array $phids, $edge_type) {
384 // For simplicity, we just seed the graph with the affected nodes rather
385 // than seeding it with their edges. To do this, we just add synthetic
386 // edges from an imaginary '<seed>' node to the known edges.
389 $graph = id(new PhabricatorEdgeGraph())
390 ->setEdgeType($edge_type)
397 foreach ($phids as $phid) {
398 $cycle = $graph->detectCycles($phid);
400 throw new PhabricatorEdgeCycleException($edge_type, $cycle);