@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

Support key schemata and column nullability

Summary:
Ref T1191. The major issue motivation here is that InnoDB keys have a maximum length of 767 bytes. When we move `utf8` colums to `utf8mb4` columns, they'll jump from 3 bytes per character to 4 bytes per character, which may make some indexes too long. Add key schema to help spot this.

Also add nullability since it doesn't hurt.

Test Plan: See screenshots.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T1191

Differential Revision: https://secure.phabricator.com/D10499

+338 -16
+2
src/__phutil_library_map__.php
··· 1349 1349 'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php', 1350 1350 'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php', 1351 1351 'PhabricatorConfigJSONOptionType' => 'applications/config/custom/PhabricatorConfigJSONOptionType.php', 1352 + 'PhabricatorConfigKeySchema' => 'applications/config/schema/PhabricatorConfigKeySchema.php', 1352 1353 'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php', 1353 1354 'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php', 1354 1355 'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php', ··· 4242 4243 'PhabricatorConfigIssueListController' => 'PhabricatorConfigController', 4243 4244 'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController', 4244 4245 'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType', 4246 + 'PhabricatorConfigKeySchema' => 'PhabricatorConfigStorageSchema', 4245 4247 'PhabricatorConfigListController' => 'PhabricatorConfigController', 4246 4248 'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource', 4247 4249 'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
+1 -1
src/applications/config/application/PhabricatorConfigApplication.php
··· 45 45 'database/'. 46 46 '(?:(?P<database>[^/]+)/'. 47 47 '(?:(?P<table>[^/]+)/'. 48 - '(?:(?P<column>[^/]+)/)?)?)?' 48 + '(?:(?:col/(?P<column>[^/]+)|key/(?P<key>[^/]+))/)?)?)?' 49 49 => 'PhabricatorConfigDatabaseController', 50 50 '(?P<verb>ignore|unignore)/(?P<key>[^/]+)/' 51 51 => 'PhabricatorConfigIgnoreController',
+172 -5
src/applications/config/controller/PhabricatorConfigDatabaseController.php
··· 3 3 final class PhabricatorConfigDatabaseController 4 4 extends PhabricatorConfigController { 5 5 6 + const MAX_INNODB_KEY_LENGTH = 767; 7 + 6 8 private $database; 7 9 private $table; 8 10 private $column; 11 + private $key; 9 12 10 13 public function willProcessRequest(array $data) { 11 14 $this->database = idx($data, 'database'); 12 15 $this->table = idx($data, 'table'); 13 16 $this->column = idx($data, 'column'); 17 + $this->key = idx($data, 'key'); 14 18 } 15 19 16 20 public function processRequest() { ··· 43 47 $this->database, 44 48 $this->table, 45 49 $this->column); 50 + } else if ($this->key) { 51 + return $this->renderKey( 52 + $comp, 53 + $expect, 54 + $actual, 55 + $this->database, 56 + $this->table, 57 + $this->key); 46 58 } else if ($this->table) { 47 59 return $this->renderTable( 48 60 $comp, ··· 77 89 $crumbs->addTextCrumb( 78 90 $this->database, 79 91 $this->getApplicationURI('database/'.$this->database.'/')); 80 - if ($this->column) { 92 + if ($this->column || $this->key) { 81 93 $crumbs->addTextCrumb( 82 94 $this->table, 83 95 $this->getApplicationURI( 84 96 'database/'.$this->database.'/'.$this->table.'/')); 85 - $crumbs->addTextCrumb($this->column); 97 + if ($this->column) { 98 + $crumbs->addTextCrumb($this->column); 99 + } else { 100 + $crumbs->addTextCrumb($this->key); 101 + } 86 102 } else { 87 103 $crumbs->addTextCrumb($this->table); 88 104 } ··· 276 292 $type_issue = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE; 277 293 $charset_issue = PhabricatorConfigStorageSchema::ISSUE_CHARSET; 278 294 $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; 295 + $nullable_issue = PhabricatorConfigStorageSchema::ISSUE_NULLABLE; 279 296 280 297 $database = $comp->getDatabase($database_name); 281 298 if (!$database) { ··· 322 339 'database/'. 323 340 $database_name.'/'. 324 341 $table_name.'/'. 342 + 'col/'. 325 343 $column_name.'/'), 326 344 ), 327 345 $column_name), ··· 330 348 $column->getColumnType(), 331 349 $column->hasIssue($type_issue)), 332 350 $this->renderAttr( 351 + $column->getNullable() 352 + ? pht('Yes') 353 + : pht('No'), 354 + $column->hasIssue($nullable_issue)), 355 + $this->renderAttr( 333 356 $column->getCharacterSet(), 334 357 $column->hasIssue($charset_issue)), 335 358 $this->renderAttr( ··· 342 365 ->setHeaders( 343 366 array( 344 367 null, 345 - pht('Table'), 368 + pht('Column'), 346 369 pht('Data Type'), 347 370 pht('Column Type'), 371 + pht('Nullable'), 348 372 pht('Character Set'), 349 373 pht('Collation'), 350 374 )) ··· 358 382 null 359 383 )); 360 384 385 + $key_rows = array(); 386 + foreach ($table->getKeys() as $key_name => $key) { 387 + $expect_key = null; 388 + if ($expect_table) { 389 + $expect_key = $expect_table->getKey($key_name); 390 + } 391 + 392 + $status = $key->getStatus(); 393 + 394 + $size = 0; 395 + foreach ($key->getColumnNames() as $column_name) { 396 + $column = $table->getColumn($column_name); 397 + if (!$column) { 398 + $size = 0; 399 + break; 400 + } 401 + $size += $column->getKeyByteLength(); 402 + } 403 + 404 + $size_formatted = null; 405 + if ($size) { 406 + $size_formatted = $this->renderAttr( 407 + $size, 408 + ($size > self::MAX_INNODB_KEY_LENGTH)); 409 + } 410 + 411 + $key_rows[] = array( 412 + $this->renderIcon($status), 413 + phutil_tag( 414 + 'a', 415 + array( 416 + 'href' => $this->getApplicationURI( 417 + 'database/'. 418 + $database_name.'/'. 419 + $table_name.'/'. 420 + 'key/'. 421 + $key_name.'/'), 422 + ), 423 + $key_name), 424 + implode(', ', $key->getColumnNames()), 425 + $size_formatted, 426 + ); 427 + } 428 + 429 + $keys_view = id(new AphrontTableView($key_rows)) 430 + ->setHeaders( 431 + array( 432 + null, 433 + pht('Key'), 434 + pht('Columns'), 435 + pht('Size'), 436 + )) 437 + ->setColumnClasses( 438 + array( 439 + null, 440 + 'wide pri', 441 + null, 442 + null, 443 + )); 444 + 361 445 $title = pht('Database Status: %s.%s', $database_name, $table_name); 362 446 363 447 if ($actual_table) { ··· 388 472 $box = id(new PHUIObjectBoxView()) 389 473 ->setHeaderText($title) 390 474 ->addPropertyList($properties) 391 - ->appendChild($table_view); 475 + ->appendChild($table_view) 476 + ->appendChild($keys_view); 392 477 393 478 return $this->buildResponse($title, $box); 394 479 } ··· 412 497 } 413 498 414 499 $column = $table->getColumn($column_name); 415 - if (!$table) { 500 + if (!$column) { 416 501 return new Aphront404Response(); 417 502 } 418 503 ··· 504 589 505 590 return $this->buildResponse($title, $box); 506 591 } 592 + 593 + private function renderKey( 594 + PhabricatorConfigServerSchema $comp, 595 + PhabricatorConfigServerSchema $expect, 596 + PhabricatorConfigServerSchema $actual, 597 + $database_name, 598 + $table_name, 599 + $key_name) { 600 + 601 + $database = $comp->getDatabase($database_name); 602 + if (!$database) { 603 + return new Aphront404Response(); 604 + } 605 + 606 + $table = $database->getTable($table_name); 607 + if (!$table) { 608 + return new Aphront404Response(); 609 + } 610 + 611 + $key = $table->getKey($key_name); 612 + if (!$key) { 613 + return new Aphront404Response(); 614 + } 615 + 616 + $actual_database = $actual->getDatabase($database_name); 617 + $actual_table = null; 618 + $actual_key = null; 619 + if ($actual_database) { 620 + $actual_table = $actual_database->getTable($table_name); 621 + if ($actual_table) { 622 + $actual_key = $actual_table->getKey($key_name); 623 + } 624 + } 625 + 626 + $expect_database = $expect->getDatabase($database_name); 627 + $expect_table = null; 628 + $expect_key = null; 629 + if ($expect_database) { 630 + $expect_table = $expect_database->getTable($table_name); 631 + if ($expect_table) { 632 + $expect_key = $expect_table->getKey($key_name); 633 + } 634 + } 635 + 636 + if ($actual_key) { 637 + $actual_columns = $actual_key->getColumnNames(); 638 + } else { 639 + $actual_columns = array(); 640 + } 641 + 642 + if ($expect_key) { 643 + $expect_columns = $expect_key->getColumnNames(); 644 + } else { 645 + $expect_columns = array(); 646 + } 647 + 648 + $title = pht( 649 + 'Database Status: %s.%s (%s)', 650 + $database_name, 651 + $table_name, 652 + $key_name); 653 + 654 + $properties = $this->buildProperties( 655 + array( 656 + array( 657 + pht('Columns'), 658 + implode(', ', $actual_columns), 659 + ), 660 + array( 661 + pht('Expected Columns'), 662 + implode(', ', $expect_columns), 663 + ), 664 + ), 665 + $key->getIssues()); 666 + 667 + $box = id(new PHUIObjectBoxView()) 668 + ->setHeaderText($title) 669 + ->addPropertyList($properties); 670 + 671 + return $this->buildResponse($title, $box); 672 + } 673 + 507 674 private function renderIcon($status) { 508 675 switch ($status) { 509 676 case PhabricatorConfigStorageSchema::STATUS_OKAY:
+24
src/applications/config/schema/PhabricatorConfigColumnSchema.php
··· 7 7 private $collation; 8 8 private $columnType; 9 9 private $dataType; 10 + private $nullable; 11 + 12 + public function setNullable($nullable) { 13 + $this->nullable = $nullable; 14 + return $this; 15 + } 16 + 17 + public function getNullable() { 18 + return $this->nullable; 19 + } 10 20 11 21 public function setColumnType($column_type) { 12 22 $this->columnType = $column_type; ··· 46 56 47 57 public function getCharacterSet() { 48 58 return $this->characterSet; 59 + } 60 + 61 + public function getKeyByteLength() { 62 + $type = $this->getColumnType(); 63 + 64 + $matches = null; 65 + if (preg_match('/^varchar\((\d+)\)$/', $type, $matches)) { 66 + // For utf8mb4, each character requires 4 bytes. 67 + return ((int)$matches[1]) * 4; 68 + } 69 + 70 + // TODO: Build this out to catch overlong indexes. 71 + 72 + return 0; 49 73 } 50 74 51 75 public function compareToSimilarSchema(
+37
src/applications/config/schema/PhabricatorConfigKeySchema.php
··· 1 + <?php 2 + 3 + final class PhabricatorConfigKeySchema 4 + extends PhabricatorConfigStorageSchema { 5 + 6 + private $columnNames; 7 + 8 + public function setColumnNames(array $column_names) { 9 + $this->columnNames = $column_names; 10 + return $this; 11 + } 12 + 13 + public function getColumnNames() { 14 + return $this->columnNames; 15 + } 16 + 17 + protected function getSubschemata() { 18 + return array(); 19 + } 20 + 21 + public function compareToSimilarSchema( 22 + PhabricatorConfigStorageSchema $expect) { 23 + 24 + $issues = array(); 25 + if ($this->getColumnNames() !== $expect->getColumnNames()) { 26 + $issues[] = self::ISSUE_KEYCOLUMNS; 27 + } 28 + 29 + return $issues; 30 + } 31 + 32 + public function newEmptyClone() { 33 + $clone = clone $this; 34 + return $clone; 35 + } 36 + 37 + }
+70 -9
src/applications/config/schema/PhabricatorConfigSchemaQuery.php
··· 47 47 $databases); 48 48 $database_info = ipull($database_info, null, 'SCHEMA_NAME'); 49 49 50 + $sql = array(); 51 + foreach ($tables as $table) { 52 + $sql[] = qsprintf( 53 + $conn, 54 + '(TABLE_SCHEMA = %s AND TABLE_NAME = %s)', 55 + $table['TABLE_SCHEMA'], 56 + $table['TABLE_NAME']); 57 + } 58 + 59 + if ($sql) { 60 + $column_info = queryfx_all( 61 + $conn, 62 + 'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, 63 + COLLATION_NAME, COLUMN_TYPE, IS_NULLABLE 64 + FROM INFORMATION_SCHEMA.COLUMNS 65 + WHERE (%Q)', 66 + '('.implode(') OR (', $sql).')'); 67 + $column_info = igroup($column_info, 'TABLE_SCHEMA'); 68 + } else { 69 + $column_info = array(); 70 + } 71 + 72 + if ($sql) { 73 + $key_info = queryfx_all( 74 + $conn, 75 + 'SELECT CONSTRAINT_NAME, TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, 76 + ORDINAL_POSITION, POSITION_IN_UNIQUE_CONSTRAINT 77 + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE 78 + WHERE (%Q)', 79 + '('.implode(') OR (', $sql).')'); 80 + $key_info = igroup($key_info, 'TABLE_SCHEMA'); 81 + } else { 82 + $key_info = array(); 83 + } 84 + 50 85 $server_schema = new PhabricatorConfigServerSchema(); 51 86 52 87 $tables = igroup($tables, 'TABLE_SCHEMA'); ··· 57 92 ->setName($database_name) 58 93 ->setCharacterSet($info['DEFAULT_CHARACTER_SET_NAME']) 59 94 ->setCollation($info['DEFAULT_COLLATION_NAME']); 95 + 96 + $database_column_info = idx($column_info, $database_name, array()); 97 + $database_column_info = igroup($database_column_info, 'TABLE_NAME'); 98 + $database_key_info = idx($key_info, $database_name, array()); 99 + $database_key_info = igroup($database_key_info, 'TABLE_NAME'); 60 100 61 101 foreach ($database_tables as $table) { 62 102 $table_name = $table['TABLE_NAME']; ··· 65 105 ->setName($table_name) 66 106 ->setCollation($table['TABLE_COLLATION']); 67 107 68 - $columns = queryfx_all( 69 - $conn, 70 - 'SELECT COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_TYPE 71 - FROM INFORMATION_SCHEMA.COLUMNS 72 - WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s', 73 - $database_name, 74 - $table_name); 75 - 108 + $columns = idx($database_column_info, $table_name, array()); 76 109 foreach ($columns as $column) { 77 110 $column_schema = id(new PhabricatorConfigColumnSchema()) 78 111 ->setName($column['COLUMN_NAME']) 79 112 ->setCharacterSet($column['CHARACTER_SET_NAME']) 80 113 ->setCollation($column['COLLATION_NAME']) 81 - ->setColumnType($column['COLUMN_TYPE']); 114 + ->setColumnType($column['COLUMN_TYPE']) 115 + ->setNullable($column['IS_NULLABLE'] == 'YES'); 82 116 83 117 $table_schema->addColumn($column_schema); 118 + } 119 + 120 + $key_parts = idx($database_key_info, $table_name, array()); 121 + $keys = igroup($key_parts, 'CONSTRAINT_NAME'); 122 + foreach ($keys as $key_name => $key_pieces) { 123 + $key_pieces = isort($key_pieces, 'ORDINAL_POSITION'); 124 + $key_schema = id(new PhabricatorConfigKeySchema()) 125 + ->setName($key_name) 126 + ->setColumnNames(ipull($key_pieces, 'COLUMN_NAME')); 127 + 128 + $table_schema->addKey($key_schema); 84 129 } 85 130 86 131 $database_schema->addTable($table_schema); ··· 190 235 191 236 $comp_table->addColumn($comp_column); 192 237 } 238 + 239 + $all_keys = 240 + $actual_table->getKeys() + 241 + $expect_table->getKeys(); 242 + foreach ($all_keys as $key_name => $key_template) { 243 + $actual_key = $actual_table->getKey($key_name); 244 + $expect_key = $expect_table->getKey($key_name); 245 + 246 + $issues = $this->compareSchemata($expect_key, $actual_key); 247 + 248 + $comp_key = $key_template->newEmptyClone() 249 + ->setIssues($issues); 250 + 251 + $comp_table->addKey($comp_key); 252 + } 253 + 193 254 $comp_database->addTable($comp_table); 194 255 } 195 256 $comp_server->addDatabase($comp_database);
+11
src/applications/config/schema/PhabricatorConfigStorageSchema.php
··· 7 7 const ISSUE_CHARSET = 'charset'; 8 8 const ISSUE_COLLATION = 'collation'; 9 9 const ISSUE_COLUMNTYPE = 'columntype'; 10 + const ISSUE_NULLABLE = 'nullable'; 11 + const ISSUE_KEYCOLUMNS = 'keycolumns'; 10 12 const ISSUE_SUBWARN = 'subwarn'; 11 13 const ISSUE_SUBFAIL = 'subfail'; 12 14 ··· 98 100 return pht('Wrong Collation'); 99 101 case self::ISSUE_COLUMNTYPE: 100 102 return pht('Wrong Column Type'); 103 + case self::ISSUE_NULLABLE: 104 + return pht('Wrong Nullable Setting'); 105 + case self::ISSUE_KEYCOLUMNS: 106 + return pht('Key on Wrong Columns'); 101 107 case self::ISSUE_SUBWARN: 102 108 return pht('Subschemata Have Warnings'); 103 109 case self::ISSUE_SUBFAIL: ··· 119 125 return pht('This schema can use a better collation.'); 120 126 case self::ISSUE_COLUMNTYPE: 121 127 return pht('This schema can use a better column type.'); 128 + case self::ISSUE_NULLABLE: 129 + return pht('This schema has the wrong nullable setting.'); 130 + case self::ISSUE_KEYCOLUMNS: 131 + return pht('This schema is on the wrong columns.'); 122 132 case self::ISSUE_SUBWARN: 123 133 return pht('Subschemata have setup warnings.'); 124 134 case self::ISSUE_SUBFAIL: ··· 138 148 case self::ISSUE_COLLATION: 139 149 case self::ISSUE_COLUMNTYPE: 140 150 case self::ISSUE_SUBWARN: 151 + case self::ISSUE_KEYCOLUMNS: 141 152 return self::STATUS_WARN; 142 153 default: 143 154 throw new Exception(pht('Unknown schema issue "%s"!', $issue));
+21 -1
src/applications/config/schema/PhabricatorConfigTableSchema.php
··· 5 5 6 6 private $collation; 7 7 private $columns = array(); 8 + private $keys = array(); 8 9 9 10 public function addColumn(PhabricatorConfigColumnSchema $column) { 10 11 $key = $column->getName(); ··· 13 14 pht('Trying to add duplicate column "%s"!', $key)); 14 15 } 15 16 $this->columns[$key] = $column; 17 + return $this; 18 + } 19 + 20 + public function addKey(PhabricatorConfigKeySchema $key) { 21 + $name = $key->getName(); 22 + if (isset($this->keys[$name])) { 23 + throw new Exception( 24 + pht('Trying to add duplicate key "%s"!', $name)); 25 + } 26 + $this->keys[$name] = $key; 16 27 return $this; 17 28 } 18 29 ··· 24 35 return idx($this->getColumns(), $key); 25 36 } 26 37 38 + public function getKeys() { 39 + return $this->keys; 40 + } 41 + 42 + public function getKey($key) { 43 + return idx($this->getKeys(), $key); 44 + } 45 + 27 46 protected function getSubschemata() { 28 - return $this->getColumns(); 47 + return array_merge($this->getColumns(), $this->getKeys()); 29 48 } 30 49 31 50 public function setCollation($collation) { ··· 51 70 public function newEmptyClone() { 52 71 $clone = clone $this; 53 72 $clone->columns = array(); 73 + $clone->keys = array(); 54 74 return $clone; 55 75 } 56 76