diff --git a/src/docs/contributor/database.diviner b/src/docs/contributor/database.diviner index 48e2c20b79..8ac6bf7798 100644 --- a/src/docs/contributor/database.diviner +++ b/src/docs/contributor/database.diviner @@ -1,176 +1,212 @@ @title Database Schema @group developer This document describes key components of the database schema and should answer questions like how to store new types of data. -= Database System = +Database System +=============== -Phabricator uses MySQL with InnoDB engine. The only exception is the +Phabricator uses MySQL or another MySQL-compatible database (like MariaDB +or Amazon RDS). + +Phabricator the InnoDB table engine. The only exception is the `search_documentfield` table which uses MyISAM because MySQL doesn't support -fulltext search in InnoDB. +fulltext search in InnoDB (recent versions do, but we haven't added support +yet). -Let us know if you need to use other database system: @{article:Give Feedback! -Get Support!}. +We are unlikely to ever support other incompatible databases like PostgreSQL or +SQLite. -= PHP Drivers = +PHP Drivers +=========== Phabricator supports [[ http://www.php.net/book.mysql | MySQL ]] and -[[ http://www.php.net/book.mysqli | MySQLi ]] PHP extensions. Most installations -use MySQL but MySQLi should work equally well. +[[ http://www.php.net/book.mysqli | MySQLi ]] PHP extensions. -= Databases = +Databases +========= Each Phabricator application has its own database. The names are prefixed by -`phabricator_`. This design has two advantages: +`phabricator_` (this is configurable). This design has two advantages: -* Each database is easier to comprehend and to maintain. -* We don't do cross-database joins so each database can live on its own machine - which is useful for load-balancing. + - Each database is easier to comprehend and to maintain. + - We don't do cross-database joins so each database can live on its own + machine. This gives us flexibility in sharding data later. -= Connections = +Connections +=========== Phabricator specifies if it will use any opened connection just for reading or -also for writing. This allows opening write connections to master and read -connections to slave in master/slave replication. It is useful for -load-balancing. +also for writing. This allows opening write connections to a primary and read +connections to a replica in primary/replica setups (which are not actually +supported yet). -= Tables = +Tables +====== -Each table name is prefixed by its application. For example, Differential -revisions are stored in database `phabricator_differential` and table -`differential_revision`. This duplicity allows easy recognition of the table in -DarkConsole (see @{article:Using DarkConsole}) and other places. +Most table names are prefixed by their application names. For example, +Differential revisions are stored in database `phabricator_differential` and +table `differential_revision`. This generally makes queries easier to recognize +and understand. -The exception is tables which share the same schema over different databases -such as `edge`. +The exception is a few tables which share the same schema over different +databases such as `edge`. -We use lower-case table names with words separated by underscores. The reason is -that MySQL can be configured (with `lower_case_table_names`) to lower-case the -table names anyway. +We use lower-case table names with words separated by underscores. -= Column Names = +Column Names +============ -Phabricator uses camelCase names for columns. The main advantage is that they +Phabricator uses `camelCase` names for columns. The main advantage is that they directly map to properties in PHP classes. Don't use MySQL reserved words (such as `order`) for column names. -= Data Types = +Data Types +========== + +Phabricator defines a set of abstract data types (like `uint32`, `epoch`, and +`phid`) which map to MySQL column types. The mapping depends on the MySQL +version. -Phabricator uses `int unsigned` columns for storing dates instead of `date` or -`datetime`. We don't need to care about time-zones in both MySQL and PHP because -of it. The other reason is that PHP internally uses numbers for storing dates. +Phabricator uses `utf8mb4` character sets where available (MySQL 5.5 or newer), +and `binary` character sets in most other cases. The primary motivation is to +allow 4-byte unicode characters to be stored (the `utf8` character set, which +is more widely available, does not support them). On newer MySQL, we use +`utf8mb4` to take advantage of improved collation rules. -Phabricator uses UTF-8 encoding for storing all text data. We use -`utf8_general_ci` collation for free-text and `utf8_bin` for identifiers. +Phabricator stores dates with an `epoch` abstract data type, which maps to +`int unsigned`. Although this makes dates less readable when browsing the +database, it makes date and time manipulation more consistent and +straightforward in the application. We don't use the `enum` data type because each change to the list of possible values requires altering the table (which is slow with big tables). We use numbers (or short strings in some cases) mapped to PHP constants instead. -= JSON = +JSON and Other Serialized Data +============================== -Some data don't require structured access - you don't need to filter or order by +Some data don't require structured access -- we don't need to filter or order by them. We store these data as text fields in JSON format. This approach has several advantages: -* If we decide to add another unstructured field then we don't need to alter the - table (which is slow for big tables in MySQL). -* Table structure is not cluttered by fields which could be unused most of the - time. + - If we decide to add another unstructured field then we don't need to alter + the table (which is slow for big tables in MySQL). + - Table structure is not cluttered by fields which could be unused most of the + time. An example of such usage can be found in column `differential_diffproperty.data`. -= Primary Keys = +Primary Keys +============ + +Most tables have auto-increment column named `id`. Adding an ID column is +appropriate for most tables (even tables that have another natural unique key), +as it improves consistency and makes it easier to perform generic operations +on objects. -Most tables have auto-increment column named `id`. However creating such column -is not required for tables which are not usually directly referenced (such as -tables expressing M:N relations). Example of such table is -`differential_relationship`. +For example, @{class:LiskMigrationIterator} allows you to very easily apply a +migration to a table using a constant amount of memory provided the table has +an `id` column. -= Indexes = +Indexes +====== Create all indexes necessary for fast query execution in most cases. Don't create indexes which are not used. You can analyze queries @{article:Using DarkConsole}. Older MySQL versions are not able to use indexes for tuple search: `(a, b) IN ((%s, %d), (%s, %d))`. Use `AND` and `OR` instead: `((a = %s AND b = %d) OR (a = %s AND b = %d))`. -= Foreign Keys = +Foreign Keys +============ -We don't use InnoDB's foreign keys because our application is so great that -no inconsistencies can arise. It will just slow us down. +We don't use foreign keys because they're complicated and we haven't experienced +significant issues with data inconsistency that foreign keys could help prevent. +Empirically, we have witnessed first hand as `ON DELETE CASCADE` relationships +accidentally destroy huge amounts of data. We may pursue foreign keys +eventually, but there isn't a strong case for them at the present time. -= PHIDs = +PHIDs +===== Each globally referencable object in Phabricator has its associated PHID -(Phabricator ID) which serves as a global identifier. We use PHIDs for -referencing data in different databases. +("Phabricator ID") which serves as a global identifier, similar to a GUID. +We use PHIDs for referencing data in different databases. We use both autoincrementing IDs and global PHIDs because each is useful in -different contexts. Autoincrementing IDs are chronologically ordered and allow -us to construct short, human-readable object names (like D2258) and URIs. Global -PHIDs allow us to represent relationships between different types of objects in -a homogeneous way. +different contexts. Autoincrementing IDs are meaningfully ordered and allow +us to construct short, human-readable object names (like `D2258`) and URIs. +Global PHIDs allow us to represent relationships between different types of +objects in a homogeneous way. -For example, the concept of "subscribers" is more powerfully done with PHIDs -because we could theoretically have users, projects, teams, and more all as -"subscribers" of other objects. Using an ID column we would need to add a -"type" column to avoid ID collision; using PHIDs does not require this -additional column. +For example, infrastructure like "subscribers" can be implemented easily with +PHID relationships: different types of objects (users, projects, mailing lists) +are permitted to subscribe to different types of objects (revisions, tasks, +etc). Without PHIDs, we would need to add a "type" column to avoid ID collision; +using PHIDs makes implementing features like this simpler. -= Transactions = +Transactions +============ Transactional code should be written using transactions. Example of such code is inserting multiple records where one doesn't make sense without the other or selecting data later used for update. See chapter in @{class:LiskDAO}. -= Advanced Features = +Advanced Features +================= We don't use MySQL advanced features such as triggers, stored procedures or events because we like expressing the application logic in PHP more than in SQL. -Some of these features (especially triggers) can also cause big confusion. +Some of these features (especially triggers) can also cause a great deal of +confusion, and are generally more difficult to debug, profile, version control, +update, and understand than application code. + +Schema Denormalization +====================== + +Phabricator uses schema denormalization sparingly. Avoid denormalization unless +there is a compelling reason (usually, performance) to denormalize. -Avoiding these advanced features is also good for supporting other database -systems (which we don't support anyway). +Schema Changes and Migrations +============================= -= Schema Denormalization = +To create a new schema change or migration: -Phabricator uses schema denormalization for performance reasons sparingly. Try -to avoid it if possible. +**Create a database patch**. Database patches go in +`resources/sql/autopatches/`. To change a schema, use a `.sql` file and write +in SQL. To perform a migration, use a `.php` file and write in PHP. Name your +file `YYYYMMDD.patchname.ext`. For example, `20141225.christmas.sql`. -= Changing the Schema = +**Keep patches small**. Most schema change statements are not transactional. If +a patch contains several SQL statements and fails partway through, it normally +can not be rolled back. When a user tries to apply the patch again later, the +first statement (which, for example, adds a column) may fail (because the column +already exists). This can be avoided by keeping patches small (generally, one +statement per patch). -There are three simple steps to update the schema: +**Use namespace and character set variables**. When defining a `.sql` patch, +you should use these variables instead of hard-coding namespaces or character +set names: -# Create a `.sql` file in `resources/sql/patches/`. This file should: - - Contain the appropriate MySQL commands to update the schema. - - Be named as `YYYYMMDD.patchname.ext`. For example, `20130217.example.sql`. - - Use `${NAMESPACE}` rather than `phabricator` for database names. - - Use `COLLATE utf8_bin` for any columns that are to be used as identifiers, - such as PHID columns. Otherwise, use `COLLATE utf8_general_ci`. - - Name all indexes so it is possible to delete them later. -# Edit `src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php` and - add your patch to - @{method@phabricator:PhabricatorBuiltinPatchList::getPatches}. -# Run `bin/storage upgrade`. +| Variable | Meaning | Notes | +|---|---|---| +| {$NAMESPACE} | Storage Namespace | Defaults to `phabricator` | +| {$CHARSET} | Default Charset | Mostly used to specify table charset | +| {$COLLATE_TEXT} | Text Collation | For most text (case-sensitive) | +| {$COLLATE_SORT} | Sort Collation | For sortable text (case-insensitive) | +| {$CHARSET_FULLTEXT} | Fulltext Charset | Specify explicitly for fulltext | +| {$COLLATE_FULLTEXT} | Fulltext Collate | Specify explicitly for fulltext | -It is also possible to create more complex patches in PHP for data migration -(due to schema changes or otherwise.) However, the schema changes themselves -should be done in separate `.sql` files. Order can be guaranteed by editing -`src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php` -appropriately. -See the -[[https://secure.phabricator.com/rPb39175342dc5bee0c2246b05fa277e76a7e96ed3 -| commit adding policy storage for Paste ]] for a reasonable example of the code -changes. +**Test your patch**. Run `bin/storage upgrade` to test your patch. -= See Also = +See Also +======== -* @{class:LiskDAO} -* @{class:PhabricatorPHID} + - @{class:LiskDAO} diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php index eebd773825..7eec23f81e 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php @@ -1,572 +1,567 @@ setName('adjust') ->setExamples('**adjust** [__options__]') ->setSynopsis( pht( 'Make schemata adjustments to correct issues with characters sets, '. 'collations, and keys.')) ->setArguments( array( array( 'name' => 'unsafe', 'help' => pht( 'Permit adjustments which truncate data. This option may '. 'destroy some data, but the lost data is usually not '. 'important (most commonly, the ends of very long object '. 'titles).'), ), )); } public function execute(PhutilArgumentParser $args) { $force = $args->getArg('force'); $unsafe = $args->getArg('unsafe'); $this->requireAllPatchesApplied(); return $this->adjustSchemata($force, $unsafe); } private function requireAllPatchesApplied() { $api = $this->getAPI(); $applied = $api->getAppliedPatches(); if ($applied === null) { throw new PhutilArgumentUsageException( pht( 'You have not initialized the database yet. You must initialize '. 'the database before you can adjust schemata. Run `storage upgrade` '. 'to initialize the database.')); } $applied = array_fuse($applied); $patches = $this->getPatches(); $patches = mpull($patches, null, 'getFullKey'); $missing = array_diff_key($patches, $applied); if ($missing) { throw new PhutilArgumentUsageException( pht( 'You have not applied all available storage patches yet. You must '. 'apply all available patches before you can adjust schemata. '. 'Run `storage status` to show patch status, and `storage upgrade` '. 'to apply missing patches.')); } } private function loadSchemata() { $query = id(new PhabricatorConfigSchemaQuery()) ->setAPI($this->getAPI()); $actual = $query->loadActualSchema(); $expect = $query->loadExpectedSchema(); $comp = $query->buildComparisonSchema($expect, $actual); return array($comp, $expect, $actual); } private function adjustSchemata($force, $unsafe) { $console = PhutilConsole::getConsole(); $console->writeOut( "%s\n", pht('Verifying database schemata...')); $adjustments = $this->findAdjustments(); $api = $this->getAPI(); if (!$adjustments) { $console->writeOut( "%s\n", pht('Found no issues with schemata.')); return; } if (!$force && !$api->isCharacterSetAvailable('utf8mb4')) { $message = pht( "You have an old version of MySQL (older than 5.5) which does not ". "support the utf8mb4 character set. If you apply adjustments now ". "and later update MySQL to 5.5 or newer, you'll need to apply ". "adjustments again (and they will take a long time).\n\n". "You can exit this workflow, update MySQL now, and then run this ". "workflow again. This is recommended, but may cause a lot of downtime ". "right now.\n\n". "You can exit this workflow, continue using Phabricator without ". "applying adjustments, update MySQL at a later date, and then run ". "this workflow again. This is also a good approach, and will let you ". "delay downtime until later.\n\n". "You can proceed with this workflow, and then optionally update ". "MySQL at a later date. After you do, you'll need to apply ". "adjustments again.\n\n". "For more information, see \"Managing Storage Adjustments\" in ". "the documentation."); $console->writeOut( "\n** %s **\n\n%s\n", pht('OLD MySQL VERSION'), phutil_console_wrap($message)); $prompt = pht('Continue with old MySQL version?'); if (!phutil_console_confirm($prompt, $default_no = true)) { return; } } $table = id(new PhutilConsoleTable()) ->addColumn('database', array('title' => pht('Database'))) ->addColumn('table', array('title' => pht('Table'))) ->addColumn('name', array('title' => pht('Name'))) ->addColumn('info', array('title' => pht('Issues'))); foreach ($adjustments as $adjust) { $info = array(); foreach ($adjust['issues'] as $issue) { $info[] = PhabricatorConfigStorageSchema::getIssueName($issue); } $table->addRow(array( 'database' => $adjust['database'], 'table' => idx($adjust, 'table'), 'name' => idx($adjust, 'name'), 'info' => implode(', ', $info), )); } $console->writeOut("\n\n"); $table->draw(); if (!$force) { $console->writeOut( "\n%s\n", pht( "Found %s issues(s) with schemata, detailed above.\n\n". "You can review issues in more detail from the web interface, ". - "in Config > Database Status.\n\n". + "in Config > Database Status. To better understand the adjustment ". + "workflow, see \"Managing Storage Adjustments\" in the ". + "documentation.\n\n". "MySQL needs to copy table data to make some adjustments, so these ". - "migrations may take some time.". - - // TODO: Remove warning once this stabilizes. - "\n\n". - "WARNING: This workflow is new and unstable. If you continue, you ". - "may unrecoverably destory data. Make sure you have a backup before ". - "you proceed.", - + "migrations may take some time.", new PhutilNumber(count($adjustments)))); $prompt = pht('Fix these schema issues?'); if (!phutil_console_confirm($prompt, $default_no = true)) { return; } } $console->writeOut( "%s\n", pht('Dropping caches, for faster migrations...')); $root = dirname(phutil_get_library_root('phabricator')); $bin = $root.'/bin/cache'; phutil_passthru('%s purge --purge-all', $bin); $console->writeOut( "%s\n", pht('Fixing schema issues...')); $conn = $api->getConn(null); if ($unsafe) { queryfx($conn, 'SET SESSION sql_mode = %s', ''); } else { queryfx($conn, 'SET SESSION sql_mode = %s', 'STRICT_ALL_TABLES'); } $failed = array(); // We make changes in several phases. $phases = array( // Drop surplus autoincrements. This allows us to drop primary keys on // autoincrement columns. 'drop_auto', // Drop all keys we're going to adjust. This prevents them from // interfering with column changes. 'drop_keys', // Apply all database, table, and column changes. 'main', // Restore adjusted keys. 'add_keys', // Add missing autoincrements. 'add_auto', ); $bar = id(new PhutilConsoleProgressBar()) ->setTotal(count($adjustments) * count($phases)); foreach ($phases as $phase) { foreach ($adjustments as $adjust) { try { switch ($adjust['kind']) { case 'database': if ($phase == 'main') { queryfx( $conn, 'ALTER DATABASE %T CHARACTER SET = %s COLLATE = %s', $adjust['database'], $adjust['charset'], $adjust['collation']); } break; case 'table': if ($phase == 'main') { queryfx( $conn, 'ALTER TABLE %T.%T COLLATE = %s', $adjust['database'], $adjust['table'], $adjust['collation']); } break; case 'column': $apply = false; $auto = false; $new_auto = idx($adjust, 'auto'); if ($phase == 'drop_auto') { if ($new_auto === false) { $apply = true; $auto = false; } } else if ($phase == 'main') { $apply = true; if ($new_auto === false) { $auto = false; } else { $auto = $adjust['is_auto']; } } else if ($phase == 'add_auto') { if ($new_auto === true) { $apply = true; $auto = true; } } if ($apply) { $parts = array(); if ($auto) { $parts[] = qsprintf( $conn, 'AUTO_INCREMENT'); } if ($adjust['charset']) { $parts[] = qsprintf( $conn, 'CHARACTER SET %Q COLLATE %Q', $adjust['charset'], $adjust['collation']); } queryfx( $conn, 'ALTER TABLE %T.%T MODIFY %T %Q %Q %Q', $adjust['database'], $adjust['table'], $adjust['name'], $adjust['type'], implode(' ', $parts), $adjust['nullable'] ? 'NULL' : 'NOT NULL'); } break; case 'key': if (($phase == 'drop_keys') && $adjust['exists']) { if ($adjust['name'] == 'PRIMARY') { $key_name = 'PRIMARY KEY'; } else { $key_name = qsprintf($conn, 'KEY %T', $adjust['name']); } queryfx( $conn, 'ALTER TABLE %T.%T DROP %Q', $adjust['database'], $adjust['table'], $key_name); } if (($phase == 'add_keys') && $adjust['keep']) { // Different keys need different creation syntax. Notable // special cases are primary keys and fulltext keys. if ($adjust['name'] == 'PRIMARY') { $key_name = 'PRIMARY KEY'; } else if ($adjust['indexType'] == 'FULLTEXT') { $key_name = qsprintf($conn, 'FULLTEXT %T', $adjust['name']); } else { if ($adjust['unique']) { $key_name = qsprintf( $conn, 'UNIQUE KEY %T', $adjust['name']); } else { $key_name = qsprintf( $conn, '/* NONUNIQUE */ KEY %T', $adjust['name']); } } queryfx( $conn, 'ALTER TABLE %T.%T ADD %Q (%Q)', $adjust['database'], $adjust['table'], $key_name, implode(', ', $adjust['columns'])); } break; default: throw new Exception( pht('Unknown schema adjustment kind "%s"!', $adjust['kind'])); } } catch (AphrontQueryException $ex) { $failed[] = array($adjust, $ex); } $bar->update(1); } } $bar->done(); if (!$failed) { $console->writeOut( "%s\n", pht('Completed fixing all schema issues.')); return 0; } $table = id(new PhutilConsoleTable()) ->addColumn('target', array('title' => pht('Target'))) ->addColumn('error', array('title' => pht('Error'))); foreach ($failed as $failure) { list($adjust, $ex) = $failure; $pieces = array_select_keys($adjust, array('database', 'table', 'name')); $pieces = array_filter($pieces); $target = implode('.', $pieces); $table->addRow( array( 'target' => $target, 'error' => $ex->getMessage(), )); } $console->writeOut("\n"); $table->draw(); $console->writeOut( "\n%s\n", pht('Failed to make some schema adjustments, detailed above.')); $console->writeOut( "%s\n", pht( 'For help troubleshooting adjustments, see "Managing Storage '. 'Adjustments" in the documentation.')); return 1; } private function findAdjustments() { list($comp, $expect, $actual) = $this->loadSchemata(); $issue_charset = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $issue_collation = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $issue_columntype = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE; $issue_surpluskey = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY; $issue_missingkey = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY; $issue_columns = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS; $issue_unique = PhabricatorConfigStorageSchema::ISSUE_UNIQUE; $issue_longkey = PhabricatorConfigStorageSchema::ISSUE_LONGKEY; $issue_auto = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT; $adjustments = array(); foreach ($comp->getDatabases() as $database_name => $database) { $expect_database = $expect->getDatabase($database_name); $actual_database = $actual->getDatabase($database_name); if (!$expect_database || !$actual_database) { // If there's a real issue here, skip this stuff. continue; } $issues = array(); if ($database->hasIssue($issue_charset)) { $issues[] = $issue_charset; } if ($database->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($issues) { $adjustments[] = array( 'kind' => 'database', 'database' => $database_name, 'issues' => $issues, 'charset' => $expect_database->getCharacterSet(), 'collation' => $expect_database->getCollation(), ); } foreach ($database->getTables() as $table_name => $table) { $expect_table = $expect_database->getTable($table_name); $actual_table = $actual_database->getTable($table_name); if (!$expect_table || !$actual_table) { continue; } $issues = array(); if ($table->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($issues) { $adjustments[] = array( 'kind' => 'table', 'database' => $database_name, 'table' => $table_name, 'issues' => $issues, 'collation' => $expect_table->getCollation(), ); } foreach ($table->getColumns() as $column_name => $column) { $expect_column = $expect_table->getColumn($column_name); $actual_column = $actual_table->getColumn($column_name); if (!$expect_column || !$actual_column) { continue; } $issues = array(); if ($column->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($column->hasIssue($issue_charset)) { $issues[] = $issue_charset; } if ($column->hasIssue($issue_columntype)) { $issues[] = $issue_columntype; } if ($column->hasIssue($issue_auto)) { $issues[] = $issue_auto; } if ($issues) { if ($expect_column->getCharacterSet() === null) { // For non-text columns, we won't be specifying a collation or // character set. $charset = null; $collation = null; } else { $charset = $expect_column->getCharacterSet(); $collation = $expect_column->getCollation(); } $adjustment = array( 'kind' => 'column', 'database' => $database_name, 'table' => $table_name, 'name' => $column_name, 'issues' => $issues, 'collation' => $collation, 'charset' => $charset, 'type' => $expect_column->getColumnType(), // NOTE: We don't adjust column nullability because it is // dangerous, so always use the current nullability. 'nullable' => $actual_column->getNullable(), // NOTE: This always stores the current value, because we have // to make these updates separately. 'is_auto' => $actual_column->getAutoIncrement(), ); if ($column->hasIssue($issue_auto)) { $adjustment['auto'] = $expect_column->getAutoIncrement(); } $adjustments[] = $adjustment; } } foreach ($table->getKeys() as $key_name => $key) { $expect_key = $expect_table->getKey($key_name); $actual_key = $actual_table->getKey($key_name); $issues = array(); $keep_key = true; if ($key->hasIssue($issue_surpluskey)) { $issues[] = $issue_surpluskey; $keep_key = false; } if ($key->hasIssue($issue_missingkey)) { $issues[] = $issue_missingkey; } if ($key->hasIssue($issue_columns)) { $issues[] = $issue_columns; } if ($key->hasIssue($issue_unique)) { $issues[] = $issue_unique; } // NOTE: We can't really fix this, per se, but we may need to remove // the key to change the column type. In the best case, the new // column type won't be overlong and recreating the key really will // fix the issue. In the worst case, we get the right column type and // lose the key, which is still better than retaining the key having // the wrong column type. if ($key->hasIssue($issue_longkey)) { $issues[] = $issue_longkey; } if ($issues) { $adjustment = array( 'kind' => 'key', 'database' => $database_name, 'table' => $table_name, 'name' => $key_name, 'issues' => $issues, 'exists' => (bool)$actual_key, 'keep' => $keep_key, ); if ($keep_key) { $adjustment += array( 'columns' => $expect_key->getColumnNames(), 'unique' => $expect_key->getUnique(), 'indexType' => $expect_key->getIndexType(), ); } $adjustments[] = $adjustment; } } } } return $adjustments; } }