@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add an assocations-like "Edges" framework

Summary:
We have a lot of cases where we store object relationships, but it's all kind of messy and custom. Some particular problems:

- We go to great lengths to enforce order stability in Differential revisions, but the implementation is complex and inelegant.
- Some relationships are stored on-object, so we can't pull the inverses easily. For example, Maniphest shows child tasks but not parent tasks.
- I want to add more of these and don't want to continue building custom stuff.
- UIs like the "attach stuff to other stuff" UI need custom branches for each object type.
- Stuff like "allow commits to close tasks" is notrivial because of nonstandard metadata storage.

Provide an association-like "edge" framework to fix these problems. This is nearly identical to associations, with a few differences:

- I put edge metadata in a separate table and don't load it by default, to keep edge rows small and allow large metadata if necessary. The on-edge metadata seemed to get abused a lot at Facebook.
- I put a 'seq' column on the edges to ensure they have an explicit, stable ordering within a source and type.

This isn't actually used anywhere yet, but my first target is attaching commits to tasks for T904.

Test Plan: Made a mock page that used Editor and Query. Verified adding and removing edges, overwriting edges, writing and loading edge data, sequence number generation.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran, 20after4

Differential Revision: https://secure.phabricator.com/D2088

+861 -2
+123
resources/sql/patches/126.edges.sql
··· 1 + CREATE TABLE phabricator_maniphest.edge ( 2 + src VARCHAR(64) NOT NULL COLLATE utf8_bin, 3 + type VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, 5 + dateCreated INT UNSIGNED NOT NULL, 6 + seq INT UNSIGNED NOT NULL, 7 + dataID INT UNSIGNED, 8 + PRIMARY KEY (src, type, dst), 9 + KEY (src, type, dateCreated, seq) 10 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 11 + 12 + CREATE TABLE phabricator_maniphest.edgedata ( 13 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 14 + data LONGTEXT NOT NULL COLLATE utf8_bin 15 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 16 + 17 + 18 + 19 + CREATE TABLE phabricator_repository.edge ( 20 + src VARCHAR(64) NOT NULL COLLATE utf8_bin, 21 + type VARCHAR(64) NOT NULL COLLATE utf8_bin, 22 + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, 23 + dateCreated INT UNSIGNED NOT NULL, 24 + seq INT UNSIGNED NOT NULL, 25 + dataID INT UNSIGNED, 26 + PRIMARY KEY (src, type, dst), 27 + KEY (src, type, dateCreated, seq) 28 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 29 + 30 + CREATE TABLE phabricator_repository.edgedata ( 31 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 32 + data LONGTEXT NOT NULL COLLATE utf8_bin 33 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 34 + 35 + 36 + 37 + CREATE TABLE phabricator_differential.edge ( 38 + src VARCHAR(64) NOT NULL COLLATE utf8_bin, 39 + type VARCHAR(64) NOT NULL COLLATE utf8_bin, 40 + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, 41 + dateCreated INT UNSIGNED NOT NULL, 42 + seq INT UNSIGNED NOT NULL, 43 + dataID INT UNSIGNED, 44 + PRIMARY KEY (src, type, dst), 45 + KEY (src, type, dateCreated, seq) 46 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 47 + 48 + CREATE TABLE phabricator_differential.edgedata ( 49 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 50 + data LONGTEXT NOT NULL COLLATE utf8_bin 51 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 52 + 53 + 54 + 55 + CREATE TABLE phabricator_file.edge ( 56 + src VARCHAR(64) NOT NULL COLLATE utf8_bin, 57 + type VARCHAR(64) NOT NULL COLLATE utf8_bin, 58 + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, 59 + dateCreated INT UNSIGNED NOT NULL, 60 + seq INT UNSIGNED NOT NULL, 61 + dataID INT UNSIGNED, 62 + PRIMARY KEY (src, type, dst), 63 + KEY (src, type, dateCreated, seq) 64 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 65 + 66 + CREATE TABLE phabricator_file.edgedata ( 67 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 68 + data LONGTEXT NOT NULL COLLATE utf8_bin 69 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 70 + 71 + 72 + 73 + CREATE TABLE phabricator_user.edge ( 74 + src VARCHAR(64) NOT NULL COLLATE utf8_bin, 75 + type VARCHAR(64) NOT NULL COLLATE utf8_bin, 76 + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, 77 + dateCreated INT UNSIGNED NOT NULL, 78 + seq INT UNSIGNED NOT NULL, 79 + dataID INT UNSIGNED, 80 + PRIMARY KEY (src, type, dst), 81 + KEY (src, type, dateCreated, seq) 82 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 83 + 84 + CREATE TABLE phabricator_user.edgedata ( 85 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 86 + data LONGTEXT NOT NULL COLLATE utf8_bin 87 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 88 + 89 + 90 + 91 + CREATE TABLE phabricator_project.edge ( 92 + src VARCHAR(64) NOT NULL COLLATE utf8_bin, 93 + type VARCHAR(64) NOT NULL COLLATE utf8_bin, 94 + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, 95 + dateCreated INT UNSIGNED NOT NULL, 96 + seq INT UNSIGNED NOT NULL, 97 + dataID INT UNSIGNED, 98 + PRIMARY KEY (src, type, dst), 99 + KEY (src, type, dateCreated, seq) 100 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 101 + 102 + CREATE TABLE phabricator_project.edgedata ( 103 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 104 + data LONGTEXT NOT NULL COLLATE utf8_bin 105 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 106 + 107 + 108 + 109 + CREATE TABLE phabricator_metamta.edge ( 110 + src VARCHAR(64) NOT NULL COLLATE utf8_bin, 111 + type VARCHAR(64) NOT NULL COLLATE utf8_bin, 112 + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, 113 + dateCreated INT UNSIGNED NOT NULL, 114 + seq INT UNSIGNED NOT NULL, 115 + dataID INT UNSIGNED, 116 + PRIMARY KEY (src, type, dst), 117 + KEY (src, type, dateCreated, seq) 118 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 119 + 120 + CREATE TABLE phabricator_metamta.edgedata ( 121 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 122 + data LONGTEXT NOT NULL COLLATE utf8_bin 123 + ) ENGINE=InnoDB, COLLATE utf8_general_ci;
+6
src/__phutil_library_map__.php
··· 552 552 'PhabricatorDisabledUserController' => 'applications/auth/controller/disabled', 553 553 'PhabricatorDraft' => 'applications/draft/storage/draft', 554 554 'PhabricatorDraftDAO' => 'applications/draft/storage/base', 555 + 'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/config', 556 + 'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/base', 557 + 'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/edge', 558 + 'PhabricatorEdgeQuery' => 'infrastructure/edges/query/edge', 555 559 'PhabricatorEmailLoginController' => 'applications/auth/controller/email', 556 560 'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken', 557 561 'PhabricatorEnv' => 'infrastructure/env', ··· 1403 1407 'PhabricatorDisabledUserController' => 'PhabricatorAuthController', 1404 1408 'PhabricatorDraft' => 'PhabricatorDraftDAO', 1405 1409 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 1410 + 'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants', 1411 + 'PhabricatorEdgeQuery' => 'PhabricatorQuery', 1406 1412 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', 1407 1413 'PhabricatorEmailTokenController' => 'PhabricatorAuthController', 1408 1414 'PhabricatorEnvTestCase' => 'PhabricatorTestCase',
+54 -2
src/applications/base/storage/lisk/PhabricatorLiskDAO.php
··· 1 1 <?php 2 2 3 3 /* 4 - * Copyright 2011 Facebook, Inc. 4 + * Copyright 2012 Facebook, Inc. 5 5 * 6 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 7 * you may not use this file except in compliance with the License. ··· 16 16 * limitations under the License. 17 17 */ 18 18 19 - 19 + /** 20 + * @task edges Managing Edges 21 + * @task config Configuring Storage 22 + */ 20 23 abstract class PhabricatorLiskDAO extends LiskDAO { 21 24 25 + private $edges = array(); 26 + 27 + 28 + /* -( Managing Edges )----------------------------------------------------- */ 29 + 30 + 31 + /** 32 + * @task edges 33 + */ 34 + public function attachEdges(array $edges) { 35 + foreach ($edges as $type => $type_edges) { 36 + $this->edges[$type] = $type_edges; 37 + } 38 + return $this; 39 + } 40 + 41 + 42 + /** 43 + * @task edges 44 + */ 45 + public function getEdges($type) { 46 + $edges = idx($this->edges, $type); 47 + if ($edges === null) { 48 + throw new Exception("Call attachEdges() before getEdges()!"); 49 + } 50 + return $edges; 51 + } 52 + 53 + 54 + /** 55 + * @task edges 56 + */ 57 + public function getEdgePHIDs($type) { 58 + return ipull($this->getEdges($type), 'dst'); 59 + } 60 + 61 + 62 + /* -( Configuring Storage )------------------------------------------------ */ 63 + 64 + 65 + /** 66 + * @task config 67 + */ 22 68 public function establishLiveConnection($mode) { 23 69 $conf_provider = PhabricatorEnv::getEnvConfig( 24 70 'mysql.configuration_provider', 'DatabaseConfigurationProvider'); ··· 34 80 )); 35 81 } 36 82 83 + /** 84 + * @task config 85 + */ 37 86 public function getTableName() { 38 87 $str = 'phabricator'; 39 88 $len = strlen($str); ··· 54 103 } 55 104 } 56 105 106 + /** 107 + * @task config 108 + */ 57 109 abstract public function getApplicationName(); 58 110 }
+31
src/docs/developer/using_edges.diviner
··· 1 + @title Using Edges 2 + @group developer 3 + 4 + Guide to the Edges infrastructure. 5 + 6 + = Overview = 7 + 8 + Edges are a generic way of storing a relationship between two objects (like 9 + a Task and its attached files). If you are familiar with the Facebook 10 + associations framework, Phabricator Edges are substantially similar. 11 + 12 + An edge is defined by a source PHID (the edge origin), a destination PHID 13 + (the edge destination) and an edge type (which describes the relationship, 14 + like "is subscribed to" or "has attached file"). 15 + 16 + Every edge is directional, and stored alongside the source object. Some edges 17 + are configured to automatically write an inverse edge, effectively building 18 + a bidirectional relationship. The strength of storing relationships like this 19 + is that they work when databases are partitioned or sharded. 20 + 21 + = Reading Edges = 22 + 23 + You can load edges with @{class:PhabricatorEdgeQuery}. 24 + 25 + = Writing Edges = 26 + 27 + You can edit edges with @{class:PhabricatorEdgeEditor}. 28 + 29 + = Edges and Lisk = 30 + 31 + @{class:PhabricatorLiskDAO} includes some builtin support for edges.
+21
src/infrastructure/edges/constants/base/PhabricatorEdgeConstants.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + abstract class PhabricatorEdgeConstants { 20 + 21 + }
+10
src/infrastructure/edges/constants/base/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + 10 + phutil_require_source('PhabricatorEdgeConstants.php');
+59
src/infrastructure/edges/constants/config/PhabricatorEdgeConfig.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { 20 + 21 + const TABLE_NAME_EDGE = 'edge'; 22 + const TABLE_NAME_EDGEDATA = 'edgedata'; 23 + 24 + const TYPE_TASK_HAS_COMMIT = 1; 25 + const TYPE_COMMIT_HAS_TASK = 2; 26 + 27 + public static function getInverse($edge_type) { 28 + static $map = array( 29 + self::TYPE_TASK_HAS_COMMIT => self::TYPE_COMMIT_HAS_TASK, 30 + self::TYPE_COMMIT_HAS_TASK => self::TYPE_TASK_HAS_COMMIT, 31 + ); 32 + 33 + return idx($map, $edge_type); 34 + } 35 + 36 + public static function establishConnection($phid_type, $conn_type) { 37 + static $class_map = array( 38 + PhabricatorPHIDConstants::PHID_TYPE_TASK => 'ManiphestTask', 39 + PhabricatorPHIDConstants::PHID_TYPE_CMIT => 'PhabricatorRepository', 40 + PhabricatorPHIDConstants::PHID_TYPE_DREV => 'DifferentialRevision', 41 + PhabricatorPHIDConstants::PHID_TYPE_FILE => 'PhabricatorFile', 42 + PhabricatorPHIDConstants::PHID_TYPE_USER => 'PhabricatorUser', 43 + PhabricatorPHIDConstants::PHID_TYPE_PROJ => 'PhabricatorProject', 44 + PhabricatorPHIDConstants::PHID_TYPE_MLST => 45 + 'PhabricatorMetaMTAMailingList', 46 + ); 47 + 48 + $class = idx($class_map, $phid_type); 49 + 50 + if (!$class) { 51 + throw new Exception( 52 + "Edges are not available for objects of type '{$phid_type}'!"); 53 + } 54 + 55 + return newv($class, array())->establishConnection($conn_type); 56 + } 57 + 58 + 59 + }
+15
src/infrastructure/edges/constants/config/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/phid/constants'); 10 + phutil_require_module('phabricator', 'infrastructure/edges/constants/base'); 11 + 12 + phutil_require_module('phutil', 'utils'); 13 + 14 + 15 + phutil_require_source('PhabricatorEdgeConfig.php');
+310
src/infrastructure/edges/editor/edge/PhabricatorEdgeEditor.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + /** 20 + * Add and remove edges between objects. You can use 21 + * @{class:PhabricatorEdgeQuery} to load object edges. For more information 22 + * on edges, see @{article:Using Edges}. 23 + * 24 + * name=Adding Edges 25 + * $src = $earth_phid; 26 + * $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE; 27 + * $dst = $moon_phid; 28 + * 29 + * id(new PhabricatorEdgeEditor()) 30 + * ->addEdge($src, $type, $dst) 31 + * ->save(); 32 + * 33 + * @task edit Editing Edges 34 + * @task internal Internals 35 + */ 36 + final class PhabricatorEdgeEditor { 37 + 38 + private $addEdges = array(); 39 + private $remEdges = array(); 40 + private $openTransactions = array(); 41 + 42 + 43 + /* -( Editing Edges )------------------------------------------------------ */ 44 + 45 + 46 + /** 47 + * Add a new edge (possibly also adding its inverse). Changes take effect when 48 + * you call @{method:save()}. If the edge already exists, it will not be 49 + * overwritten. Removals queued with @{method:removeEdge} are executed before 50 + * adds, so the effect of removing and adding the same edge is to overwrite 51 + * any existing edge. 52 + * 53 + * The `$options` parameter accepts these values: 54 + * 55 + * - `data` Optional, data to write onto the edge. 56 + * - `inverse_data` Optional, data to write on the inverse edge. If not 57 + * provided, `data` will be written. 58 + * 59 + * @param phid Source object PHID. 60 + * @param const Edge type constant. 61 + * @param phid Destination object PHID. 62 + * @param map Options map (see documentation). 63 + * @return this 64 + * 65 + * @task edit 66 + */ 67 + public function addEdge($src, $type, $dst, array $options = array()) { 68 + foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) { 69 + $this->addEdges[] = $spec; 70 + } 71 + return $this; 72 + } 73 + 74 + 75 + /** 76 + * Remove an edge (possibly also removing its inverse). Changes take effect 77 + * when you call @{method:save()}. If an edge does not exist, the removal 78 + * will be ignored. Edges are added after edges are removed, so the effect of 79 + * a remove plus an add is to overwrite. 80 + * 81 + * @param phid Source object PHID. 82 + * @param const Edge type constant. 83 + * @param phid Destination object PHID. 84 + * @return this 85 + * 86 + * @task edit 87 + */ 88 + public function removeEdge($src, $type, $dst) { 89 + foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) { 90 + $this->remEdges[] = $spec; 91 + } 92 + return $this; 93 + } 94 + 95 + 96 + /** 97 + * Apply edge additions and removals queued by @{method:addEdge} and 98 + * @{method:removeEdge}. Note that transactions are opened, all additions and 99 + * removals are executed, and then transactions are saved. Thus, in some cases 100 + * it may be slightly more efficient to perform multiple edit operations 101 + * (e.g., adds followed by removals) if their outcomes are not dependent, 102 + * since transactions will not be held open as long. 103 + * 104 + * @return this 105 + * @task edit 106 + */ 107 + public function save() { 108 + 109 + // NOTE: We write edge data first, before doing any transactions, since 110 + // it's OK if we just leave it hanging out in space unattached to anything. 111 + 112 + $this->writeEdgeData(); 113 + 114 + // NOTE: Removes first, then adds, so that "remove + add" is a useful 115 + // operation meaning "overwrite". 116 + 117 + $this->executeRemoves(); 118 + $this->executeAdds(); 119 + 120 + $this->saveTransactions(); 121 + } 122 + 123 + 124 + /* -( Internals )---------------------------------------------------------- */ 125 + 126 + 127 + /** 128 + * Build the specification for an edge operation, and possibly build its 129 + * inverse as well. 130 + * 131 + * @task internal 132 + */ 133 + private function buildEdgeSpecs($src, $type, $dst, array $options = array()) { 134 + $data = array(); 135 + if (!empty($options['data'])) { 136 + $data['data'] = $options['data']; 137 + } 138 + 139 + $specs = array(); 140 + $specs[] = array( 141 + 'src' => $src, 142 + 'src_type' => phid_get_type($src), 143 + 'dst' => $dst, 144 + 'type' => $type, 145 + 'data' => $data, 146 + ); 147 + 148 + $inverse = PhabricatorEdgeConfig::getInverse($type); 149 + if ($inverse) { 150 + 151 + // If `inverse_data` is set, overwrite the edge data. Normally, just 152 + // write the same data to the inverse edge. 153 + if (array_key_exists('inverse_data', $options)) { 154 + $data['data'] = $options['inverse_data']; 155 + } 156 + 157 + $specs[] = array( 158 + 'src' => $dst, 159 + 'src_type' => phid_get_type($dst), 160 + 'dst' => $src, 161 + 'type' => $inverse, 162 + 'data' => $data, 163 + ); 164 + } 165 + 166 + return $specs; 167 + } 168 + 169 + 170 + /** 171 + * Write edge data. 172 + * 173 + * @task internal 174 + */ 175 + private function writeEdgeData() { 176 + $adds = $this->addEdges; 177 + 178 + $writes = array(); 179 + foreach ($adds as $key => $edge) { 180 + if ($edge['data']) { 181 + $writes[] = array($key, $edge['src_type'], json_encode($edge['data'])); 182 + } 183 + } 184 + 185 + foreach ($writes as $write) { 186 + list($key, $src_type, $data) = $write; 187 + $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); 188 + queryfx( 189 + $conn_w, 190 + 'INSERT INTO %T (data) VALUES (%s)', 191 + PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA, 192 + $data); 193 + $this->addEdges[$key]['data_id'] = $conn_w->getInsertID(); 194 + } 195 + } 196 + 197 + 198 + /** 199 + * Add queued edges. 200 + * 201 + * @task internal 202 + */ 203 + private function executeAdds() { 204 + $adds = $this->addEdges; 205 + $adds = igroup($adds, 'src_type'); 206 + 207 + // Assign stable sequence numbers to each edge, so we have a consistent 208 + // ordering across edges by source and type. 209 + foreach ($adds as $src_type => $edges) { 210 + $edges_by_src = igroup($edges, 'src'); 211 + foreach ($edges_by_src as $src => $src_edges) { 212 + $seq = 0; 213 + foreach ($src_edges as $key => $edge) { 214 + $src_edges[$key]['seq'] = $seq++; 215 + $src_edges[$key]['dateCreated'] = time(); 216 + } 217 + $edges_by_src[$src] = $src_edges; 218 + } 219 + $adds[$src_type] = array_mergev($edges_by_src); 220 + } 221 + 222 + $inserts = array(); 223 + foreach ($adds as $src_type => $edges) { 224 + $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); 225 + $sql = array(); 226 + foreach ($edges as $edge) { 227 + $sql[] = qsprintf( 228 + $conn_w, 229 + '(%s, %d, %s, %d, %d, %nd)', 230 + $edge['src'], 231 + $edge['type'], 232 + $edge['dst'], 233 + $edge['dateCreated'], 234 + $edge['seq'], 235 + idx($edge, 'data_id')); 236 + } 237 + $inserts[] = array($conn_w, $sql); 238 + } 239 + 240 + foreach ($inserts as $insert) { 241 + list($conn_w, $sql) = $insert; 242 + $conn_w->openTransaction(); 243 + $this->openTransactions[] = $conn_w; 244 + 245 + foreach (array_chunk($sql, 256) as $chunk) { 246 + queryfx( 247 + $conn_w, 248 + 'INSERT IGNORE INTO %T (src, type, dst, dateCreated, seq, dataID) 249 + VALUES %Q', 250 + PhabricatorEdgeConfig::TABLE_NAME_EDGE, 251 + implode(', ', $chunk)); 252 + } 253 + } 254 + } 255 + 256 + 257 + /** 258 + * Remove queued edges. 259 + * 260 + * @task internal 261 + */ 262 + private function executeRemoves() { 263 + $rems = $this->remEdges; 264 + $rems = igroup($rems, 'src_type'); 265 + 266 + $deletes = array(); 267 + foreach ($rems as $src_type => $edges) { 268 + $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); 269 + $sql = array(); 270 + foreach ($edges as $edge) { 271 + $sql[] = qsprintf( 272 + $conn_w, 273 + '(%s, %d, %s)', 274 + $edge['src'], 275 + $edge['type'], 276 + $edge['dst']); 277 + } 278 + $deletes[] = array($conn_w, $sql); 279 + } 280 + 281 + foreach ($deletes as $delete) { 282 + list($conn_w, $sql) = $delete; 283 + 284 + $conn_w->openTransaction(); 285 + $this->openTransactions[] = $conn_w; 286 + 287 + foreach (array_chunk($sql, 256) as $chunk) { 288 + queryfx( 289 + $conn_w, 290 + 'DELETE FROM %T WHERE (src, type, dst) IN (%Q)', 291 + PhabricatorEdgeConfig::TABLE_NAME_EDGE, 292 + implode(', ', $chunk)); 293 + } 294 + } 295 + } 296 + 297 + 298 + /** 299 + * Save open transactions. 300 + * 301 + * @task internal 302 + */ 303 + private function saveTransactions() { 304 + foreach ($this->openTransactions as $key => $conn_w) { 305 + $conn_w->saveTransaction(); 306 + unset($this->openTransactions[$key]); 307 + } 308 + } 309 + 310 + }
+17
src/infrastructure/edges/editor/edge/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/phid/utils'); 10 + phutil_require_module('phabricator', 'infrastructure/edges/constants/config'); 11 + phutil_require_module('phabricator', 'storage/qsprintf'); 12 + phutil_require_module('phabricator', 'storage/queryfx'); 13 + 14 + phutil_require_module('phutil', 'utils'); 15 + 16 + 17 + phutil_require_source('PhabricatorEdgeEditor.php');
+197
src/infrastructure/edges/query/edge/PhabricatorEdgeQuery.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + /** 20 + * Load object edges created by @{class:PhabricatorEdgeEditor}. 21 + * 22 + * name=Querying Edges 23 + * $src = $earth_phid; 24 + * $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE; 25 + * 26 + * // Load the earth's satellites. 27 + * $satellite_edges = id(new PhabricatorEdgeQuery()) 28 + * ->withSourcePHIDs(array($src)) 29 + * ->withEdgeTypes(array($type)) 30 + * ->execute(); 31 + * 32 + * For more information on edges, see @{article:Using Edges}. 33 + * 34 + * @task config Configuring the Query 35 + * @task exec Executing the Query 36 + * @task internal Internal 37 + */ 38 + final class PhabricatorEdgeQuery extends PhabricatorQuery { 39 + 40 + private $sourcePHIDs; 41 + private $edgeTypes; 42 + 43 + private $needEdgeData; 44 + 45 + 46 + /* -( Configuring the Query )---------------------------------------------- */ 47 + 48 + 49 + /** 50 + * Find edges originating at one or more source PHIDs. You MUST provide this 51 + * to execute an edge query. 52 + * 53 + * @param list List of source PHIDs. 54 + * @return this 55 + * 56 + * @task config 57 + */ 58 + public function withSourcePHIDs(array $source_phids) { 59 + $this->sourcePHIDs = $source_phids; 60 + return $this; 61 + } 62 + 63 + 64 + /** 65 + * Find edges of specific types. 66 + * 67 + * @param list List of PhabricatorEdgeConfig type constants. 68 + * @return this 69 + * 70 + * @task config 71 + */ 72 + public function withEdgeTypes(array $types) { 73 + $this->edgeTypes = $types; 74 + return $this; 75 + } 76 + 77 + 78 + /** 79 + * When loading edges, also load edge data. 80 + * 81 + * @param bool True to load edge data. 82 + * @return this 83 + * 84 + * @task config 85 + */ 86 + public function needEdgeData($need) { 87 + $this->needEdgeData = $need; 88 + return $this; 89 + } 90 + 91 + 92 + /* -( Executing the Query )------------------------------------------------ */ 93 + 94 + 95 + /** 96 + * Load specified edges. 97 + * 98 + * @task exec 99 + */ 100 + public function execute() { 101 + if (!$this->sourcePHIDs) { 102 + throw new Exception( 103 + "You must use withSourcePHIDs() to query edges."); 104 + } 105 + 106 + $sources = phid_group_by_type($this->sourcePHIDs); 107 + 108 + $result = array(); 109 + 110 + // When a query specifies types, make sure we return data for all queried 111 + // types. This is mostly to make sure PhabricatorLiskDAO->attachEdges() 112 + // gets some data, so that getEdges() doesn't throw later. 113 + if ($this->edgeTypes) { 114 + foreach ($this->sourcePHIDs as $phid) { 115 + foreach ($this->edgeTypes as $type) { 116 + $result[$phid][$type] = array(); 117 + } 118 + } 119 + } 120 + 121 + foreach ($sources as $type => $phids) { 122 + $conn_r = PhabricatorEdgeConfig::establishConnection($type, 'r'); 123 + 124 + $where = $this->buildWhereClause($conn_r); 125 + $order = $this->buildOrderClause($conn_r); 126 + 127 + $edges = queryfx_all( 128 + $conn_r, 129 + 'SELECT edge.* FROM %T edge %Q %Q', 130 + PhabricatorEdgeConfig::TABLE_NAME_EDGE, 131 + $where, 132 + $order); 133 + 134 + if ($this->needEdgeData) { 135 + $data_ids = array_filter(ipull($edges, 'dataID')); 136 + $data_map = array(); 137 + if ($data_ids) { 138 + $data_rows = queryfx_all( 139 + $conn_r, 140 + 'SELECT edgedata.* FROM %T edgedata WHERE id IN (%Ld)', 141 + PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA, 142 + $data_ids); 143 + foreach ($data_rows as $row) { 144 + $data_map[$row['id']] = idx( 145 + json_decode($row['data'], true), 146 + 'data'); 147 + } 148 + } 149 + foreach ($edges as $key => $edge) { 150 + $edges[$key]['data'] = idx($data_map, $edge['dataID']); 151 + } 152 + } 153 + 154 + foreach ($edges as $edge) { 155 + $result[$edge['src']][$edge['type']][$edge['dst']] = $edge; 156 + } 157 + } 158 + 159 + return $result; 160 + } 161 + 162 + 163 + /* -( Internals )---------------------------------------------------------- */ 164 + 165 + 166 + /** 167 + * @task internal 168 + */ 169 + private function buildWhereClause($conn_r) { 170 + $where = array(); 171 + 172 + if ($this->sourcePHIDs) { 173 + $where[] = qsprintf( 174 + $conn_r, 175 + 'edge.src IN (%Ls)', 176 + $this->sourcePHIDs); 177 + } 178 + 179 + if ($this->edgeTypes) { 180 + $where[] = qsprintf( 181 + $conn_r, 182 + 'edge.type IN (%Ls)', 183 + $this->edgeTypes); 184 + } 185 + 186 + return $this->formatWhereClause($where); 187 + } 188 + 189 + 190 + /** 191 + * @task internal 192 + */ 193 + private function buildOrderClause($conn_r) { 194 + return 'ORDER BY edge.dateCreated DESC, edge.seq ASC'; 195 + } 196 + 197 + }
+18
src/infrastructure/edges/query/edge/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/phid/utils'); 10 + phutil_require_module('phabricator', 'infrastructure/edges/constants/config'); 11 + phutil_require_module('phabricator', 'infrastructure/query/base'); 12 + phutil_require_module('phabricator', 'storage/qsprintf'); 13 + phutil_require_module('phabricator', 'storage/queryfx'); 14 + 15 + phutil_require_module('phutil', 'utils'); 16 + 17 + 18 + phutil_require_source('PhabricatorEdgeQuery.php');