@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
3final class PhabricatorConfigSchemaQuery extends Phobject {
4
5 private $refs;
6 private $apis;
7
8 public function setRefs(array $refs) {
9 $this->refs = $refs;
10 return $this;
11 }
12
13 public function getRefs() {
14 if (!$this->refs) {
15 return PhabricatorDatabaseRef::getMasterDatabaseRefs();
16 }
17 return $this->refs;
18 }
19
20 public function setAPIs(array $apis) {
21 $map = array();
22 foreach ($apis as $api) {
23 $map[$api->getRef()->getRefKey()] = $api;
24 }
25 $this->apis = $map;
26 return $this;
27 }
28
29 private function getDatabaseNames(PhabricatorDatabaseRef $ref) {
30 $api = $this->getAPI($ref);
31 $patches = PhabricatorSQLPatchList::buildAllPatches();
32 return $api->getDatabaseList(
33 $patches,
34 $only_living = true);
35 }
36
37 private function getAPI(PhabricatorDatabaseRef $ref) {
38 $key = $ref->getRefKey();
39
40 if (isset($this->apis[$key])) {
41 return $this->apis[$key];
42 }
43
44 return id(new PhabricatorStorageManagementAPI())
45 ->setUser($ref->getUser())
46 ->setHost($ref->getHost())
47 ->setPort($ref->getPort())
48 ->setNamespace(PhabricatorLiskDAO::getDefaultStorageNamespace())
49 ->setPassword($ref->getPass());
50 }
51
52 public function loadActualSchemata() {
53 $refs = $this->getRefs();
54
55 $schemata = array();
56 foreach ($refs as $ref) {
57 $schema = $this->loadActualSchemaForServer($ref);
58 $schemata[$schema->getRef()->getRefKey()] = $schema;
59 }
60
61 return $schemata;
62 }
63
64 private function loadActualSchemaForServer(PhabricatorDatabaseRef $ref) {
65 $databases = $this->getDatabaseNames($ref);
66
67 $conn = $ref->newManagementConnection();
68
69 $tables = queryfx_all(
70 $conn,
71 'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION, ENGINE
72 FROM INFORMATION_SCHEMA.TABLES
73 WHERE TABLE_SCHEMA IN (%Ls)',
74 $databases);
75
76 $database_info = queryfx_all(
77 $conn,
78 'SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
79 FROM INFORMATION_SCHEMA.SCHEMATA
80 WHERE SCHEMA_NAME IN (%Ls)',
81 $databases);
82 $database_info = ipull($database_info, null, 'SCHEMA_NAME');
83
84 // Find databases which exist, but which the user does not have permission
85 // to see.
86 $invisible_databases = array();
87 foreach ($databases as $database_name) {
88 if (isset($database_info[$database_name])) {
89 continue;
90 }
91
92 try {
93 queryfx($conn, 'SHOW TABLES IN %T', $database_name);
94 } catch (AphrontAccessDeniedQueryException $ex) {
95 // This database exists, the user just doesn't have permission to
96 // see it.
97 $invisible_databases[] = $database_name;
98 } catch (AphrontSchemaQueryException $ex) {
99 // This database is legitimately missing.
100 }
101 }
102
103 $sql = array();
104 foreach ($tables as $table) {
105 $sql[] = qsprintf(
106 $conn,
107 '(TABLE_SCHEMA = %s AND TABLE_NAME = %s)',
108 $table['TABLE_SCHEMA'],
109 $table['TABLE_NAME']);
110 }
111
112 if ($sql) {
113 $column_info = queryfx_all(
114 $conn,
115 'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME,
116 COLLATION_NAME, COLUMN_TYPE, IS_NULLABLE, EXTRA
117 FROM INFORMATION_SCHEMA.COLUMNS
118 WHERE %LO',
119 $sql);
120 $column_info = igroup($column_info, 'TABLE_SCHEMA');
121 } else {
122 $column_info = array();
123 }
124
125 // NOTE: Tables like KEY_COLUMN_USAGE and TABLE_CONSTRAINTS only contain
126 // primary, unique, and foreign keys, so we can't use them here. We pull
127 // indexes later on using SHOW INDEXES.
128
129 $server_schema = id(new PhabricatorConfigServerSchema())
130 ->setRef($ref);
131
132 $tables = igroup($tables, 'TABLE_SCHEMA');
133 foreach ($tables as $database_name => $database_tables) {
134 $info = $database_info[$database_name];
135
136 $database_schema = id(new PhabricatorConfigDatabaseSchema())
137 ->setName($database_name)
138 ->setCharacterSet($info['DEFAULT_CHARACTER_SET_NAME'])
139 ->setCollation($info['DEFAULT_COLLATION_NAME']);
140
141 $database_column_info = idx($column_info, $database_name, array());
142 $database_column_info = igroup($database_column_info, 'TABLE_NAME');
143
144 foreach ($database_tables as $table) {
145 $table_name = $table['TABLE_NAME'];
146
147 $table_schema = id(new PhabricatorConfigTableSchema())
148 ->setName($table_name)
149 ->setCollation($table['TABLE_COLLATION'])
150 ->setEngine($table['ENGINE']);
151
152 $columns = idx($database_column_info, $table_name, array());
153 foreach ($columns as $column) {
154 if (strpos($column['EXTRA'], 'auto_increment') === false) {
155 $auto_increment = false;
156 } else {
157 $auto_increment = true;
158 }
159
160 $column_schema = id(new PhabricatorConfigColumnSchema())
161 ->setName($column['COLUMN_NAME'])
162 ->setCharacterSet($column['CHARACTER_SET_NAME'])
163 ->setCollation($column['COLLATION_NAME'])
164 ->setColumnType($column['COLUMN_TYPE'])
165 ->setNullable($column['IS_NULLABLE'] == 'YES')
166 ->setAutoIncrement($auto_increment);
167
168 $table_schema->addColumn($column_schema);
169 }
170
171 $key_parts = queryfx_all(
172 $conn,
173 'SHOW INDEXES FROM %T.%T',
174 $database_name,
175 $table_name);
176 $keys = igroup($key_parts, 'Key_name');
177 foreach ($keys as $key_name => $key_pieces) {
178 $key_pieces = isort($key_pieces, 'Seq_in_index');
179 $head = head($key_pieces);
180
181 // This handles string indexes which index only a prefix of a field.
182 $column_names = array();
183 foreach ($key_pieces as $piece) {
184 $name = $piece['Column_name'];
185 if ($piece['Sub_part']) {
186 $name = $name.'('.$piece['Sub_part'].')';
187 }
188 $column_names[] = $name;
189 }
190
191 $key_schema = id(new PhabricatorConfigKeySchema())
192 ->setName($key_name)
193 ->setColumnNames($column_names)
194 ->setUnique(!$head['Non_unique'])
195 ->setIndexType($head['Index_type']);
196
197 $table_schema->addKey($key_schema);
198 }
199
200 $database_schema->addTable($table_schema);
201 }
202
203 $server_schema->addDatabase($database_schema);
204 }
205
206 foreach ($invisible_databases as $database_name) {
207 $server_schema->addDatabase(
208 id(new PhabricatorConfigDatabaseSchema())
209 ->setName($database_name)
210 ->setAccessDenied(true));
211 }
212
213 return $server_schema;
214 }
215
216 public function loadExpectedSchemata() {
217 $refs = $this->getRefs();
218
219 $schemata = array();
220 foreach ($refs as $ref) {
221 $schema = $this->loadExpectedSchemaForServer($ref);
222 $schemata[$schema->getRef()->getRefKey()] = $schema;
223 }
224
225 return $schemata;
226 }
227
228 public function loadExpectedSchemaForServer(PhabricatorDatabaseRef $ref) {
229 $databases = $this->getDatabaseNames($ref);
230 $info = $this->getAPI($ref)->getCharsetInfo();
231
232 $specs = id(new PhutilClassMapQuery())
233 ->setAncestorClass(PhabricatorConfigSchemaSpec::class)
234 ->execute();
235
236 $server_schema = id(new PhabricatorConfigServerSchema())
237 ->setRef($ref);
238
239 foreach ($specs as $spec) {
240 $spec
241 ->setUTF8Charset(
242 $info[PhabricatorStorageManagementAPI::CHARSET_DEFAULT])
243 ->setUTF8BinaryCollation(
244 $info[PhabricatorStorageManagementAPI::COLLATE_TEXT])
245 ->setUTF8SortingCollation(
246 $info[PhabricatorStorageManagementAPI::COLLATE_SORT])
247 ->setServer($server_schema)
248 ->buildSchemata($server_schema);
249 }
250
251 return $server_schema;
252 }
253
254 public function buildComparisonSchemata(
255 array $expect_servers,
256 array $actual_servers) {
257
258 $schemata = array();
259 foreach ($actual_servers as $key => $actual_server) {
260 $schemata[$key] = $this->buildComparisonSchemaForServer(
261 $expect_servers[$key],
262 $actual_server);
263 }
264
265 return $schemata;
266 }
267
268 private function buildComparisonSchemaForServer(
269 PhabricatorConfigServerSchema $expect,
270 PhabricatorConfigServerSchema $actual) {
271
272 $comp_server = $actual->newEmptyClone();
273
274 $all_databases = $actual->getDatabases() + $expect->getDatabases();
275 foreach ($all_databases as $database_name => $database_template) {
276 $actual_database = $actual->getDatabase($database_name);
277 $expect_database = $expect->getDatabase($database_name);
278
279 $issues = $this->compareSchemata($expect_database, $actual_database);
280
281 $comp_database = $database_template->newEmptyClone()
282 ->setIssues($issues);
283
284 if (!$actual_database) {
285 $actual_database = $expect_database->newEmptyClone();
286 }
287
288 if (!$expect_database) {
289 $expect_database = $actual_database->newEmptyClone();
290 }
291
292 $all_tables =
293 $actual_database->getTables() +
294 $expect_database->getTables();
295 foreach ($all_tables as $table_name => $table_template) {
296 $actual_table = $actual_database->getTable($table_name);
297 $expect_table = $expect_database->getTable($table_name);
298
299 $issues = $this->compareSchemata($expect_table, $actual_table);
300
301 $comp_table = $table_template->newEmptyClone()
302 ->setIssues($issues);
303
304 if (!$actual_table) {
305 $actual_table = $expect_table->newEmptyClone();
306 }
307 if (!$expect_table) {
308 $expect_table = $actual_table->newEmptyClone();
309 }
310
311 $all_columns =
312 $actual_table->getColumns() +
313 $expect_table->getColumns();
314 foreach ($all_columns as $column_name => $column_template) {
315 $actual_column = $actual_table->getColumn($column_name);
316 $expect_column = $expect_table->getColumn($column_name);
317
318 $issues = $this->compareSchemata($expect_column, $actual_column);
319
320 $comp_column = $column_template->newEmptyClone()
321 ->setIssues($issues);
322
323 $comp_table->addColumn($comp_column);
324 }
325
326 $all_keys =
327 $actual_table->getKeys() +
328 $expect_table->getKeys();
329 foreach ($all_keys as $key_name => $key_template) {
330 $actual_key = $actual_table->getKey($key_name);
331 $expect_key = $expect_table->getKey($key_name);
332
333 $issues = $this->compareSchemata($expect_key, $actual_key);
334
335 $comp_key = $key_template->newEmptyClone()
336 ->setIssues($issues);
337
338 $comp_table->addKey($comp_key);
339 }
340
341 $comp_table->setPersistenceType($expect_table->getPersistenceType());
342
343 $comp_database->addTable($comp_table);
344 }
345 $comp_server->addDatabase($comp_database);
346 }
347
348 return $comp_server;
349 }
350
351 private function compareSchemata(
352 ?PhabricatorConfigStorageSchema $expect = null,
353 ?PhabricatorConfigStorageSchema $actual = null) {
354
355 $expect_is_key = ($expect instanceof PhabricatorConfigKeySchema);
356 $actual_is_key = ($actual instanceof PhabricatorConfigKeySchema);
357
358 if ($expect_is_key || $actual_is_key) {
359 $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY;
360 $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY;
361 } else {
362 $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSING;
363 $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUS;
364 }
365
366 if (!$expect && !$actual) {
367 throw new Exception(pht('Can not compare two missing schemata!'));
368 } else if ($expect && !$actual) {
369 $issues = array($missing_issue);
370 } else if ($actual && !$expect) {
371 $issues = array($surplus_issue);
372 } else {
373 $issues = $actual->compareTo($expect);
374 }
375
376 return $issues;
377 }
378
379
380}