[DATABASE] Fix MariaDB schema verification

This commit is contained in:
Alexei Sorokin 2020-06-28 20:05:11 +03:00
parent 737f3eb553
commit b924c180ae
5 changed files with 141 additions and 97 deletions

View File

@ -30,7 +30,7 @@ class Foreign_link extends Managed_DataObject
return array(
'fields' => array(
'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'),
'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'),
@ -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)) {
throw new ServerException('Empty user_id or service for Foreign_link::getByUserID');
@ -71,7 +71,7 @@ class Foreign_link extends Managed_DataObject
return $flink;
}
static function getByForeignID($foreign_id, $service)
public static function getByForeignID($foreign_id, $service)
{
if (empty($foreign_id) || empty($service)) {
throw new ServerException('Empty foreign_id or service for Foreign_link::getByForeignID');
@ -89,7 +89,7 @@ class Foreign_link extends Managed_DataObject
return $flink;
}
function set_flags($noticesend, $noticerecv, $replysync, $repeatsync, $friendsync)
public function set_flags($noticesend, $noticerecv, $replysync, $repeatsync, $friendsync)
{
if ($noticesend) {
$this->noticesync |= FOREIGN_NOTICE_SEND;
@ -125,7 +125,7 @@ class Foreign_link extends Managed_DataObject
}
// Convenience methods
function getForeignUser()
public function getForeignUser()
{
$fuser = new Foreign_user();
$fuser->service = $this->service;
@ -140,29 +140,30 @@ class Foreign_link extends Managed_DataObject
return $fuser;
}
function getUser()
public function getUser()
{
return Profile::getByID($this->user_id)->getUser();
}
function getProfile()
public function getProfile()
{
return Profile::getByID($this->user_id);
}
// Make sure we only ever delete one record at a time
function safeDelete()
public function safeDelete()
{
if (!empty($this->user_id)
&& !empty($this->foreign_id)
&& !empty($this->service))
{
&& !empty($this->service)) {
return $this->delete();
} else {
common_debug(LOG_WARNING,
common_debug(
LOG_WARNING,
'Foreign_link::safeDelete() tried to delete a '
. 'Foreign_link without a fully specified compound key: '
. var_export($this, true));
. var_export($this, true)
);
return false;
}
}

View File

@ -83,39 +83,53 @@ class MysqlSchema extends Schema
$name = $row['COLUMN_NAME'];
$field = [];
// warning -- 'unsigned' attr on numbers isn't given in DATA_TYPE and friends.
// It is stuck in on COLUMN_TYPE though (eg 'bigint(20) unsigned')
$field['type'] = $type = $row['DATA_TYPE'];
$type = $field['type'] = $row['DATA_TYPE'];
if ($type == 'char' || $type == 'varchar') {
if ($row['CHARACTER_MAXIMUM_LENGTH'] !== null) {
$field['length'] = intval($row['CHARACTER_MAXIMUM_LENGTH']);
switch ($type) {
case 'char':
case 'varchar':
if (!is_null($row['CHARACTER_MAXIMUM_LENGTH'])) {
$field['length'] = (int) $row['CHARACTER_MAXIMUM_LENGTH'];
}
}
if ($type == 'decimal') {
break;
case 'decimal':
// Other int types may report these values, but they're irrelevant.
// Just ignore them!
if ($row['NUMERIC_PRECISION'] !== null) {
$field['precision'] = intval($row['NUMERIC_PRECISION']);
if (!is_null($row['NUMERIC_PRECISION'])) {
$field['precision'] = (int) $row['NUMERIC_PRECISION'];
}
if ($row['NUMERIC_SCALE'] !== null) {
$field['scale'] = intval($row['NUMERIC_SCALE']);
if (!is_null($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') {
$field['not null'] = true;
}
if ($row['COLUMN_DEFAULT'] !== null) {
// Hack for timestamp columns
if ($row['COLUMN_DEFAULT'] === 'current_timestamp()') {
// skip timestamp columns as they get a CURRENT_TIMESTAMP default implicitly
$col_default = $row['COLUMN_DEFAULT'];
if (!is_null($col_default) && $col_default !== 'NULL') {
if ($this->isNumericType($field)) {
$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') {
$field['default'] = 'CURRENT_TIMESTAMP';
}
} elseif ($this->isNumericType($type)) {
$field['default'] = intval($row['COLUMN_DEFAULT']);
} 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) {
@ -135,11 +149,11 @@ class MysqlSchema extends Schema
// ^ ...... how to specify?
}
/* @fixme check against defaults?
if ($row['CHARACTER_SET_NAME'] !== null) {
$def['charset'] = $row['CHARACTER_SET_NAME'];
$def['collate'] = $row['COLLATION_NAME'];
}*/
$table_props = $this->getTableProperties($table, ['TABLE_COLLATION']);
$collate = $row['COLLATION_NAME'];
if (!empty($collate) && $collate !== $table_props['TABLE_COLLATION']) {
$field['collate'] = $collate;
}
$def['fields'][$name] = $field;
}
@ -389,7 +403,7 @@ class MysqlSchema extends Schema
public function appendAlterExtras(array &$phrase, $tableName, array $def)
{
// 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?
$oldProps = $this->getTableProperties($tableName, ['ENGINE', 'TABLE_COLLATION']);
$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?
* @param array $cd
* @return bool
*/
private function _isString(array $cd)
private function isStringType(array $cd): bool
{
$strings = ['char', 'varchar', 'text'];
return in_array(strtolower($cd['type']), $strings);
@ -448,7 +474,6 @@ class MysqlSchema extends Schema
{
$map = [
'integer' => 'int',
'bool' => 'tinyint',
'numeric' => 'decimal',
];
@ -476,15 +501,13 @@ class MysqlSchema extends Schema
public function typeAndSize(string $name, array $column)
{
if ($column['type'] === 'enum') {
$vals = [];
foreach ($column['enum'] as &$val) {
$vals[] = "'" . $val . "'";
$vals[] = "'{$val}'";
}
return 'enum(' . implode(',', $vals) . ')';
} elseif ($this->_isString($column)) {
} elseif ($this->isStringType($column)) {
$col = parent::typeAndSize($name, $column);
if (!empty($column['charset'])) {
$col .= ' CHARSET ' . $column['charset'];
}
if (!empty($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,
* or type variants that we wouldn't get back from getTableDef().
*
* @param string $tableName
* @param array $tableDef
* @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) {
if ($col['type'] == 'serial') {
switch ($col['type']) {
case 'serial':
$col['type'] = 'int';
$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);

View File

@ -110,8 +110,8 @@ class PgsqlSchema extends Schema
}
if ($row['column_default'] !== null) {
$field['default'] = $row['column_default'];
if ($this->isNumericType($type)) {
$field['default'] = intval($field['default']);
if ($this->isNumericType($field)) {
$field['default'] = (int) $field['default'];
}
}
@ -266,6 +266,12 @@ class PgsqlSchema extends Schema
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
* altering a column.
@ -395,11 +401,14 @@ class PgsqlSchema extends Schema
* This lets us strip out unsupported things like comments, foreign keys,
* or type variants that we wouldn't get back from getTableDef().
*
* @param string $tableName
* @param array $tableDef
* @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) {
// No convenient support for field descriptions
unset($col['description']);
@ -409,22 +418,19 @@ class PgsqlSchema extends Schema
$col['type'] = 'int';
$col['auto_increment'] = true;
break;
case 'timestamp':
// FIXME: ON UPDATE CURRENT_TIMESTAMP
if (!array_key_exists('default', $col)) {
$col['default'] = 'CURRENT_TIMESTAMP';
}
// no break
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') {
$col['default'] = null;
$col['not null'] = false;
}
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);

View File

@ -139,7 +139,7 @@ class Schema
public function buildCreateTable($name, $def)
{
$def = $this->validateDef($name, $def);
$def = $this->filterDef($def);
$def = $this->filterDef($name, $def);
$sql = [];
foreach ($def['fields'] as $col => $colDef) {
@ -572,10 +572,10 @@ class Schema
// Filter the DB-independent table definition to match the current
// database engine's features and limitations.
$def = $this->validateDef($tableName, $def);
$def = $this->filterDef($def);
$def = $this->filterDef($tableName, $def);
$statements = [];
$fields = $this->diffArrays($old, $def, 'fields', [$this, 'columnsEqual']);
$fields = $this->diffArrays($old, $def, 'fields');
$uniques = $this->diffArrays($old, $def, 'unique keys');
$indexes = $this->diffArrays($old, $def, 'indexes');
$foreign = $this->diffArrays($old, $def, 'foreign keys');
@ -813,20 +813,6 @@ class Schema
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
* ColumnDef objects.
@ -886,9 +872,11 @@ class Schema
if (isset($cd['default'])) {
$line[] = 'default';
$line[] = $this->quoteDefaultValue($cd);
} elseif (!empty($cd['not null'])) {
// Can't have both not null AND default!
$line[] = 'not null';
}
if (!empty($cd['not null'])) {
$line[] = 'NOT NULL';
} else {
$line[] = 'NULL';
}
return implode(' ', $line);
@ -1004,11 +992,21 @@ class Schema
* This lets us strip out unsupported things like comments, foreign keys,
* or type variants that we wouldn't get back from getTableDef().
*
* @param string $tableName
* @param array $tableDef
* @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;
}
@ -1037,13 +1035,6 @@ class Schema
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
*

View File

@ -61,7 +61,7 @@ class Activitypub_rsa extends Managed_DataObject
],
'primary key' => ['profile_id'],
'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->profile_id = $profile->getID();
$apRSA->public_key = $public_key;
$apRSA->modified = common_sql_now();
$apRSA->created = common_sql_now();
if (!$apRSA->update()) {
$apRSA->insert();
}