@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 * 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}.
7 *
8 * Edges are not directly policy aware, and this editor makes low-level changes
9 * below the policy layer.
10 *
11 * name=Adding Edges
12 * $src = $earth_phid;
13 * $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
14 * $dst = $moon_phid;
15 *
16 * id(new PhabricatorEdgeEditor())
17 * ->addEdge($src, $type, $dst)
18 * ->save();
19 *
20 * @task edit Editing Edges
21 * @task cycles Cycle Prevention
22 * @task internal Internals
23 */
24final class PhabricatorEdgeEditor extends Phobject {
25
26 private $addEdges = array();
27 private $remEdges = array();
28 private $openTransactions = array();
29
30
31/* -( Editing Edges )------------------------------------------------------ */
32
33
34 /**
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
40 * any existing edge.
41 *
42 * The `$options` parameter accepts these values:
43 *
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.
47 *
48 * @param string $src Source object PHID.
49 * @param int $edge_type Edge type constant
50 * (SomeClassEdgeType::EDGECONST).
51 * @param string $dst Destination object PHID.
52 * @param map $options (optional) Options map (see documentation).
53 * @return $this
54 *
55 * @task edit
56 */
57 public function addEdge($src, $edge_type, $dst, array $options = array()) {
58 foreach ($this->buildEdgeSpecs($src, $edge_type, $dst, $options) as $spec) {
59 $this->addEdges[] = $spec;
60 }
61 return $this;
62 }
63
64
65 /**
66 * Remove an edge (possibly also removing its inverse). Changes take effect
67 * when you call @{method:save}. If an edge does not exist, the removal
68 * will be ignored. Edges are added after edges are removed, so the effect of
69 * a remove plus an add is to overwrite.
70 *
71 * @param string $src Source object PHID.
72 * @param int $edge_type Edge type constant
73 * (SomeClassEdgeType::EDGECONST).
74 * @param string $dst Destination object PHID.
75 * @return $this
76 *
77 * @task edit
78 */
79 public function removeEdge($src, $edge_type, $dst) {
80 foreach ($this->buildEdgeSpecs($src, $edge_type, $dst) as $spec) {
81 $this->remEdges[] = $spec;
82 }
83 return $this;
84 }
85
86
87 /**
88 * Apply edge additions and removals queued by @{method:addEdge} and
89 * @{method:removeEdge}. Note that transactions are opened, all additions and
90 * removals are executed, and then transactions are saved. Thus, in some cases
91 * it may be slightly more efficient to perform multiple edit operations
92 * (e.g., adds followed by removals) if their outcomes are not dependent,
93 * since transactions will not be held open as long.
94 *
95 * @task edit
96 */
97 public function save() {
98
99 $cycle_types = $this->getPreventCyclesEdgeTypes();
100
101 $locks = array();
102 $caught = null;
103 try {
104
105 // NOTE: We write edge data first, before doing any transactions, since
106 // it's OK if we just leave it hanging out in space unattached to
107 // anything.
108 $this->writeEdgeData();
109
110 // If we're going to perform cycle detection, lock the edge type before
111 // doing edits.
112 if ($cycle_types) {
113 $src_phids = ipull($this->addEdges, 'src');
114 foreach ($cycle_types as $cycle_type) {
115 $key = 'edge.cycle:'.$cycle_type;
116 $locks[] = PhabricatorGlobalLock::newLock($key)->lock(15);
117 }
118 }
119
120 static $id = 0;
121 $id++;
122
123 // NOTE: Removes first, then adds, so that "remove + add" is a useful
124 // operation meaning "overwrite".
125
126 $this->executeRemoves();
127 $this->executeAdds();
128
129 foreach ($cycle_types as $cycle_type) {
130 $this->detectCycles($src_phids, $cycle_type);
131 }
132
133 $this->saveTransactions();
134 } catch (Exception $ex) {
135 $caught = $ex;
136 }
137
138 if ($caught) {
139 $this->killTransactions();
140 }
141
142 foreach ($locks as $lock) {
143 $lock->unlock();
144 }
145
146 if ($caught) {
147 throw $caught;
148 }
149 }
150
151
152/* -( Internals )---------------------------------------------------------- */
153
154
155 /**
156 * Build the specification for an edge operation, and possibly build its
157 * inverse as well.
158 *
159 * @task internal
160 */
161 private function buildEdgeSpecs($src, $type, $dst, array $options = array()) {
162 $data = array();
163 if (!empty($options['data'])) {
164 $data['data'] = $options['data'];
165 }
166
167 $src_type = phid_get_type($src);
168 $dst_type = phid_get_type($dst);
169
170 $specs = array();
171 $specs[] = array(
172 'src' => $src,
173 'src_type' => $src_type,
174 'dst' => $dst,
175 'dst_type' => $dst_type,
176 'type' => $type,
177 'data' => $data,
178 );
179
180 $type_obj = PhabricatorEdgeType::getByConstant($type);
181 $inverse = $type_obj->getInverseEdgeConstant();
182 if ($inverse !== null) {
183
184 // If `inverse_data` is set, overwrite the edge data. Normally, just
185 // write the same data to the inverse edge.
186 if (array_key_exists('inverse_data', $options)) {
187 $data['data'] = $options['inverse_data'];
188 }
189
190 $specs[] = array(
191 'src' => $dst,
192 'src_type' => $dst_type,
193 'dst' => $src,
194 'dst_type' => $src_type,
195 'type' => $inverse,
196 'data' => $data,
197 );
198 }
199
200 return $specs;
201 }
202
203
204 /**
205 * Write edge data.
206 *
207 * @task internal
208 */
209 private function writeEdgeData() {
210 $adds = $this->addEdges;
211
212 $writes = array();
213 foreach ($adds as $key => $edge) {
214 if ($edge['data']) {
215 $writes[] = array($key, $edge['src_type'], json_encode($edge['data']));
216 }
217 }
218
219 foreach ($writes as $write) {
220 list($key, $src_type, $data) = $write;
221 $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
222 queryfx(
223 $conn_w,
224 'INSERT INTO %T (data) VALUES (%s)',
225 PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
226 $data);
227 $this->addEdges[$key]['data_id'] = $conn_w->getInsertID();
228 }
229 }
230
231
232 /**
233 * Add queued edges.
234 *
235 * @task internal
236 */
237 private function executeAdds() {
238 $adds = $this->addEdges;
239 $adds = igroup($adds, 'src_type');
240
241 // Assign stable sequence numbers to each edge, so we have a consistent
242 // ordering across edges by source and type.
243 foreach ($adds as $src_type => $edges) {
244 $edges_by_src = igroup($edges, 'src');
245 foreach ($edges_by_src as $src => $src_edges) {
246 $seq = 0;
247 foreach ($src_edges as $key => $edge) {
248 $src_edges[$key]['seq'] = $seq++;
249 $src_edges[$key]['dateCreated'] = time();
250 }
251 $edges_by_src[$src] = $src_edges;
252 }
253 $adds[$src_type] = array_mergev($edges_by_src);
254 }
255
256 $inserts = array();
257 foreach ($adds as $src_type => $edges) {
258 $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
259 $sql = array();
260 foreach ($edges as $edge) {
261 $sql[] = qsprintf(
262 $conn_w,
263 '(%s, %d, %s, %d, %d, %nd)',
264 $edge['src'],
265 $edge['type'],
266 $edge['dst'],
267 $edge['dateCreated'],
268 $edge['seq'],
269 idx($edge, 'data_id'));
270 }
271 $inserts[] = array($conn_w, $sql);
272 }
273
274 foreach ($inserts as $insert) {
275 list($conn_w, $sql) = $insert;
276 $conn_w->openTransaction();
277 $this->openTransactions[] = $conn_w;
278
279 foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
280 queryfx(
281 $conn_w,
282 'INSERT INTO %T (src, type, dst, dateCreated, seq, dataID)
283 VALUES %LQ ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)',
284 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
285 $chunk);
286 }
287 }
288 }
289
290
291 /**
292 * Remove queued edges.
293 *
294 * @task internal
295 */
296 private function executeRemoves() {
297 $rems = $this->remEdges;
298 $rems = igroup($rems, 'src_type');
299
300 $deletes = array();
301 foreach ($rems as $src_type => $edges) {
302 $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
303 $sql = array();
304 foreach ($edges as $edge) {
305 $sql[] = qsprintf(
306 $conn_w,
307 '(src = %s AND type = %d AND dst = %s)',
308 $edge['src'],
309 $edge['type'],
310 $edge['dst']);
311 }
312 $deletes[] = array($conn_w, $sql);
313 }
314
315 foreach ($deletes as $delete) {
316 list($conn_w, $sql) = $delete;
317
318 $conn_w->openTransaction();
319 $this->openTransactions[] = $conn_w;
320
321 foreach (array_chunk($sql, 256) as $chunk) {
322 queryfx(
323 $conn_w,
324 'DELETE FROM %T WHERE %LO',
325 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
326 $chunk);
327 }
328 }
329 }
330
331
332 /**
333 * Save open transactions.
334 *
335 * @task internal
336 */
337 private function saveTransactions() {
338 foreach ($this->openTransactions as $key => $conn_w) {
339 $conn_w->saveTransaction();
340 unset($this->openTransactions[$key]);
341 }
342 }
343
344 private function killTransactions() {
345 foreach ($this->openTransactions as $key => $conn_w) {
346 $conn_w->killTransaction();
347 unset($this->openTransactions[$key]);
348 }
349 }
350
351
352/* -( Cycle Prevention )--------------------------------------------------- */
353
354
355 /**
356 * Get a list of all edge types which are being added, and which we should
357 * prevent cycles on.
358 *
359 * @return list<int|string> List of edge type constants which should have
360 * cycles prevented.
361 * @task cycle
362 */
363 private function getPreventCyclesEdgeTypes() {
364 $edge_types = array();
365 foreach ($this->addEdges as $edge) {
366 $edge_types[$edge['type']] = true;
367 }
368 foreach ($edge_types as $type => $ignored) {
369 $type_obj = PhabricatorEdgeType::getByConstant($type);
370 if (!$type_obj->shouldPreventCycles()) {
371 unset($edge_types[$type]);
372 }
373 }
374 return array_keys($edge_types);
375 }
376
377
378 /**
379 * Detect graph cycles of a given edge type. If the edit introduces a cycle,
380 * a @{class:PhabricatorEdgeCycleException} is thrown with details.
381 *
382 * @return void
383 * @task cycle
384 */
385 private function detectCycles(array $phids, $edge_type) {
386 // For simplicity, we just seed the graph with the affected nodes rather
387 // than seeding it with their edges. To do this, we just add synthetic
388 // edges from an imaginary '<seed>' node to the known edges.
389
390
391 $graph = id(new PhabricatorEdgeGraph())
392 ->setEdgeType($edge_type)
393 ->addNodes(
394 array(
395 '<seed>' => $phids,
396 ))
397 ->loadGraph();
398
399 foreach ($phids as $phid) {
400 $cycle = $graph->detectCycles($phid);
401 if ($cycle) {
402 throw new PhabricatorEdgeCycleException($edge_type, $cycle);
403 }
404 }
405 }
406
407
408}