[DATABASE] Fix MariaDB schema verification

This commit is contained in:
Alexei Sorokin 2020-06-28 20:05:11 +03:00 committed by Diogo Peralta Cordeiro
parent 32a7cd6458
commit eb993df072
5 changed files with 141 additions and 97 deletions

View File

@ -30,7 +30,7 @@ class Foreign_link extends Managed_DataObject
return array( return array(
'fields' => array( 'fields' => array(
'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'link to user on this system, if exists'), 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'link to user on this system, if exists'),
'foreign_id' => array('type' => 'int', 'size' => 'big', 'unsigned' => true, 'not null' => true, 'description' => 'link to user on foreign service, if exists'), 'foreign_id' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'link to user on foreign service, if exists'),
'service' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to service'), 'service' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to service'),
'credentials' => array('type' => 'varchar', 'length' => 191, 'description' => 'authc credentials, typically a password'), 'credentials' => array('type' => 'varchar', 'length' => 191, 'description' => 'authc credentials, typically a password'),
'noticesync' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 1, 'description' => 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies'), 'noticesync' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 1, 'description' => 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies'),
@ -53,7 +53,7 @@ class Foreign_link extends Managed_DataObject
); );
} }
static function getByUserID($user_id, $service) public static function getByUserID($user_id, $service)
{ {
if (empty($user_id) || empty($service)) { if (empty($user_id) || empty($service)) {
throw new ServerException('Empty user_id or service for Foreign_link::getByUserID'); throw new ServerException('Empty user_id or service for Foreign_link::getByUserID');
@ -71,7 +71,7 @@ class Foreign_link extends Managed_DataObject
return $flink; return $flink;
} }
static function getByForeignID($foreign_id, $service) public static function getByForeignID($foreign_id, $service)
{ {
if (empty($foreign_id) || empty($service)) { if (empty($foreign_id) || empty($service)) {
throw new ServerException('Empty foreign_id or service for Foreign_link::getByForeignID'); throw new ServerException('Empty foreign_id or service for Foreign_link::getByForeignID');
@ -89,7 +89,7 @@ class Foreign_link extends Managed_DataObject
return $flink; return $flink;
} }
function set_flags($noticesend, $noticerecv, $replysync, $repeatsync, $friendsync) public function set_flags($noticesend, $noticerecv, $replysync, $repeatsync, $friendsync)
{ {
if ($noticesend) { if ($noticesend) {
$this->noticesync |= FOREIGN_NOTICE_SEND; $this->noticesync |= FOREIGN_NOTICE_SEND;
@ -125,7 +125,7 @@ class Foreign_link extends Managed_DataObject
} }
// Convenience methods // Convenience methods
function getForeignUser() public function getForeignUser()
{ {
$fuser = new Foreign_user(); $fuser = new Foreign_user();
$fuser->service = $this->service; $fuser->service = $this->service;
@ -140,29 +140,30 @@ class Foreign_link extends Managed_DataObject
return $fuser; return $fuser;
} }
function getUser() public function getUser()
{ {
return Profile::getByID($this->user_id)->getUser(); return Profile::getByID($this->user_id)->getUser();
} }
function getProfile() public function getProfile()
{ {
return Profile::getByID($this->user_id); return Profile::getByID($this->user_id);
} }
// Make sure we only ever delete one record at a time // Make sure we only ever delete one record at a time
function safeDelete() public function safeDelete()
{ {
if (!empty($this->user_id) if (!empty($this->user_id)
&& !empty($this->foreign_id) && !empty($this->foreign_id)
&& !empty($this->service)) && !empty($this->service)) {
{
return $this->delete(); return $this->delete();
} else { } else {
common_debug(LOG_WARNING, common_debug(
LOG_WARNING,
'Foreign_link::safeDelete() tried to delete a ' 'Foreign_link::safeDelete() tried to delete a '
. 'Foreign_link without a fully specified compound key: ' . 'Foreign_link without a fully specified compound key: '
. var_export($this, true)); . var_export($this, true)
);
return false; return false;
} }
} }

View File

@ -83,39 +83,53 @@ class MysqlSchema extends Schema
$name = $row['COLUMN_NAME']; $name = $row['COLUMN_NAME'];
$field = []; $field = [];
// warning -- 'unsigned' attr on numbers isn't given in DATA_TYPE and friends. $type = $field['type'] = $row['DATA_TYPE'];
// It is stuck in on COLUMN_TYPE though (eg 'bigint(20) unsigned')
$field['type'] = $type = $row['DATA_TYPE'];
if ($type == 'char' || $type == 'varchar') { switch ($type) {
if ($row['CHARACTER_MAXIMUM_LENGTH'] !== null) { case 'char':
$field['length'] = intval($row['CHARACTER_MAXIMUM_LENGTH']); case 'varchar':
if (!is_null($row['CHARACTER_MAXIMUM_LENGTH'])) {
$field['length'] = (int) $row['CHARACTER_MAXIMUM_LENGTH'];
} }
} break;
if ($type == 'decimal') { case 'decimal':
// Other int types may report these values, but they're irrelevant. // Other int types may report these values, but they're irrelevant.
// Just ignore them! // Just ignore them!
if ($row['NUMERIC_PRECISION'] !== null) { if (!is_null($row['NUMERIC_PRECISION'])) {
$field['precision'] = intval($row['NUMERIC_PRECISION']); $field['precision'] = (int) $row['NUMERIC_PRECISION'];
} }
if ($row['NUMERIC_SCALE'] !== null) { if (!is_null($row['NUMERIC_SCALE'])) {
$field['scale'] = intval($row['NUMERIC_SCALE']); $field['scale'] = (int) $row['NUMERIC_SCALE'];
} }
break;
case 'enum':
$enum = preg_replace("/^enum\('(.+)'\)$/", '\1', $row['COLUMN_TYPE']);
$field['enum'] = explode("','", $enum);
break;
} }
if ($row['IS_NULLABLE'] == 'NO') { if ($row['IS_NULLABLE'] == 'NO') {
$field['not null'] = true; $field['not null'] = true;
} }
if ($row['COLUMN_DEFAULT'] !== null) { $col_default = $row['COLUMN_DEFAULT'];
// Hack for timestamp columns if (!is_null($col_default) && $col_default !== 'NULL') {
if ($row['COLUMN_DEFAULT'] === 'current_timestamp()') { if ($this->isNumericType($field)) {
// skip timestamp columns as they get a CURRENT_TIMESTAMP default implicitly $field['default'] = (int) $col_default;
} elseif ($col_default === 'CURRENT_TIMESTAMP'
|| $col_default === 'current_timestamp()') {
// A hack for "datetime" fields
// Skip "timestamp" as they get a CURRENT_TIMESTAMP default implicitly
if ($type !== 'timestamp') { if ($type !== 'timestamp') {
$field['default'] = 'CURRENT_TIMESTAMP'; $field['default'] = 'CURRENT_TIMESTAMP';
} }
} elseif ($this->isNumericType($type)) {
$field['default'] = intval($row['COLUMN_DEFAULT']);
} else { } else {
$field['default'] = $row['COLUMN_DEFAULT']; $match = "/^'(.*)'$/";
if (preg_match($match, $col_default)) {
$field['default'] = preg_replace($match, '\1', $col_default);
} else {
$field['default'] = $col_default;
}
} }
} }
if ($row['COLUMN_KEY'] !== null) { if ($row['COLUMN_KEY'] !== null) {
@ -135,11 +149,11 @@ class MysqlSchema extends Schema
// ^ ...... how to specify? // ^ ...... how to specify?
} }
/* @fixme check against defaults? $table_props = $this->getTableProperties($table, ['TABLE_COLLATION']);
if ($row['CHARACTER_SET_NAME'] !== null) { $collate = $row['COLLATION_NAME'];
$def['charset'] = $row['CHARACTER_SET_NAME']; if (!empty($collate) && $collate !== $table_props['TABLE_COLLATION']) {
$def['collate'] = $row['COLLATION_NAME']; $field['collate'] = $collate;
}*/ }
$def['fields'][$name] = $field; $def['fields'][$name] = $field;
} }
@ -389,7 +403,7 @@ class MysqlSchema extends Schema
public function appendAlterExtras(array &$phrase, $tableName, array $def) public function appendAlterExtras(array &$phrase, $tableName, array $def)
{ {
// Check for table properties: make sure we're using a sane // Check for table properties: make sure we're using a sane
// engine type and charset/collation. // engine type and collation.
// @fixme make the default engine configurable? // @fixme make the default engine configurable?
$oldProps = $this->getTableProperties($tableName, ['ENGINE', 'TABLE_COLLATION']); $oldProps = $this->getTableProperties($tableName, ['ENGINE', 'TABLE_COLLATION']);
$engine = $this->preferredEngine($def); $engine = $this->preferredEngine($def);
@ -403,12 +417,24 @@ class MysqlSchema extends Schema
} }
} }
private function isNumericType(array $cd): bool
{
$ints = array_map(
function ($s) {
return $s . 'int';
},
['tiny', 'small', 'medium', 'big']
);
$ints = array_merge($ints, ['int', 'numeric', 'serial']);
return in_array(strtolower($cd['type']), $ints);
}
/** /**
* Is this column a string type? * Is this column a string type?
* @param array $cd * @param array $cd
* @return bool * @return bool
*/ */
private function _isString(array $cd) private function isStringType(array $cd): bool
{ {
$strings = ['char', 'varchar', 'text']; $strings = ['char', 'varchar', 'text'];
return in_array(strtolower($cd['type']), $strings); return in_array(strtolower($cd['type']), $strings);
@ -448,7 +474,6 @@ class MysqlSchema extends Schema
{ {
$map = [ $map = [
'integer' => 'int', 'integer' => 'int',
'bool' => 'tinyint',
'numeric' => 'decimal', 'numeric' => 'decimal',
]; ];
@ -476,15 +501,13 @@ class MysqlSchema extends Schema
public function typeAndSize(string $name, array $column) public function typeAndSize(string $name, array $column)
{ {
if ($column['type'] === 'enum') { if ($column['type'] === 'enum') {
$vals = [];
foreach ($column['enum'] as &$val) { foreach ($column['enum'] as &$val) {
$vals[] = "'" . $val . "'"; $vals[] = "'{$val}'";
} }
return 'enum(' . implode(',', $vals) . ')'; return 'enum(' . implode(',', $vals) . ')';
} elseif ($this->_isString($column)) { } elseif ($this->isStringType($column)) {
$col = parent::typeAndSize($name, $column); $col = parent::typeAndSize($name, $column);
if (!empty($column['charset'])) {
$col .= ' CHARSET ' . $column['charset'];
}
if (!empty($column['collate'])) { if (!empty($column['collate'])) {
$col .= ' COLLATE ' . $column['collate']; $col .= ' COLLATE ' . $column['collate'];
} }
@ -501,16 +524,39 @@ class MysqlSchema extends Schema
* This lets us strip out unsupported things like comments, foreign keys, * This lets us strip out unsupported things like comments, foreign keys,
* or type variants that we wouldn't get back from getTableDef(). * or type variants that we wouldn't get back from getTableDef().
* *
* @param string $tableName
* @param array $tableDef * @param array $tableDef
* @return array * @return array
*/ */
public function filterDef(array $tableDef) public function filterDef(string $tableName, array $tableDef)
{ {
$version = $this->conn->getVersion(); $tableDef = parent::filterDef($tableName, $tableDef);
// Get existing table collation if the table exists.
// To know if collation that's been set is unique for the table.
try {
$table_props = $this->getTableProperties($tableName, ['TABLE_COLLATION']);
$table_collate = $table_props['TABLE_COLLATION'];
} catch (SchemaTableMissingException $e) {
$table_collate = null;
}
foreach ($tableDef['fields'] as $name => &$col) { foreach ($tableDef['fields'] as $name => &$col) {
if ($col['type'] == 'serial') { switch ($col['type']) {
case 'serial':
$col['type'] = 'int'; $col['type'] = 'int';
$col['auto_increment'] = true; $col['auto_increment'] = true;
break;
case 'bool':
$col['type'] = 'int';
$col['size'] = 'tiny';
$col['default'] = (int) $col['default'];
break;
}
if (!empty($col['collate'])
&& $col['collate'] === $table_collate) {
unset($col['collate']);
} }
$col['type'] = $this->mapType($col); $col['type'] = $this->mapType($col);

View File

@ -110,8 +110,8 @@ class PgsqlSchema extends Schema
} }
if ($row['column_default'] !== null) { if ($row['column_default'] !== null) {
$field['default'] = $row['column_default']; $field['default'] = $row['column_default'];
if ($this->isNumericType($type)) { if ($this->isNumericType($field)) {
$field['default'] = intval($field['default']); $field['default'] = (int) $field['default'];
} }
} }
@ -266,6 +266,12 @@ class PgsqlSchema extends Schema
return $out; return $out;
} }
private function isNumericType(array $cd): bool
{
$ints = ['int', 'numeric', 'serial'];
return in_array(strtolower($cd['type']), $ints);
}
/** /**
* Return the proper SQL for creating or * Return the proper SQL for creating or
* altering a column. * altering a column.
@ -395,11 +401,14 @@ class PgsqlSchema extends Schema
* This lets us strip out unsupported things like comments, foreign keys, * This lets us strip out unsupported things like comments, foreign keys,
* or type variants that we wouldn't get back from getTableDef(). * or type variants that we wouldn't get back from getTableDef().
* *
* @param string $tableName
* @param array $tableDef * @param array $tableDef
* @return array * @return array
*/ */
public function filterDef(array $tableDef) public function filterDef(string $tableName, array $tableDef)
{ {
$tableDef = parent::filterDef($tableName, $tableDef);
foreach ($tableDef['fields'] as $name => &$col) { foreach ($tableDef['fields'] as $name => &$col) {
// No convenient support for field descriptions // No convenient support for field descriptions
unset($col['description']); unset($col['description']);
@ -409,22 +418,19 @@ class PgsqlSchema extends Schema
$col['type'] = 'int'; $col['type'] = 'int';
$col['auto_increment'] = true; $col['auto_increment'] = true;
break; break;
case 'timestamp':
// FIXME: ON UPDATE CURRENT_TIMESTAMP
if (!array_key_exists('default', $col)) {
$col['default'] = 'CURRENT_TIMESTAMP';
}
// no break
case 'datetime': case 'datetime':
// Replace archaic MySQL-specific zero-dates with NULL // Replace archaic MySQL-specific zero dates with NULL
if (($col['default'] ?? null) === '0000-00-00 00:00:00') { if (($col['default'] ?? null) === '0000-00-00 00:00:00') {
$col['default'] = null; $col['default'] = null;
$col['not null'] = false; $col['not null'] = false;
} }
break; break;
case 'timestamp':
// In MariaDB: If the column does not permit NULL values,
// assigning NULL (or not referencing the column at all
// when inserting) will set the column to CURRENT_TIMESTAMP
// FIXME: ON UPDATE CURRENT_TIMESTAMP
if ($col['not null'] && !isset($col['default'])) {
$col['default'] = 'CURRENT_TIMESTAMP';
}
break;
} }
$col['type'] = $this->mapType($col); $col['type'] = $this->mapType($col);

View File

@ -139,7 +139,7 @@ class Schema
public function buildCreateTable($name, $def) public function buildCreateTable($name, $def)
{ {
$def = $this->validateDef($name, $def); $def = $this->validateDef($name, $def);
$def = $this->filterDef($def); $def = $this->filterDef($name, $def);
$sql = []; $sql = [];
foreach ($def['fields'] as $col => $colDef) { foreach ($def['fields'] as $col => $colDef) {
@ -572,10 +572,10 @@ class Schema
// Filter the DB-independent table definition to match the current // Filter the DB-independent table definition to match the current
// database engine's features and limitations. // database engine's features and limitations.
$def = $this->validateDef($tableName, $def); $def = $this->validateDef($tableName, $def);
$def = $this->filterDef($def); $def = $this->filterDef($tableName, $def);
$statements = []; $statements = [];
$fields = $this->diffArrays($old, $def, 'fields', [$this, 'columnsEqual']); $fields = $this->diffArrays($old, $def, 'fields');
$uniques = $this->diffArrays($old, $def, 'unique keys'); $uniques = $this->diffArrays($old, $def, 'unique keys');
$indexes = $this->diffArrays($old, $def, 'indexes'); $indexes = $this->diffArrays($old, $def, 'indexes');
$foreign = $this->diffArrays($old, $def, 'foreign keys'); $foreign = $this->diffArrays($old, $def, 'foreign keys');
@ -813,20 +813,6 @@ class Schema
return $this->conn->quoteSmart($val); return $this->conn->quoteSmart($val);
} }
/**
* Check if two column definitions are equivalent.
* The default implementation checks _everything_ but in many cases
* you may be able to discard a bunch of equivalencies.
*
* @param array $a
* @param array $b
* @return bool
*/
public function columnsEqual(array $a, array $b)
{
return !array_diff_assoc($a, $b) && !array_diff_assoc($b, $a);
}
/** /**
* Returns the array of names from an array of * Returns the array of names from an array of
* ColumnDef objects. * ColumnDef objects.
@ -886,9 +872,11 @@ class Schema
if (isset($cd['default'])) { if (isset($cd['default'])) {
$line[] = 'default'; $line[] = 'default';
$line[] = $this->quoteDefaultValue($cd); $line[] = $this->quoteDefaultValue($cd);
} elseif (!empty($cd['not null'])) { }
// Can't have both not null AND default! if (!empty($cd['not null'])) {
$line[] = 'not null'; $line[] = 'NOT NULL';
} else {
$line[] = 'NULL';
} }
return implode(' ', $line); return implode(' ', $line);
@ -1004,11 +992,21 @@ class Schema
* This lets us strip out unsupported things like comments, foreign keys, * This lets us strip out unsupported things like comments, foreign keys,
* or type variants that we wouldn't get back from getTableDef(). * or type variants that we wouldn't get back from getTableDef().
* *
* @param string $tableName
* @param array $tableDef * @param array $tableDef
* @return array * @return array
*/ */
public function filterDef(array $tableDef) public function filterDef(string $tableName, array $tableDef)
{ {
foreach ($tableDef['fields'] as $name => &$col) {
if (array_key_exists('default', $col) && is_null($col['default'])) {
unset($col['default']);
}
if (array_key_exists('not null', $col) && $col['not null'] !== true) {
unset($col['not null']);
}
}
return $tableDef; return $tableDef;
} }
@ -1037,13 +1035,6 @@ class Schema
return $def; return $def;
} }
public function isNumericType($type)
{
$type = strtolower($type);
$known = ['int', 'serial', 'numeric'];
return in_array($type, $known);
}
/** /**
* Pull info from the query into a fun-fun array of dooooom * Pull info from the query into a fun-fun array of dooooom
* *

View File

@ -61,7 +61,7 @@ class Activitypub_rsa extends Managed_DataObject
], ],
'primary key' => ['profile_id'], 'primary key' => ['profile_id'],
'foreign keys' => [ 'foreign keys' => [
'activitypub_profile_profile_id_fkey' => ['profile', ['profile_id' => 'id']], 'activitypub_rsa_profile_id_fkey' => ['profile', ['profile_id' => 'id']],
], ],
]; ];
} }
@ -182,7 +182,7 @@ class Activitypub_rsa extends Managed_DataObject
$apRSA = new Activitypub_rsa(); $apRSA = new Activitypub_rsa();
$apRSA->profile_id = $profile->getID(); $apRSA->profile_id = $profile->getID();
$apRSA->public_key = $public_key; $apRSA->public_key = $public_key;
$apRSA->modified = common_sql_now(); $apRSA->created = common_sql_now();
if (!$apRSA->update()) { if (!$apRSA->update()) {
$apRSA->insert(); $apRSA->insert();
} }