@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
3abstract class PhabricatorConfigSchemaSpec extends Phobject {
4
5 private $server;
6 private $utf8Charset;
7 private $utf8BinaryCollation;
8 private $utf8SortingCollation;
9
10 const DATATYPE_UNKNOWN = '<unknown>';
11
12 public function setUTF8SortingCollation($utf8_sorting_collation) {
13 $this->utf8SortingCollation = $utf8_sorting_collation;
14 return $this;
15 }
16
17 public function getUTF8SortingCollation() {
18 return $this->utf8SortingCollation;
19 }
20
21 public function setUTF8BinaryCollation($utf8_binary_collation) {
22 $this->utf8BinaryCollation = $utf8_binary_collation;
23 return $this;
24 }
25
26 public function getUTF8BinaryCollation() {
27 return $this->utf8BinaryCollation;
28 }
29
30 public function setUTF8Charset($utf8_charset) {
31 $this->utf8Charset = $utf8_charset;
32 return $this;
33 }
34
35 public function getUTF8Charset() {
36 return $this->utf8Charset;
37 }
38
39 public function setServer(PhabricatorConfigServerSchema $server) {
40 $this->server = $server;
41 return $this;
42 }
43
44 public function getServer() {
45 return $this->server;
46 }
47
48 abstract public function buildSchemata();
49
50 protected function buildLiskObjectSchema(PhabricatorLiskDAO $object) {
51 $index_options = array();
52
53 $persistence = $object->getSchemaPersistence();
54 if ($persistence !== null) {
55 $index_options['persistence'] = $persistence;
56 }
57
58 $this->buildRawSchema(
59 $object->getApplicationName(),
60 $object->getTableName(),
61 $object->getSchemaColumns(),
62 $object->getSchemaKeys(),
63 $index_options);
64 }
65
66 protected function buildFerretIndexSchema(PhabricatorFerretEngine $engine) {
67 $index_options = array(
68 'persistence' => PhabricatorConfigTableSchema::PERSISTENCE_INDEX,
69 );
70
71 $this->buildRawSchema(
72 $engine->getApplicationName(),
73 $engine->getDocumentTableName(),
74 $engine->getDocumentSchemaColumns(),
75 $engine->getDocumentSchemaKeys(),
76 $index_options);
77
78 $this->buildRawSchema(
79 $engine->getApplicationName(),
80 $engine->getFieldTableName(),
81 $engine->getFieldSchemaColumns(),
82 $engine->getFieldSchemaKeys(),
83 $index_options);
84
85 $this->buildRawSchema(
86 $engine->getApplicationName(),
87 $engine->getNgramsTableName(),
88 $engine->getNgramsSchemaColumns(),
89 $engine->getNgramsSchemaKeys(),
90 $index_options);
91
92 // NOTE: The common ngrams table is not marked as an index table. It is
93 // tiny and persisting it across a restore saves us a lot of work garbage
94 // collecting common ngrams from the index after it gets built.
95
96 $this->buildRawSchema(
97 $engine->getApplicationName(),
98 $engine->getCommonNgramsTableName(),
99 $engine->getCommonNgramsSchemaColumns(),
100 $engine->getCommonNgramsSchemaKeys());
101 }
102
103 protected function buildRawSchema(
104 $database_name,
105 $table_name,
106 array $columns,
107 array $keys,
108 array $options = array()) {
109
110 PhutilTypeSpec::checkMap(
111 $options,
112 array(
113 'persistence' => 'optional string',
114 ));
115
116 $database = $this->getDatabase($database_name);
117
118 $table = $this->newTable($table_name);
119
120 if (PhabricatorSearchDocument::isInnoDBFulltextEngineAvailable()) {
121 $fulltext_engine = 'InnoDB';
122 } else {
123 $fulltext_engine = 'MyISAM';
124 }
125
126 foreach ($columns as $name => $type) {
127 if ($type === null) {
128 continue;
129 }
130
131 $details = $this->getDetailsForDataType($type);
132
133 $column_type = $details['type'];
134 $charset = $details['charset'];
135 $collation = $details['collation'];
136 $nullable = $details['nullable'];
137 $auto = $details['auto'];
138
139 $column = $this->newColumn($name)
140 ->setDataType($type)
141 ->setColumnType($column_type)
142 ->setCharacterSet($charset)
143 ->setCollation($collation)
144 ->setNullable($nullable)
145 ->setAutoIncrement($auto);
146
147 // If this table has any FULLTEXT fields, we expect it to use the best
148 // available FULLTEXT engine, which may not be InnoDB.
149 switch ($type) {
150 case 'fulltext':
151 case 'fulltext?':
152 $table->setEngine($fulltext_engine);
153 break;
154 }
155
156 $table->addColumn($column);
157 }
158
159 foreach ($keys as $key_name => $key_spec) {
160 if ($key_spec === null) {
161 // This is a subclass removing a key which Lisk expects.
162 continue;
163 }
164
165 $key = $this->newKey($key_name)
166 ->setColumnNames(idx($key_spec, 'columns', array()));
167
168 $key->setUnique((bool)idx($key_spec, 'unique'));
169 $key->setIndexType(idx($key_spec, 'type', 'BTREE'));
170
171 $table->addKey($key);
172 }
173
174 $persistence_type = idx($options, 'persistence');
175 if ($persistence_type !== null) {
176 $table->setPersistenceType($persistence_type);
177 }
178
179 $database->addTable($table);
180 }
181
182 protected function buildEdgeSchemata(PhabricatorLiskDAO $object) {
183 $this->buildRawSchema(
184 $object->getApplicationName(),
185 PhabricatorEdgeConfig::TABLE_NAME_EDGE,
186 array(
187 'src' => 'phid',
188 'type' => 'uint32',
189 'dst' => 'phid',
190 'dateCreated' => 'epoch',
191 'seq' => 'uint32',
192 'dataID' => 'id?',
193 ),
194 array(
195 'PRIMARY' => array(
196 'columns' => array('src', 'type', 'dst'),
197 'unique' => true,
198 ),
199 'src' => array(
200 'columns' => array('src', 'type', 'dateCreated', 'seq'),
201 ),
202 'key_dst' => array(
203 'columns' => array('dst', 'type', 'src'),
204 'unique' => true,
205 ),
206 ));
207
208 $this->buildRawSchema(
209 $object->getApplicationName(),
210 PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
211 array(
212 'id' => 'auto',
213 'data' => 'text',
214 ),
215 array(
216 'PRIMARY' => array(
217 'columns' => array('id'),
218 'unique' => true,
219 ),
220 ));
221 }
222
223 protected function getDatabase($name) {
224 $server = $this->getServer();
225
226 $database = $server->getDatabase($this->getNamespacedDatabase($name));
227 if (!$database) {
228 $database = $this->newDatabase($name);
229 $server->addDatabase($database);
230 }
231
232 return $database;
233 }
234
235 protected function newDatabase($name) {
236 return id(new PhabricatorConfigDatabaseSchema())
237 ->setName($this->getNamespacedDatabase($name))
238 ->setCharacterSet($this->getUTF8Charset())
239 ->setCollation($this->getUTF8BinaryCollation());
240 }
241
242 protected function getNamespacedDatabase($name) {
243 $namespace = PhabricatorLiskDAO::getStorageNamespace();
244 return $namespace.'_'.$name;
245 }
246
247 protected function newTable($name) {
248 return id(new PhabricatorConfigTableSchema())
249 ->setName($name)
250 ->setCollation($this->getUTF8BinaryCollation())
251 ->setEngine('InnoDB');
252 }
253
254 protected function newColumn($name) {
255 return id(new PhabricatorConfigColumnSchema())
256 ->setName($name);
257 }
258
259 protected function newKey($name) {
260 return id(new PhabricatorConfigKeySchema())
261 ->setName($name);
262 }
263
264 public function getMaximumByteLengthForDataType($data_type) {
265 $info = $this->getDetailsForDataType($data_type);
266 return idx($info, 'bytes');
267 }
268
269 private function getDetailsForDataType($data_type) {
270 $column_type = null;
271 $charset = null;
272 $collation = null;
273 $auto = false;
274 $bytes = null;
275
276 // If the type ends with "?", make the column nullable.
277 $nullable = false;
278 if (preg_match('/\?$/', $data_type)) {
279 $nullable = true;
280 $data_type = substr($data_type, 0, -1);
281 }
282
283 // NOTE: MySQL allows fragments like "VARCHAR(32) CHARACTER SET binary",
284 // but just interprets that to mean "VARBINARY(32)". The fragment is
285 // totally disallowed in a MODIFY statement vs a CREATE TABLE statement.
286
287 $is_binary = ($this->getUTF8Charset() == 'binary');
288 $matches = null;
289 $pattern = '/^(fulltext|sort|text|char)(\d+)?\z/';
290 if (preg_match($pattern, $data_type, $matches)) {
291
292 // Limit the permitted column lengths under the theory that it would
293 // be nice to eventually reduce this to a small set of standard lengths.
294
295 static $valid_types = array(
296 'text255' => true,
297 'text160' => true,
298 'text128' => true,
299 'text64' => true,
300 'text40' => true,
301 'text32' => true,
302 'text20' => true,
303 'text16' => true,
304 'text12' => true,
305 'text8' => true,
306 'text4' => true,
307 'text' => true,
308 'char3' => true,
309 'sort255' => true,
310 'sort128' => true,
311 'sort64' => true,
312 'sort32' => true,
313 'sort' => true,
314 'fulltext' => true,
315 );
316
317 if (empty($valid_types[$data_type])) {
318 throw new Exception(pht('Unknown column type "%s"!', $data_type));
319 }
320
321 $type = $matches[1];
322 $size = idx($matches, 2);
323
324 if ($size) {
325 $bytes = $size;
326 }
327
328 switch ($type) {
329 case 'text':
330 if ($is_binary) {
331 if ($size) {
332 $column_type = 'varbinary('.$size.')';
333 } else {
334 $column_type = 'longblob';
335 }
336 } else {
337 if ($size) {
338 $column_type = 'varchar('.$size.')';
339 } else {
340 $column_type = 'longtext';
341 }
342 }
343 break;
344 case 'sort':
345 if ($size) {
346 $column_type = 'varchar('.$size.')';
347 } else {
348 $column_type = 'longtext';
349 }
350 break;
351 case 'fulltext':
352 // MySQL (at least, under MyISAM) refuses to create a FULLTEXT index
353 // on a LONGBLOB column. We'd also lose case insensitivity in search.
354 // Force this column to utf8 collation. This will truncate results
355 // with 4-byte UTF characters in their text, but work reasonably in
356 // the majority of cases.
357 $column_type = 'longtext';
358 break;
359 case 'char':
360 $column_type = 'char('.$size.')';
361 break;
362 }
363
364 switch ($type) {
365 case 'text':
366 case 'char':
367 if ($is_binary) {
368 // We leave collation and character set unspecified in order to
369 // generate valid SQL.
370 } else {
371 $charset = $this->getUTF8Charset();
372 $collation = $this->getUTF8BinaryCollation();
373 }
374 break;
375 case 'sort':
376 case 'fulltext':
377 if ($is_binary) {
378 $charset = 'utf8';
379 } else {
380 $charset = $this->getUTF8Charset();
381 }
382 $collation = $this->getUTF8SortingCollation();
383 break;
384 }
385 } else {
386 switch ($data_type) {
387 case 'auto':
388 $column_type = 'int(10) unsigned';
389 $auto = true;
390 break;
391 case 'auto64':
392 $column_type = 'bigint(20) unsigned';
393 $auto = true;
394 break;
395 case 'id':
396 case 'epoch':
397 case 'uint32':
398 $column_type = 'int(10) unsigned';
399 break;
400 case 'sint32':
401 $column_type = 'int(10)';
402 break;
403 case 'id64':
404 case 'uint64':
405 $column_type = 'bigint(20) unsigned';
406 break;
407 case 'sint64':
408 $column_type = 'bigint(20)';
409 break;
410 case 'phid':
411 case 'policy':
412 case 'hashpath64':
413 case 'ipaddress':
414 $column_type = 'varbinary(64)';
415 break;
416 case 'bytes64':
417 $column_type = 'binary(64)';
418 break;
419 case 'bytes40':
420 $column_type = 'binary(40)';
421 break;
422 case 'bytes32':
423 $column_type = 'binary(32)';
424 break;
425 case 'bytes20':
426 $column_type = 'binary(20)';
427 break;
428 case 'bytes12':
429 $column_type = 'binary(12)';
430 break;
431 case 'bytes4':
432 $column_type = 'binary(4)';
433 break;
434 case 'bytes':
435 $column_type = 'longblob';
436 break;
437 case 'bool':
438 $column_type = 'tinyint(1)';
439 break;
440 case 'double':
441 $column_type = 'double';
442 break;
443 case 'date':
444 $column_type = 'date';
445 break;
446 default:
447 $column_type = self::DATATYPE_UNKNOWN;
448 $charset = self::DATATYPE_UNKNOWN;
449 $collation = self::DATATYPE_UNKNOWN;
450 break;
451 }
452 }
453
454 return array(
455 'type' => $column_type,
456 'charset' => $charset,
457 'collation' => $collation,
458 'nullable' => $nullable,
459 'auto' => $auto,
460 'bytes' => $bytes,
461 );
462 }
463
464}