@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
at recaptime-dev/main 381 lines 11 kB view raw
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}