@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 380 lines 12 kB view raw
1<?php 2 3final class PhabricatorConfigSchemaQuery extends Phobject { 4 5 private $refs; 6 private $apis; 7 8 public function setRefs(array $refs) { 9 $this->refs = $refs; 10 return $this; 11 } 12 13 public function getRefs() { 14 if (!$this->refs) { 15 return PhabricatorDatabaseRef::getMasterDatabaseRefs(); 16 } 17 return $this->refs; 18 } 19 20 public function setAPIs(array $apis) { 21 $map = array(); 22 foreach ($apis as $api) { 23 $map[$api->getRef()->getRefKey()] = $api; 24 } 25 $this->apis = $map; 26 return $this; 27 } 28 29 private function getDatabaseNames(PhabricatorDatabaseRef $ref) { 30 $api = $this->getAPI($ref); 31 $patches = PhabricatorSQLPatchList::buildAllPatches(); 32 return $api->getDatabaseList( 33 $patches, 34 $only_living = true); 35 } 36 37 private function getAPI(PhabricatorDatabaseRef $ref) { 38 $key = $ref->getRefKey(); 39 40 if (isset($this->apis[$key])) { 41 return $this->apis[$key]; 42 } 43 44 return id(new PhabricatorStorageManagementAPI()) 45 ->setUser($ref->getUser()) 46 ->setHost($ref->getHost()) 47 ->setPort($ref->getPort()) 48 ->setNamespace(PhabricatorLiskDAO::getDefaultStorageNamespace()) 49 ->setPassword($ref->getPass()); 50 } 51 52 public function loadActualSchemata() { 53 $refs = $this->getRefs(); 54 55 $schemata = array(); 56 foreach ($refs as $ref) { 57 $schema = $this->loadActualSchemaForServer($ref); 58 $schemata[$schema->getRef()->getRefKey()] = $schema; 59 } 60 61 return $schemata; 62 } 63 64 private function loadActualSchemaForServer(PhabricatorDatabaseRef $ref) { 65 $databases = $this->getDatabaseNames($ref); 66 67 $conn = $ref->newManagementConnection(); 68 69 $tables = queryfx_all( 70 $conn, 71 'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION, ENGINE 72 FROM INFORMATION_SCHEMA.TABLES 73 WHERE TABLE_SCHEMA IN (%Ls)', 74 $databases); 75 76 $database_info = queryfx_all( 77 $conn, 78 'SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME 79 FROM INFORMATION_SCHEMA.SCHEMATA 80 WHERE SCHEMA_NAME IN (%Ls)', 81 $databases); 82 $database_info = ipull($database_info, null, 'SCHEMA_NAME'); 83 84 // Find databases which exist, but which the user does not have permission 85 // to see. 86 $invisible_databases = array(); 87 foreach ($databases as $database_name) { 88 if (isset($database_info[$database_name])) { 89 continue; 90 } 91 92 try { 93 queryfx($conn, 'SHOW TABLES IN %T', $database_name); 94 } catch (AphrontAccessDeniedQueryException $ex) { 95 // This database exists, the user just doesn't have permission to 96 // see it. 97 $invisible_databases[] = $database_name; 98 } catch (AphrontSchemaQueryException $ex) { 99 // This database is legitimately missing. 100 } 101 } 102 103 $sql = array(); 104 foreach ($tables as $table) { 105 $sql[] = qsprintf( 106 $conn, 107 '(TABLE_SCHEMA = %s AND TABLE_NAME = %s)', 108 $table['TABLE_SCHEMA'], 109 $table['TABLE_NAME']); 110 } 111 112 if ($sql) { 113 $column_info = queryfx_all( 114 $conn, 115 'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, 116 COLLATION_NAME, COLUMN_TYPE, IS_NULLABLE, EXTRA 117 FROM INFORMATION_SCHEMA.COLUMNS 118 WHERE %LO', 119 $sql); 120 $column_info = igroup($column_info, 'TABLE_SCHEMA'); 121 } else { 122 $column_info = array(); 123 } 124 125 // NOTE: Tables like KEY_COLUMN_USAGE and TABLE_CONSTRAINTS only contain 126 // primary, unique, and foreign keys, so we can't use them here. We pull 127 // indexes later on using SHOW INDEXES. 128 129 $server_schema = id(new PhabricatorConfigServerSchema()) 130 ->setRef($ref); 131 132 $tables = igroup($tables, 'TABLE_SCHEMA'); 133 foreach ($tables as $database_name => $database_tables) { 134 $info = $database_info[$database_name]; 135 136 $database_schema = id(new PhabricatorConfigDatabaseSchema()) 137 ->setName($database_name) 138 ->setCharacterSet($info['DEFAULT_CHARACTER_SET_NAME']) 139 ->setCollation($info['DEFAULT_COLLATION_NAME']); 140 141 $database_column_info = idx($column_info, $database_name, array()); 142 $database_column_info = igroup($database_column_info, 'TABLE_NAME'); 143 144 foreach ($database_tables as $table) { 145 $table_name = $table['TABLE_NAME']; 146 147 $table_schema = id(new PhabricatorConfigTableSchema()) 148 ->setName($table_name) 149 ->setCollation($table['TABLE_COLLATION']) 150 ->setEngine($table['ENGINE']); 151 152 $columns = idx($database_column_info, $table_name, array()); 153 foreach ($columns as $column) { 154 if (strpos($column['EXTRA'], 'auto_increment') === false) { 155 $auto_increment = false; 156 } else { 157 $auto_increment = true; 158 } 159 160 $column_schema = id(new PhabricatorConfigColumnSchema()) 161 ->setName($column['COLUMN_NAME']) 162 ->setCharacterSet($column['CHARACTER_SET_NAME']) 163 ->setCollation($column['COLLATION_NAME']) 164 ->setColumnType($column['COLUMN_TYPE']) 165 ->setNullable($column['IS_NULLABLE'] == 'YES') 166 ->setAutoIncrement($auto_increment); 167 168 $table_schema->addColumn($column_schema); 169 } 170 171 $key_parts = queryfx_all( 172 $conn, 173 'SHOW INDEXES FROM %T.%T', 174 $database_name, 175 $table_name); 176 $keys = igroup($key_parts, 'Key_name'); 177 foreach ($keys as $key_name => $key_pieces) { 178 $key_pieces = isort($key_pieces, 'Seq_in_index'); 179 $head = head($key_pieces); 180 181 // This handles string indexes which index only a prefix of a field. 182 $column_names = array(); 183 foreach ($key_pieces as $piece) { 184 $name = $piece['Column_name']; 185 if ($piece['Sub_part']) { 186 $name = $name.'('.$piece['Sub_part'].')'; 187 } 188 $column_names[] = $name; 189 } 190 191 $key_schema = id(new PhabricatorConfigKeySchema()) 192 ->setName($key_name) 193 ->setColumnNames($column_names) 194 ->setUnique(!$head['Non_unique']) 195 ->setIndexType($head['Index_type']); 196 197 $table_schema->addKey($key_schema); 198 } 199 200 $database_schema->addTable($table_schema); 201 } 202 203 $server_schema->addDatabase($database_schema); 204 } 205 206 foreach ($invisible_databases as $database_name) { 207 $server_schema->addDatabase( 208 id(new PhabricatorConfigDatabaseSchema()) 209 ->setName($database_name) 210 ->setAccessDenied(true)); 211 } 212 213 return $server_schema; 214 } 215 216 public function loadExpectedSchemata() { 217 $refs = $this->getRefs(); 218 219 $schemata = array(); 220 foreach ($refs as $ref) { 221 $schema = $this->loadExpectedSchemaForServer($ref); 222 $schemata[$schema->getRef()->getRefKey()] = $schema; 223 } 224 225 return $schemata; 226 } 227 228 public function loadExpectedSchemaForServer(PhabricatorDatabaseRef $ref) { 229 $databases = $this->getDatabaseNames($ref); 230 $info = $this->getAPI($ref)->getCharsetInfo(); 231 232 $specs = id(new PhutilClassMapQuery()) 233 ->setAncestorClass(PhabricatorConfigSchemaSpec::class) 234 ->execute(); 235 236 $server_schema = id(new PhabricatorConfigServerSchema()) 237 ->setRef($ref); 238 239 foreach ($specs as $spec) { 240 $spec 241 ->setUTF8Charset( 242 $info[PhabricatorStorageManagementAPI::CHARSET_DEFAULT]) 243 ->setUTF8BinaryCollation( 244 $info[PhabricatorStorageManagementAPI::COLLATE_TEXT]) 245 ->setUTF8SortingCollation( 246 $info[PhabricatorStorageManagementAPI::COLLATE_SORT]) 247 ->setServer($server_schema) 248 ->buildSchemata($server_schema); 249 } 250 251 return $server_schema; 252 } 253 254 public function buildComparisonSchemata( 255 array $expect_servers, 256 array $actual_servers) { 257 258 $schemata = array(); 259 foreach ($actual_servers as $key => $actual_server) { 260 $schemata[$key] = $this->buildComparisonSchemaForServer( 261 $expect_servers[$key], 262 $actual_server); 263 } 264 265 return $schemata; 266 } 267 268 private function buildComparisonSchemaForServer( 269 PhabricatorConfigServerSchema $expect, 270 PhabricatorConfigServerSchema $actual) { 271 272 $comp_server = $actual->newEmptyClone(); 273 274 $all_databases = $actual->getDatabases() + $expect->getDatabases(); 275 foreach ($all_databases as $database_name => $database_template) { 276 $actual_database = $actual->getDatabase($database_name); 277 $expect_database = $expect->getDatabase($database_name); 278 279 $issues = $this->compareSchemata($expect_database, $actual_database); 280 281 $comp_database = $database_template->newEmptyClone() 282 ->setIssues($issues); 283 284 if (!$actual_database) { 285 $actual_database = $expect_database->newEmptyClone(); 286 } 287 288 if (!$expect_database) { 289 $expect_database = $actual_database->newEmptyClone(); 290 } 291 292 $all_tables = 293 $actual_database->getTables() + 294 $expect_database->getTables(); 295 foreach ($all_tables as $table_name => $table_template) { 296 $actual_table = $actual_database->getTable($table_name); 297 $expect_table = $expect_database->getTable($table_name); 298 299 $issues = $this->compareSchemata($expect_table, $actual_table); 300 301 $comp_table = $table_template->newEmptyClone() 302 ->setIssues($issues); 303 304 if (!$actual_table) { 305 $actual_table = $expect_table->newEmptyClone(); 306 } 307 if (!$expect_table) { 308 $expect_table = $actual_table->newEmptyClone(); 309 } 310 311 $all_columns = 312 $actual_table->getColumns() + 313 $expect_table->getColumns(); 314 foreach ($all_columns as $column_name => $column_template) { 315 $actual_column = $actual_table->getColumn($column_name); 316 $expect_column = $expect_table->getColumn($column_name); 317 318 $issues = $this->compareSchemata($expect_column, $actual_column); 319 320 $comp_column = $column_template->newEmptyClone() 321 ->setIssues($issues); 322 323 $comp_table->addColumn($comp_column); 324 } 325 326 $all_keys = 327 $actual_table->getKeys() + 328 $expect_table->getKeys(); 329 foreach ($all_keys as $key_name => $key_template) { 330 $actual_key = $actual_table->getKey($key_name); 331 $expect_key = $expect_table->getKey($key_name); 332 333 $issues = $this->compareSchemata($expect_key, $actual_key); 334 335 $comp_key = $key_template->newEmptyClone() 336 ->setIssues($issues); 337 338 $comp_table->addKey($comp_key); 339 } 340 341 $comp_table->setPersistenceType($expect_table->getPersistenceType()); 342 343 $comp_database->addTable($comp_table); 344 } 345 $comp_server->addDatabase($comp_database); 346 } 347 348 return $comp_server; 349 } 350 351 private function compareSchemata( 352 ?PhabricatorConfigStorageSchema $expect = null, 353 ?PhabricatorConfigStorageSchema $actual = null) { 354 355 $expect_is_key = ($expect instanceof PhabricatorConfigKeySchema); 356 $actual_is_key = ($actual instanceof PhabricatorConfigKeySchema); 357 358 if ($expect_is_key || $actual_is_key) { 359 $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY; 360 $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY; 361 } else { 362 $missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSING; 363 $surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUS; 364 } 365 366 if (!$expect && !$actual) { 367 throw new Exception(pht('Can not compare two missing schemata!')); 368 } else if ($expect && !$actual) { 369 $issues = array($missing_issue); 370 } else if ($actual && !$expect) { 371 $issues = array($surplus_issue); 372 } else { 373 $issues = $actual->compareTo($expect); 374 } 375 376 return $issues; 377 } 378 379 380}