Guarantee terms in PhabricatorAuthPasswordEngine are strings
[phabricator/blender.git] / src / infrastructure / edges / editor / PhabricatorEdgeEditor.php
blob588b5267b4b90202dc2161b838ba9e2c4d7cd450
1 <?php
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}.
8 * Edges are not directly policy aware, and this editor makes low-level changes
9 * below the policy layer.
11 * name=Adding Edges
12 * $src = $earth_phid;
13 * $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
14 * $dst = $moon_phid;
16 * id(new PhabricatorEdgeEditor())
17 * ->addEdge($src, $type, $dst)
18 * ->save();
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 )------------------------------------------------------ */
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.
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).
52 * @return this
54 * @task edit
56 public function addEdge($src, $type, $dst, array $options = array()) {
57 foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) {
58 $this->addEdges[] = $spec;
60 return $this;
64 /**
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.
73 * @return this
75 * @task edit
77 public function removeEdge($src, $type, $dst) {
78 foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) {
79 $this->remEdges[] = $spec;
81 return $this;
85 /**
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.
93 * @return this
94 * @task edit
96 public function save() {
98 $cycle_types = $this->getPreventCyclesEdgeTypes();
100 $locks = array();
101 $caught = null;
102 try {
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
106 // anything.
107 $this->writeEdgeData();
109 // If we're going to perform cycle detection, lock the edge type before
110 // doing edits.
111 if ($cycle_types) {
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);
119 static $id = 0;
120 $id++;
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) {
134 $caught = $ex;
137 if ($caught) {
138 $this->killTransactions();
141 foreach ($locks as $lock) {
142 $lock->unlock();
145 if ($caught) {
146 throw $caught;
151 /* -( Internals )---------------------------------------------------------- */
155 * Build the specification for an edge operation, and possibly build its
156 * inverse as well.
158 * @task internal
160 private function buildEdgeSpecs($src, $type, $dst, array $options = array()) {
161 $data = 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);
169 $specs = array();
170 $specs[] = array(
171 'src' => $src,
172 'src_type' => $src_type,
173 'dst' => $dst,
174 'dst_type' => $dst_type,
175 'type' => $type,
176 'data' => $data,
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'];
189 $specs[] = array(
190 'src' => $dst,
191 'src_type' => $dst_type,
192 'dst' => $src,
193 'dst_type' => $src_type,
194 'type' => $inverse,
195 'data' => $data,
199 return $specs;
204 * Write edge data.
206 * @task internal
208 private function writeEdgeData() {
209 $adds = $this->addEdges;
211 $writes = array();
212 foreach ($adds as $key => $edge) {
213 if ($edge['data']) {
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');
221 queryfx(
222 $conn_w,
223 'INSERT INTO %T (data) VALUES (%s)',
224 PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
225 $data);
226 $this->addEdges[$key]['data_id'] = $conn_w->getInsertID();
232 * Add queued edges.
234 * @task internal
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) {
245 $seq = 0;
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);
255 $inserts = array();
256 foreach ($adds as $src_type => $edges) {
257 $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
258 $sql = array();
259 foreach ($edges as $edge) {
260 $sql[] = qsprintf(
261 $conn_w,
262 '(%s, %d, %s, %d, %d, %nd)',
263 $edge['src'],
264 $edge['type'],
265 $edge['dst'],
266 $edge['dateCreated'],
267 $edge['seq'],
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) {
279 queryfx(
280 $conn_w,
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,
284 $chunk);
291 * Remove queued edges.
293 * @task internal
295 private function executeRemoves() {
296 $rems = $this->remEdges;
297 $rems = igroup($rems, 'src_type');
299 $deletes = array();
300 foreach ($rems as $src_type => $edges) {
301 $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
302 $sql = array();
303 foreach ($edges as $edge) {
304 $sql[] = qsprintf(
305 $conn_w,
306 '(src = %s AND type = %d AND dst = %s)',
307 $edge['src'],
308 $edge['type'],
309 $edge['dst']);
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) {
321 queryfx(
322 $conn_w,
323 'DELETE FROM %T WHERE %LO',
324 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
325 $chunk);
332 * Save open transactions.
334 * @task internal
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
356 * prevent cycles on.
358 * @return list<const> List of edge types which should have cycles prevented.
359 * @task cycle
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.
380 * @return void
381 * @task cycle
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)
391 ->addNodes(
392 array(
393 '<seed>' => $phids,
395 ->loadGraph();
397 foreach ($phids as $phid) {
398 $cycle = $graph->detectCycles($phid);
399 if ($cycle) {
400 throw new PhabricatorEdgeCycleException($edge_type, $cycle);