2014-09-18 17:22:18 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
final class PhabricatorConfigSchemaQuery extends Phobject {
|
|
|
|
|
|
|
|
private $api;
|
|
|
|
|
|
|
|
public function setAPI(PhabricatorStorageManagementAPI $api) {
|
|
|
|
$this->api = $api;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getAPI() {
|
|
|
|
if (!$this->api) {
|
|
|
|
throw new Exception(pht('Call setAPI() before issuing a query!'));
|
|
|
|
}
|
|
|
|
return $this->api;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getConn() {
|
|
|
|
return $this->getAPI()->getConn(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getDatabaseNames() {
|
|
|
|
$api = $this->getAPI();
|
|
|
|
$patches = PhabricatorSQLPatchList::buildAllPatches();
|
|
|
|
return $api->getDatabaseList(
|
|
|
|
$patches,
|
|
|
|
$only_living = true);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function loadActualSchema() {
|
|
|
|
$databases = $this->getDatabaseNames();
|
|
|
|
|
|
|
|
$conn = $this->getConn();
|
|
|
|
$tables = queryfx_all(
|
|
|
|
$conn,
|
|
|
|
'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION
|
|
|
|
FROM INFORMATION_SCHEMA.TABLES
|
|
|
|
WHERE TABLE_SCHEMA IN (%Ls)',
|
|
|
|
$databases);
|
|
|
|
|
|
|
|
$database_info = queryfx_all(
|
|
|
|
$conn,
|
|
|
|
'SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
|
|
|
|
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
|
|
WHERE SCHEMA_NAME IN (%Ls)',
|
|
|
|
$databases);
|
|
|
|
$database_info = ipull($database_info, null, 'SCHEMA_NAME');
|
|
|
|
|
2014-09-18 17:32:21 +02:00
|
|
|
$sql = array();
|
|
|
|
foreach ($tables as $table) {
|
|
|
|
$sql[] = qsprintf(
|
|
|
|
$conn,
|
|
|
|
'(TABLE_SCHEMA = %s AND TABLE_NAME = %s)',
|
|
|
|
$table['TABLE_SCHEMA'],
|
|
|
|
$table['TABLE_NAME']);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($sql) {
|
|
|
|
$column_info = queryfx_all(
|
|
|
|
$conn,
|
|
|
|
'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME,
|
|
|
|
COLLATION_NAME, COLUMN_TYPE, IS_NULLABLE
|
|
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
|
|
WHERE (%Q)',
|
|
|
|
'('.implode(') OR (', $sql).')');
|
|
|
|
$column_info = igroup($column_info, 'TABLE_SCHEMA');
|
|
|
|
} else {
|
|
|
|
$column_info = array();
|
|
|
|
}
|
|
|
|
|
2014-09-19 20:46:30 +02:00
|
|
|
// NOTE: Tables like KEY_COLUMN_USAGE and TABLE_CONSTRAINTS only contain
|
|
|
|
// primary, unique, and foreign keys, so we can't use them here. We pull
|
|
|
|
// indexes later on using SHOW INDEXES.
|
2014-09-18 17:32:21 +02:00
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
$server_schema = new PhabricatorConfigServerSchema();
|
|
|
|
|
|
|
|
$tables = igroup($tables, 'TABLE_SCHEMA');
|
|
|
|
foreach ($tables as $database_name => $database_tables) {
|
|
|
|
$info = $database_info[$database_name];
|
|
|
|
|
|
|
|
$database_schema = id(new PhabricatorConfigDatabaseSchema())
|
|
|
|
->setName($database_name)
|
|
|
|
->setCharacterSet($info['DEFAULT_CHARACTER_SET_NAME'])
|
|
|
|
->setCollation($info['DEFAULT_COLLATION_NAME']);
|
|
|
|
|
2014-09-18 17:32:21 +02:00
|
|
|
$database_column_info = idx($column_info, $database_name, array());
|
|
|
|
$database_column_info = igroup($database_column_info, 'TABLE_NAME');
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
foreach ($database_tables as $table) {
|
|
|
|
$table_name = $table['TABLE_NAME'];
|
|
|
|
|
|
|
|
$table_schema = id(new PhabricatorConfigTableSchema())
|
|
|
|
->setName($table_name)
|
|
|
|
->setCollation($table['TABLE_COLLATION']);
|
|
|
|
|
2014-09-18 17:32:21 +02:00
|
|
|
$columns = idx($database_column_info, $table_name, array());
|
2014-09-18 17:22:18 +02:00
|
|
|
foreach ($columns as $column) {
|
|
|
|
$column_schema = id(new PhabricatorConfigColumnSchema())
|
|
|
|
->setName($column['COLUMN_NAME'])
|
|
|
|
->setCharacterSet($column['CHARACTER_SET_NAME'])
|
|
|
|
->setCollation($column['COLLATION_NAME'])
|
2014-09-18 17:32:21 +02:00
|
|
|
->setColumnType($column['COLUMN_TYPE'])
|
|
|
|
->setNullable($column['IS_NULLABLE'] == 'YES');
|
2014-09-18 17:22:18 +02:00
|
|
|
|
|
|
|
$table_schema->addColumn($column_schema);
|
|
|
|
}
|
|
|
|
|
2014-09-19 20:46:30 +02:00
|
|
|
$key_parts = queryfx_all(
|
|
|
|
$conn,
|
|
|
|
'SHOW INDEXES FROM %T.%T',
|
|
|
|
$database_name,
|
|
|
|
$table_name);
|
|
|
|
$keys = igroup($key_parts, 'Key_name');
|
2014-09-18 17:32:21 +02:00
|
|
|
foreach ($keys as $key_name => $key_pieces) {
|
2014-09-19 20:46:30 +02:00
|
|
|
$key_pieces = isort($key_pieces, 'Seq_in_index');
|
|
|
|
$head = head($key_pieces);
|
2014-09-19 20:46:44 +02:00
|
|
|
|
|
|
|
// This handles string indexes which index only a prefix of a field.
|
|
|
|
$column_names = array();
|
|
|
|
foreach ($key_pieces as $piece) {
|
|
|
|
$name = $piece['Column_name'];
|
|
|
|
if ($piece['Sub_part']) {
|
|
|
|
$name = $name.'('.$piece['Sub_part'].')';
|
|
|
|
}
|
|
|
|
$column_names[] = $name;
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:32:21 +02:00
|
|
|
$key_schema = id(new PhabricatorConfigKeySchema())
|
|
|
|
->setName($key_name)
|
2014-09-19 20:46:44 +02:00
|
|
|
->setColumnNames($column_names)
|
2014-09-19 20:46:30 +02:00
|
|
|
->setUnique(!$head['Non_unique']);
|
2014-09-18 17:32:21 +02:00
|
|
|
|
|
|
|
$table_schema->addKey($key_schema);
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
$database_schema->addTable($table_schema);
|
|
|
|
}
|
|
|
|
|
|
|
|
$server_schema->addDatabase($database_schema);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $server_schema;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function loadExpectedSchema() {
|
|
|
|
$databases = $this->getDatabaseNames();
|
|
|
|
|
|
|
|
$api = $this->getAPI();
|
|
|
|
|
|
|
|
if ($api->isCharacterSetAvailable('utf8mb4')) {
|
|
|
|
// If utf8mb4 is available, we use it with the utf8mb4_unicode_ci
|
|
|
|
// collation. This is most correct, and will sort properly.
|
|
|
|
|
|
|
|
$utf8_charset = 'utf8mb4';
|
2014-09-18 17:25:34 +02:00
|
|
|
$utf8_collation = 'utf8mb4_unicode_ci';
|
2014-09-18 17:22:18 +02:00
|
|
|
} else {
|
|
|
|
// If utf8mb4 is not available, we use binary. This allows us to store
|
|
|
|
// 4-byte unicode characters. This has some tradeoffs:
|
|
|
|
//
|
|
|
|
// Unicode characters won't sort correctly. There's nothing we can do
|
|
|
|
// about this while still supporting 4-byte characters.
|
|
|
|
//
|
|
|
|
// It's possible that strings will be truncated in the middle of a
|
|
|
|
// character on insert. We encourage users to set STRICT_ALL_TABLES
|
|
|
|
// to prevent this.
|
|
|
|
|
|
|
|
$utf8_charset = 'binary';
|
2014-09-18 17:25:34 +02:00
|
|
|
$utf8_collation = 'binary';
|
2014-09-18 17:22:18 +02:00
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:54 +02:00
|
|
|
$specs = id(new PhutilSymbolLoader())
|
|
|
|
->setAncestorClass('PhabricatorConfigSchemaSpec')
|
|
|
|
->loadObjects();
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
$server_schema = new PhabricatorConfigServerSchema();
|
2014-09-18 17:22:54 +02:00
|
|
|
foreach ($specs as $spec) {
|
2014-09-18 17:25:34 +02:00
|
|
|
$spec
|
|
|
|
->setUTF8Collation($utf8_collation)
|
|
|
|
->setUTF8Charset($utf8_charset)
|
|
|
|
->setServer($server_schema)
|
|
|
|
->buildSchemata($server_schema);
|
2014-09-18 17:22:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $server_schema;
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:54 +02:00
|
|
|
public function buildComparisonSchema(
|
|
|
|
PhabricatorConfigServerSchema $expect,
|
|
|
|
PhabricatorConfigServerSchema $actual) {
|
|
|
|
|
|
|
|
$comp_server = $actual->newEmptyClone();
|
|
|
|
|
|
|
|
$all_databases = $actual->getDatabases() + $expect->getDatabases();
|
|
|
|
foreach ($all_databases as $database_name => $database_template) {
|
|
|
|
$actual_database = $actual->getDatabase($database_name);
|
|
|
|
$expect_database = $expect->getDatabase($database_name);
|
|
|
|
|
|
|
|
$issues = $this->compareSchemata($expect_database, $actual_database);
|
|
|
|
|
|
|
|
$comp_database = $database_template->newEmptyClone()
|
|
|
|
->setIssues($issues);
|
|
|
|
|
|
|
|
if (!$actual_database) {
|
|
|
|
$actual_database = $expect_database->newEmptyClone();
|
|
|
|
}
|
|
|
|
if (!$expect_database) {
|
|
|
|
$expect_database = $actual_database->newEmptyClone();
|
|
|
|
}
|
|
|
|
|
|
|
|
$all_tables =
|
|
|
|
$actual_database->getTables() +
|
|
|
|
$expect_database->getTables();
|
|
|
|
foreach ($all_tables as $table_name => $table_template) {
|
|
|
|
$actual_table = $actual_database->getTable($table_name);
|
|
|
|
$expect_table = $expect_database->getTable($table_name);
|
|
|
|
|
|
|
|
$issues = $this->compareSchemata($expect_table, $actual_table);
|
|
|
|
|
|
|
|
$comp_table = $table_template->newEmptyClone()
|
|
|
|
->setIssues($issues);
|
|
|
|
|
|
|
|
if (!$actual_table) {
|
|
|
|
$actual_table = $expect_table->newEmptyClone();
|
|
|
|
}
|
|
|
|
if (!$expect_table) {
|
|
|
|
$expect_table = $actual_table->newEmptyClone();
|
|
|
|
}
|
|
|
|
|
|
|
|
$all_columns =
|
|
|
|
$actual_table->getColumns() +
|
|
|
|
$expect_table->getColumns();
|
|
|
|
foreach ($all_columns as $column_name => $column_template) {
|
|
|
|
$actual_column = $actual_table->getColumn($column_name);
|
|
|
|
$expect_column = $expect_table->getColumn($column_name);
|
|
|
|
|
|
|
|
$issues = $this->compareSchemata($expect_column, $actual_column);
|
|
|
|
|
|
|
|
$comp_column = $column_template->newEmptyClone()
|
|
|
|
->setIssues($issues);
|
|
|
|
|
|
|
|
$comp_table->addColumn($comp_column);
|
|
|
|
}
|
2014-09-18 17:32:21 +02:00
|
|
|
|
|
|
|
$all_keys =
|
|
|
|
$actual_table->getKeys() +
|
|
|
|
$expect_table->getKeys();
|
|
|
|
foreach ($all_keys as $key_name => $key_template) {
|
|
|
|
$actual_key = $actual_table->getKey($key_name);
|
|
|
|
$expect_key = $expect_table->getKey($key_name);
|
|
|
|
|
|
|
|
$issues = $this->compareSchemata($expect_key, $actual_key);
|
|
|
|
|
|
|
|
$comp_key = $key_template->newEmptyClone()
|
|
|
|
->setIssues($issues);
|
|
|
|
|
|
|
|
$comp_table->addKey($comp_key);
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:54 +02:00
|
|
|
$comp_database->addTable($comp_table);
|
|
|
|
}
|
|
|
|
$comp_server->addDatabase($comp_database);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $comp_server;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function compareSchemata(
|
|
|
|
PhabricatorConfigStorageSchema $expect = null,
|
|
|
|
PhabricatorConfigStorageSchema $actual = null) {
|
|
|
|
|
Generate expected schemata for User/People tables
Summary:
Ref T1191. Some notes here:
- Drops the old LDAP and OAuth info tables. These were migrated to the ExternalAccount table a very long time ago.
- Separates surplus/missing keys from other types of surplus/missing things. In the long run, my plan is to have only two notice levels:
- Error: something we can't fix (missing database, table, or column; overlong key).
- Warning: something we can fix (surplus anything, missing key, bad column type, bad key columns, bad uniqueness, bad collation or charset).
- For now, retaining three levels is helpful in generating all the expected scheamta.
Test Plan:
- Saw ~200 issues resolve, leaving ~1,300.
- Grepped for removed tables.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T1191
Differential Revision: https://secure.phabricator.com/D10580
2014-10-01 16:36:47 +02:00
|
|
|
$expect_is_key = ($expect instanceof PhabricatorConfigKeySchema);
|
|
|
|
$actual_is_key = ($actual instanceof PhabricatorConfigKeySchema);
|
|
|
|
|
|
|
|
if ($expect_is_key || $actual_is_key) {
|
|
|
|
$missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY;
|
|
|
|
$surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY;
|
|
|
|
} else {
|
|
|
|
$missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSING;
|
|
|
|
$surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUS;
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:54 +02:00
|
|
|
if (!$expect && !$actual) {
|
|
|
|
throw new Exception(pht('Can not compare two missing schemata!'));
|
|
|
|
} else if ($expect && !$actual) {
|
Generate expected schemata for User/People tables
Summary:
Ref T1191. Some notes here:
- Drops the old LDAP and OAuth info tables. These were migrated to the ExternalAccount table a very long time ago.
- Separates surplus/missing keys from other types of surplus/missing things. In the long run, my plan is to have only two notice levels:
- Error: something we can't fix (missing database, table, or column; overlong key).
- Warning: something we can fix (surplus anything, missing key, bad column type, bad key columns, bad uniqueness, bad collation or charset).
- For now, retaining three levels is helpful in generating all the expected scheamta.
Test Plan:
- Saw ~200 issues resolve, leaving ~1,300.
- Grepped for removed tables.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T1191
Differential Revision: https://secure.phabricator.com/D10580
2014-10-01 16:36:47 +02:00
|
|
|
$issues = array($missing_issue);
|
2014-09-18 17:22:54 +02:00
|
|
|
} else if ($actual && !$expect) {
|
Generate expected schemata for User/People tables
Summary:
Ref T1191. Some notes here:
- Drops the old LDAP and OAuth info tables. These were migrated to the ExternalAccount table a very long time ago.
- Separates surplus/missing keys from other types of surplus/missing things. In the long run, my plan is to have only two notice levels:
- Error: something we can't fix (missing database, table, or column; overlong key).
- Warning: something we can fix (surplus anything, missing key, bad column type, bad key columns, bad uniqueness, bad collation or charset).
- For now, retaining three levels is helpful in generating all the expected scheamta.
Test Plan:
- Saw ~200 issues resolve, leaving ~1,300.
- Grepped for removed tables.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T1191
Differential Revision: https://secure.phabricator.com/D10580
2014-10-01 16:36:47 +02:00
|
|
|
$issues = array($surplus_issue);
|
2014-09-18 17:22:54 +02:00
|
|
|
} else {
|
|
|
|
$issues = $actual->compareTo($expect);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $issues;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
}
|