Path: blob/master/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
12241 views
<?php12final class PhabricatorStorageManagementAPI extends Phobject {34private $ref;5private $host;6private $user;7private $port;8private $password;9private $namespace;10private $conns = array();11private $disableUTF8MB4;1213const CHARSET_DEFAULT = 'CHARSET';14const CHARSET_SORT = 'CHARSET_SORT';15const CHARSET_FULLTEXT = 'CHARSET_FULLTEXT';16const COLLATE_TEXT = 'COLLATE_TEXT';17const COLLATE_SORT = 'COLLATE_SORT';18const COLLATE_FULLTEXT = 'COLLATE_FULLTEXT';1920const TABLE_STATUS = 'patch_status';21const TABLE_HOSTSTATE = 'hoststate';2223public function setDisableUTF8MB4($disable_utf8_mb4) {24$this->disableUTF8MB4 = $disable_utf8_mb4;25return $this;26}2728public function getDisableUTF8MB4() {29return $this->disableUTF8MB4;30}3132public function setNamespace($namespace) {33$this->namespace = $namespace;34PhabricatorLiskDAO::pushStorageNamespace($namespace);35return $this;36}3738public function getNamespace() {39return $this->namespace;40}4142public function setUser($user) {43$this->user = $user;44return $this;45}4647public function getUser() {48return $this->user;49}5051public function setPassword($password) {52$this->password = $password;53return $this;54}5556public function getPassword() {57return $this->password;58}5960public function setHost($host) {61$this->host = $host;62return $this;63}6465public function getHost() {66return $this->host;67}6869public function setPort($port) {70$this->port = $port;71return $this;72}7374public function getPort() {75return $this->port;76}7778public function setRef(PhabricatorDatabaseRef $ref) {79$this->ref = $ref;80return $this;81}8283public function getRef() {84return $this->ref;85}8687public function getDatabaseName($fragment) {88return $this->namespace.'_'.$fragment;89}9091public function getInternalDatabaseName($name) {92$namespace = $this->getNamespace();9394$prefix = $namespace.'_';95if (strncmp($name, $prefix, strlen($prefix))) {96return null;97}9899return substr($name, strlen($prefix));100}101102public function getDisplayName() {103return $this->getRef()->getDisplayName();104}105106public function getDatabaseList(array $patches, $only_living = false) {107assert_instances_of($patches, 'PhabricatorStoragePatch');108109$list = array();110111foreach ($patches as $patch) {112if ($patch->getType() == 'db') {113if ($only_living && $patch->isDead()) {114continue;115}116$list[] = $this->getDatabaseName($patch->getName());117}118}119120return $list;121}122123public function getConn($fragment) {124$database = $this->getDatabaseName($fragment);125$return = &$this->conns[$this->host][$this->user][$database];126if (!$return) {127$return = PhabricatorDatabaseRef::newRawConnection(128array(129'user' => $this->user,130'pass' => $this->password,131'host' => $this->host,132'port' => $this->port,133'database' => $fragment134? $database135: null,136));137}138return $return;139}140141public function getAppliedPatches() {142try {143$applied = queryfx_all(144$this->getConn('meta_data'),145'SELECT patch FROM %T',146self::TABLE_STATUS);147return ipull($applied, 'patch');148} catch (AphrontAccessDeniedQueryException $ex) {149throw new PhutilProxyException(150pht(151'Failed while trying to read schema status: the database "%s" '.152'exists, but the current user ("%s") does not have permission to '.153'access it. GRANT the current user more permissions, or use a '.154'different user.',155$this->getDatabaseName('meta_data'),156$this->getUser()),157$ex);158} catch (AphrontQueryException $ex) {159return null;160}161}162163public function getPatchDurations() {164try {165$rows = queryfx_all(166$this->getConn('meta_data'),167'SELECT patch, duration FROM %T WHERE duration IS NOT NULL',168self::TABLE_STATUS);169return ipull($rows, 'duration', 'patch');170} catch (AphrontQueryException $ex) {171return array();172}173}174175public function createDatabase($fragment) {176$info = $this->getCharsetInfo();177178queryfx(179$this->getConn(null),180'CREATE DATABASE IF NOT EXISTS %T COLLATE %T',181$this->getDatabaseName($fragment),182$info[self::COLLATE_TEXT]);183}184185public function createTable($fragment, $table, array $cols) {186queryfx(187$this->getConn($fragment),188'CREATE TABLE IF NOT EXISTS %T.%T (%Q) '.189'ENGINE=InnoDB, COLLATE utf8_general_ci',190$this->getDatabaseName($fragment),191$table,192implode(', ', $cols));193}194195public function getLegacyPatches(array $patches) {196assert_instances_of($patches, 'PhabricatorStoragePatch');197198try {199$row = queryfx_one(200$this->getConn('meta_data'),201'SELECT version FROM %T',202'schema_version');203$version = $row['version'];204} catch (AphrontQueryException $ex) {205return array();206}207208$legacy = array();209foreach ($patches as $key => $patch) {210if ($patch->getLegacy() !== false && $patch->getLegacy() <= $version) {211$legacy[] = $key;212}213}214215return $legacy;216}217218public function markPatchApplied($patch, $duration = null) {219$conn = $this->getConn('meta_data');220221queryfx(222$conn,223'INSERT INTO %T (patch, applied) VALUES (%s, %d)',224self::TABLE_STATUS,225$patch,226time());227228// We didn't add this column for a long time, so it may not exist yet.229if ($duration !== null) {230try {231queryfx(232$conn,233'UPDATE %T SET duration = %d WHERE patch = %s',234self::TABLE_STATUS,235(int)floor($duration * 1000000),236$patch);237} catch (AphrontQueryException $ex) {238// Just ignore this, as it almost certainly indicates that we just239// don't have the column yet.240}241}242}243244public function applyPatch(PhabricatorStoragePatch $patch) {245$type = $patch->getType();246$name = $patch->getName();247switch ($type) {248case 'db':249$this->createDatabase($name);250break;251case 'sql':252$this->applyPatchSQL($name);253break;254case 'php':255$this->applyPatchPHP($name);256break;257default:258throw new Exception(pht("Unable to apply patch of type '%s'.", $type));259}260}261262public function applyPatchSQL($sql) {263$sql = Filesystem::readFile($sql);264$queries = preg_split('/;\s+/', $sql);265$queries = array_filter($queries);266267$conn = $this->getConn(null);268269$charset_info = $this->getCharsetInfo();270foreach ($charset_info as $key => $value) {271$charset_info[$key] = qsprintf($conn, '%T', $value);272}273274foreach ($queries as $query) {275$query = str_replace('{$NAMESPACE}', $this->namespace, $query);276277foreach ($charset_info as $key => $value) {278$query = str_replace('{$'.$key.'}', $value, $query);279}280281try {282// NOTE: We're using the unsafe "%Z" conversion here. There's no283// avoiding it since we're executing raw text files full of SQL.284queryfx($conn, '%Z', $query);285} catch (AphrontAccessDeniedQueryException $ex) {286throw new PhutilProxyException(287pht(288'Unable to access a required database or table. This almost '.289'always means that the user you are connecting with ("%s") does '.290'not have sufficient permissions granted in MySQL. You can '.291'use `bin/storage databases` to get a list of all databases '.292'permission is required on.',293$this->getUser()),294$ex);295}296}297}298299public function applyPatchPHP($script) {300$schema_conn = $this->getConn(null);301require_once $script;302}303304public function isCharacterSetAvailable($character_set) {305if ($character_set == 'utf8mb4') {306if ($this->getDisableUTF8MB4()) {307return false;308}309}310311$conn = $this->getConn(null);312return self::isCharacterSetAvailableOnConnection($character_set, $conn);313}314315public function getClientCharset() {316if ($this->isCharacterSetAvailable('utf8mb4')) {317return 'utf8mb4';318} else {319return 'utf8';320}321}322323public static function isCharacterSetAvailableOnConnection(324$character_set,325AphrontDatabaseConnection $conn) {326$result = queryfx_one(327$conn,328'SELECT CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.CHARACTER_SETS329WHERE CHARACTER_SET_NAME = %s',330$character_set);331332return (bool)$result;333}334335public function getCharsetInfo() {336if ($this->isCharacterSetAvailable('utf8mb4')) {337// If utf8mb4 is available, we use it with the utf8mb4_unicode_ci338// collation. This is most correct, and will sort properly.339340$charset = 'utf8mb4';341$charset_sort = 'utf8mb4';342$charset_full = 'utf8mb4';343$collate_text = 'utf8mb4_bin';344$collate_sort = 'utf8mb4_unicode_ci';345$collate_full = 'utf8mb4_unicode_ci';346} else {347// If utf8mb4 is not available, we use binary for most data. This allows348// us to store 4-byte unicode characters.349//350// It's possible that strings will be truncated in the middle of a351// character on insert. We encourage users to set STRICT_ALL_TABLES352// to prevent this.353//354// For "fulltext" and "sort" columns, we don't use binary.355//356// With "fulltext", we can not use binary because MySQL won't let us.357// We use 3-byte utf8 instead and accept being unable to index 4-byte358// characters.359//360// With "sort", if we use binary we lose case insensitivity (for361// example, "[email protected]" and "[email protected]" would no362// longer be identified as the same email address). This can be very363// confusing and is far worse overall than not supporting 4-byte unicode364// characters, so we use 3-byte utf8 and accept limited 4-byte support as365// a tradeoff to get sensible collation behavior. Many columns where366// collation is important rarely contain 4-byte characters anyway, so we367// are not giving up too much.368369$charset = 'binary';370$charset_sort = 'utf8';371$charset_full = 'utf8';372$collate_text = 'binary';373$collate_sort = 'utf8_general_ci';374$collate_full = 'utf8_general_ci';375}376377return array(378self::CHARSET_DEFAULT => $charset,379self::CHARSET_SORT => $charset_sort,380self::CHARSET_FULLTEXT => $charset_full,381self::COLLATE_TEXT => $collate_text,382self::COLLATE_SORT => $collate_sort,383self::COLLATE_FULLTEXT => $collate_full,384);385}386387}388389390