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