Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
12241 views
1
<?php
2
3
final class PhabricatorStorageManagementAPI extends Phobject {
4
5
private $ref;
6
private $host;
7
private $user;
8
private $port;
9
private $password;
10
private $namespace;
11
private $conns = array();
12
private $disableUTF8MB4;
13
14
const CHARSET_DEFAULT = 'CHARSET';
15
const CHARSET_SORT = 'CHARSET_SORT';
16
const CHARSET_FULLTEXT = 'CHARSET_FULLTEXT';
17
const COLLATE_TEXT = 'COLLATE_TEXT';
18
const COLLATE_SORT = 'COLLATE_SORT';
19
const COLLATE_FULLTEXT = 'COLLATE_FULLTEXT';
20
21
const TABLE_STATUS = 'patch_status';
22
const TABLE_HOSTSTATE = 'hoststate';
23
24
public function setDisableUTF8MB4($disable_utf8_mb4) {
25
$this->disableUTF8MB4 = $disable_utf8_mb4;
26
return $this;
27
}
28
29
public function getDisableUTF8MB4() {
30
return $this->disableUTF8MB4;
31
}
32
33
public function setNamespace($namespace) {
34
$this->namespace = $namespace;
35
PhabricatorLiskDAO::pushStorageNamespace($namespace);
36
return $this;
37
}
38
39
public function getNamespace() {
40
return $this->namespace;
41
}
42
43
public function setUser($user) {
44
$this->user = $user;
45
return $this;
46
}
47
48
public function getUser() {
49
return $this->user;
50
}
51
52
public function setPassword($password) {
53
$this->password = $password;
54
return $this;
55
}
56
57
public function getPassword() {
58
return $this->password;
59
}
60
61
public function setHost($host) {
62
$this->host = $host;
63
return $this;
64
}
65
66
public function getHost() {
67
return $this->host;
68
}
69
70
public function setPort($port) {
71
$this->port = $port;
72
return $this;
73
}
74
75
public function getPort() {
76
return $this->port;
77
}
78
79
public function setRef(PhabricatorDatabaseRef $ref) {
80
$this->ref = $ref;
81
return $this;
82
}
83
84
public function getRef() {
85
return $this->ref;
86
}
87
88
public function getDatabaseName($fragment) {
89
return $this->namespace.'_'.$fragment;
90
}
91
92
public function getInternalDatabaseName($name) {
93
$namespace = $this->getNamespace();
94
95
$prefix = $namespace.'_';
96
if (strncmp($name, $prefix, strlen($prefix))) {
97
return null;
98
}
99
100
return substr($name, strlen($prefix));
101
}
102
103
public function getDisplayName() {
104
return $this->getRef()->getDisplayName();
105
}
106
107
public function getDatabaseList(array $patches, $only_living = false) {
108
assert_instances_of($patches, 'PhabricatorStoragePatch');
109
110
$list = array();
111
112
foreach ($patches as $patch) {
113
if ($patch->getType() == 'db') {
114
if ($only_living && $patch->isDead()) {
115
continue;
116
}
117
$list[] = $this->getDatabaseName($patch->getName());
118
}
119
}
120
121
return $list;
122
}
123
124
public function getConn($fragment) {
125
$database = $this->getDatabaseName($fragment);
126
$return = &$this->conns[$this->host][$this->user][$database];
127
if (!$return) {
128
$return = PhabricatorDatabaseRef::newRawConnection(
129
array(
130
'user' => $this->user,
131
'pass' => $this->password,
132
'host' => $this->host,
133
'port' => $this->port,
134
'database' => $fragment
135
? $database
136
: null,
137
));
138
}
139
return $return;
140
}
141
142
public function getAppliedPatches() {
143
try {
144
$applied = queryfx_all(
145
$this->getConn('meta_data'),
146
'SELECT patch FROM %T',
147
self::TABLE_STATUS);
148
return ipull($applied, 'patch');
149
} catch (AphrontAccessDeniedQueryException $ex) {
150
throw new PhutilProxyException(
151
pht(
152
'Failed while trying to read schema status: the database "%s" '.
153
'exists, but the current user ("%s") does not have permission to '.
154
'access it. GRANT the current user more permissions, or use a '.
155
'different user.',
156
$this->getDatabaseName('meta_data'),
157
$this->getUser()),
158
$ex);
159
} catch (AphrontQueryException $ex) {
160
return null;
161
}
162
}
163
164
public function getPatchDurations() {
165
try {
166
$rows = queryfx_all(
167
$this->getConn('meta_data'),
168
'SELECT patch, duration FROM %T WHERE duration IS NOT NULL',
169
self::TABLE_STATUS);
170
return ipull($rows, 'duration', 'patch');
171
} catch (AphrontQueryException $ex) {
172
return array();
173
}
174
}
175
176
public function createDatabase($fragment) {
177
$info = $this->getCharsetInfo();
178
179
queryfx(
180
$this->getConn(null),
181
'CREATE DATABASE IF NOT EXISTS %T COLLATE %T',
182
$this->getDatabaseName($fragment),
183
$info[self::COLLATE_TEXT]);
184
}
185
186
public function createTable($fragment, $table, array $cols) {
187
queryfx(
188
$this->getConn($fragment),
189
'CREATE TABLE IF NOT EXISTS %T.%T (%Q) '.
190
'ENGINE=InnoDB, COLLATE utf8_general_ci',
191
$this->getDatabaseName($fragment),
192
$table,
193
implode(', ', $cols));
194
}
195
196
public function getLegacyPatches(array $patches) {
197
assert_instances_of($patches, 'PhabricatorStoragePatch');
198
199
try {
200
$row = queryfx_one(
201
$this->getConn('meta_data'),
202
'SELECT version FROM %T',
203
'schema_version');
204
$version = $row['version'];
205
} catch (AphrontQueryException $ex) {
206
return array();
207
}
208
209
$legacy = array();
210
foreach ($patches as $key => $patch) {
211
if ($patch->getLegacy() !== false && $patch->getLegacy() <= $version) {
212
$legacy[] = $key;
213
}
214
}
215
216
return $legacy;
217
}
218
219
public function markPatchApplied($patch, $duration = null) {
220
$conn = $this->getConn('meta_data');
221
222
queryfx(
223
$conn,
224
'INSERT INTO %T (patch, applied) VALUES (%s, %d)',
225
self::TABLE_STATUS,
226
$patch,
227
time());
228
229
// We didn't add this column for a long time, so it may not exist yet.
230
if ($duration !== null) {
231
try {
232
queryfx(
233
$conn,
234
'UPDATE %T SET duration = %d WHERE patch = %s',
235
self::TABLE_STATUS,
236
(int)floor($duration * 1000000),
237
$patch);
238
} catch (AphrontQueryException $ex) {
239
// Just ignore this, as it almost certainly indicates that we just
240
// don't have the column yet.
241
}
242
}
243
}
244
245
public function applyPatch(PhabricatorStoragePatch $patch) {
246
$type = $patch->getType();
247
$name = $patch->getName();
248
switch ($type) {
249
case 'db':
250
$this->createDatabase($name);
251
break;
252
case 'sql':
253
$this->applyPatchSQL($name);
254
break;
255
case 'php':
256
$this->applyPatchPHP($name);
257
break;
258
default:
259
throw new Exception(pht("Unable to apply patch of type '%s'.", $type));
260
}
261
}
262
263
public function applyPatchSQL($sql) {
264
$sql = Filesystem::readFile($sql);
265
$queries = preg_split('/;\s+/', $sql);
266
$queries = array_filter($queries);
267
268
$conn = $this->getConn(null);
269
270
$charset_info = $this->getCharsetInfo();
271
foreach ($charset_info as $key => $value) {
272
$charset_info[$key] = qsprintf($conn, '%T', $value);
273
}
274
275
foreach ($queries as $query) {
276
$query = str_replace('{$NAMESPACE}', $this->namespace, $query);
277
278
foreach ($charset_info as $key => $value) {
279
$query = str_replace('{$'.$key.'}', $value, $query);
280
}
281
282
try {
283
// NOTE: We're using the unsafe "%Z" conversion here. There's no
284
// avoiding it since we're executing raw text files full of SQL.
285
queryfx($conn, '%Z', $query);
286
} catch (AphrontAccessDeniedQueryException $ex) {
287
throw new PhutilProxyException(
288
pht(
289
'Unable to access a required database or table. This almost '.
290
'always means that the user you are connecting with ("%s") does '.
291
'not have sufficient permissions granted in MySQL. You can '.
292
'use `bin/storage databases` to get a list of all databases '.
293
'permission is required on.',
294
$this->getUser()),
295
$ex);
296
}
297
}
298
}
299
300
public function applyPatchPHP($script) {
301
$schema_conn = $this->getConn(null);
302
require_once $script;
303
}
304
305
public function isCharacterSetAvailable($character_set) {
306
if ($character_set == 'utf8mb4') {
307
if ($this->getDisableUTF8MB4()) {
308
return false;
309
}
310
}
311
312
$conn = $this->getConn(null);
313
return self::isCharacterSetAvailableOnConnection($character_set, $conn);
314
}
315
316
public function getClientCharset() {
317
if ($this->isCharacterSetAvailable('utf8mb4')) {
318
return 'utf8mb4';
319
} else {
320
return 'utf8';
321
}
322
}
323
324
public static function isCharacterSetAvailableOnConnection(
325
$character_set,
326
AphrontDatabaseConnection $conn) {
327
$result = queryfx_one(
328
$conn,
329
'SELECT CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.CHARACTER_SETS
330
WHERE CHARACTER_SET_NAME = %s',
331
$character_set);
332
333
return (bool)$result;
334
}
335
336
public function getCharsetInfo() {
337
if ($this->isCharacterSetAvailable('utf8mb4')) {
338
// If utf8mb4 is available, we use it with the utf8mb4_unicode_ci
339
// collation. This is most correct, and will sort properly.
340
341
$charset = 'utf8mb4';
342
$charset_sort = 'utf8mb4';
343
$charset_full = 'utf8mb4';
344
$collate_text = 'utf8mb4_bin';
345
$collate_sort = 'utf8mb4_unicode_ci';
346
$collate_full = 'utf8mb4_unicode_ci';
347
} else {
348
// If utf8mb4 is not available, we use binary for most data. This allows
349
// us to store 4-byte unicode characters.
350
//
351
// It's possible that strings will be truncated in the middle of a
352
// character on insert. We encourage users to set STRICT_ALL_TABLES
353
// to prevent this.
354
//
355
// For "fulltext" and "sort" columns, we don't use binary.
356
//
357
// With "fulltext", we can not use binary because MySQL won't let us.
358
// We use 3-byte utf8 instead and accept being unable to index 4-byte
359
// characters.
360
//
361
// With "sort", if we use binary we lose case insensitivity (for
362
// example, "[email protected]" and "[email protected]" would no
363
// longer be identified as the same email address). This can be very
364
// confusing and is far worse overall than not supporting 4-byte unicode
365
// characters, so we use 3-byte utf8 and accept limited 4-byte support as
366
// a tradeoff to get sensible collation behavior. Many columns where
367
// collation is important rarely contain 4-byte characters anyway, so we
368
// are not giving up too much.
369
370
$charset = 'binary';
371
$charset_sort = 'utf8';
372
$charset_full = 'utf8';
373
$collate_text = 'binary';
374
$collate_sort = 'utf8_general_ci';
375
$collate_full = 'utf8_general_ci';
376
}
377
378
return array(
379
self::CHARSET_DEFAULT => $charset,
380
self::CHARSET_SORT => $charset_sort,
381
self::CHARSET_FULLTEXT => $charset_full,
382
self::COLLATE_TEXT => $collate_text,
383
self::COLLATE_SORT => $collate_sort,
384
self::COLLATE_FULLTEXT => $collate_full,
385
);
386
}
387
388
}
389
390