Skip to content

Commit 877cb13

Browse files
author
epriestley
committed
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
1 parent bc61f36 commit 877cb13

File tree

12 files changed

+861
-2
lines changed

12 files changed

+861
-2
lines changed

resources/sql/patches/126.edges.sql

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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;

src/__phutil_library_map__.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,10 @@
552552
'PhabricatorDisabledUserController' => 'applications/auth/controller/disabled',
553553
'PhabricatorDraft' => 'applications/draft/storage/draft',
554554
'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',
555559
'PhabricatorEmailLoginController' => 'applications/auth/controller/email',
556560
'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken',
557561
'PhabricatorEnv' => 'infrastructure/env',
@@ -1403,6 +1407,8 @@
14031407
'PhabricatorDisabledUserController' => 'PhabricatorAuthController',
14041408
'PhabricatorDraft' => 'PhabricatorDraftDAO',
14051409
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
1410+
'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants',
1411+
'PhabricatorEdgeQuery' => 'PhabricatorQuery',
14061412
'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
14071413
'PhabricatorEmailTokenController' => 'PhabricatorAuthController',
14081414
'PhabricatorEnvTestCase' => 'PhabricatorTestCase',

src/applications/base/storage/lisk/PhabricatorLiskDAO.php

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright 2011 Facebook, Inc.
4+
* Copyright 2012 Facebook, Inc.
55
*
66
* Licensed under the Apache License, Version 2.0 (the "License");
77
* you may not use this file except in compliance with the License.
@@ -16,9 +16,55 @@
1616
* limitations under the License.
1717
*/
1818

19-
19+
/**
20+
* @task edges Managing Edges
21+
* @task config Configuring Storage
22+
*/
2023
abstract class PhabricatorLiskDAO extends LiskDAO {
2124

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+
*/
2268
public function establishLiveConnection($mode) {
2369
$conf_provider = PhabricatorEnv::getEnvConfig(
2470
'mysql.configuration_provider', 'DatabaseConfigurationProvider');
@@ -34,6 +80,9 @@ public function establishLiveConnection($mode) {
3480
));
3581
}
3682

83+
/**
84+
* @task config
85+
*/
3786
public function getTableName() {
3887
$str = 'phabricator';
3988
$len = strlen($str);
@@ -54,5 +103,8 @@ public function getTableName() {
54103
}
55104
}
56105

106+
/**
107+
* @task config
108+
*/
57109
abstract public function getApplicationName();
58110
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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');
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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');

0 commit comments

Comments
 (0)