diff --git a/lib/Db/Mysql.php b/lib/Db/Mysql.php index 1fc22d85f..efc7d987b 100644 --- a/lib/Db/Mysql.php +++ b/lib/Db/Mysql.php @@ -11,6 +11,19 @@ class Mysql extends BaseDriver protected $sqlParts = []; + /** + * @var string[] + * @internal + */ + protected $typeMap = [ + 'smallint' => 'smallInt', + 'mediumint' => 'mediumInt', + 'int' => 'int', + 'text' => 'text', + 'mediumtext' => 'mediumText', + 'longtext' => 'longText', + ]; + public function getSql($type, $sqlParts, $identifierConverter = null) { $this->aliases = []; @@ -73,20 +86,7 @@ public function getColumns(string $table, callable $phpKeyConverter = null): arr $dbColumns = $this->db->fetchAll("SHOW COLUMNS FROM $table"); foreach ($dbColumns as $dbColumn) { - $column = []; - - $column['cast'] = $this->getCastType($dbColumn['Type']); - - // Auto increment column cant have default value, so it's allow to be null - if ('YES' === $dbColumn['Null'] || false !== strpos($dbColumn['Extra'], 'auto_increment')) { - $column['nullable'] = true; - } - - [$default, $isCustom] = $this->getCustomDefault($dbColumn); - if ($isCustom) { - $column['default'] = $default; - } - + $column = $this->parseColumn($dbColumn); $name = $phpKeyConverter ? $phpKeyConverter($dbColumn['Field']) : $dbColumn['Field']; $columns[$name] = $column; } @@ -393,6 +393,144 @@ protected function generateLockSql() } } + /** + * Parse column metadata from database column information + * + * @param array $dbColumn + * @return array + */ + protected function parseColumn(array $dbColumn): array + { + $column = $this->parseColumnType($dbColumn['Type']); + + // Auto increment column cant have default value, so it's allow to be null + if ('YES' === $dbColumn['Null'] || false !== strpos($dbColumn['Extra'], 'auto_increment')) { + $column['nullable'] = true; + } + + [$default, $isCustom] = $this->getCustomDefault($dbColumn); + if ($isCustom) { + $column['default'] = $default; + } + + return $column; + } + + /** + * Parse column metadata from database column type + * + * $columnType examples: + * + * bigint(20) unsigned + * decimal(16,2) unsigned + * timestamp + * + * @param string $columnType + * @return array + * @internal + */ + protected function parseColumnType(string $columnType): array + { + [$type, $left] = explode('(', $columnType) + [1 => 0]; + $length = (int) $left; + + switch ($type) { + case 'tinyint': + return 1 === $length ? [ + 'type' => 'bool', + 'cast' => 'bool', + ] : [ + 'type' => 'tinyInt', + 'cast' => 'int', + 'unsigned' => $this->isTypeUnsigned($columnType), + ]; + + case 'smallint': + case 'mediumint': + case 'int': + return [ + 'type' => $this->typeMap[$type], + 'cast' => 'int', + 'unsigned' => $this->isTypeUnsigned($columnType), + ]; + + case 'bigint': + return [ + 'type' => 'bigInt', + 'cast' => 'intString', + 'unsigned' => $this->isTypeUnsigned($columnType), + ]; + + case 'timestamp': + case 'datetime': + return [ + 'type' => $type, + 'cast' => 'datetime', + ]; + + case 'date': + return [ + 'type' => 'date', + 'cast' => 'date', + ]; + + case 'decimal': + // $left = 16,2) unsigned + $scale = (int) explode(',', $left)[1]; + return [ + 'type' => 'decimal', + 'cast' => 'decimal', + 'unsigned' => $this->isTypeUnsigned($columnType), + 'length' => $length, + 'scale' => $scale, + ]; + + case 'json': + return [ + 'type' => 'json', + 'cast' => 'json', + 'length' => 1024 ** 3, // 1GB + ]; + + case 'char': + case 'varchar': + return [ + 'type' => 'string', + 'cast' => 'string', + 'length' => $length, + ]; + + case 'text': + case 'mediumtext': + case 'longtext': + return [ + 'type' => $this->typeMap[$type], + 'cast' => 'string', + ]; + + default: + return [ + 'type' => 'string', + 'cast' => 'string', + ]; + } + } + + /** + * @param string $columnType + * @return bool + * @internal + */ + protected function isTypeUnsigned(string $columnType): bool + { + return 'unsigned' === substr($columnType, -8); + } + + /** + * @param string $columnType + * @return string + * @deprecated + */ protected function getCastType($columnType) { $parts = explode('(', $columnType); diff --git a/tests/unit/Model/CastTraitTest.php b/tests/unit/Model/CastTraitTest.php index 6b5fe994b..639cee510 100644 --- a/tests/unit/Model/CastTraitTest.php +++ b/tests/unit/Model/CastTraitTest.php @@ -812,4 +812,149 @@ public function providerForTestSaveDecimal(): array [false, '0', '0'], ]; } + + public function testGetColumns() + { + $columns = TestCast::getColumns(); + $this->assertSame([ + 'int_column' => [ + 'type' => 'int', + 'cast' => 'int', + 'unsigned' => true, + 'nullable' => true, + ], + 'nullable_int_column' => [ + 'type' => 'int', + 'cast' => 'int', + 'unsigned' => false, + 'nullable' => true, + ], + 'nullable_default_int_column' => [ + 'type' => 'int', + 'cast' => 'int', + 'unsigned' => false, + 'nullable' => true, + 'default' => 7, + ], + 'big_int_column' => [ + 'type' => 'bigInt', + 'cast' => 'intString', + 'unsigned' => false, + ], + 'nullable_big_int_column' => [ + 'type' => 'bigInt', + 'cast' => 'intString', + 'unsigned' => false, + 'nullable' => true, + ], + 'bool_column' => [ + 'type' => 'bool', + 'cast' => 'bool', + ], + 'nullable_bool_column' => [ + 'type' => 'bool', + 'cast' => 'bool', + 'nullable' => true, + ], + 'string_column' => [ + 'type' => 'string', + 'cast' => 'string', + 'length' => 255, + ], + 'nullable_string_column' => [ + 'type' => 'string', + 'cast' => 'string', + 'length' => 255, + 'nullable' => true, + ], + 'datetime_column' => [ + 'type' => 'datetime', + 'cast' => 'datetime', + 'nullable' => true, + ], + 'nullable_datetime_column' => [ + 'type' => 'datetime', + 'cast' => 'datetime', + 'nullable' => true, + ], + 'date_column' => [ + 'type' => 'date', + 'cast' => 'date', + 'nullable' => true, + ], + 'nullable_date_column' => [ + 'type' => 'date', + 'cast' => 'date', + 'nullable' => true, + ], + 'json_column' => [ + 'type' => 'string', + 'cast' => 'array', + 'length' => 255, + 'default' => [ + ], + ], + 'nullable_json_column' => [ + 'type' => 'string', + 'cast' => 'string', + 'length' => 255, + 'nullable' => true, + ], + 'object_column' => [ + 'type' => 'string', + 'cast' => 'object', + 'length' => 255, + ], + 'nullable_object_column' => [ + 'type' => 'string', + 'cast' => 'object', + 'length' => 255, + 'nullable' => true, + ], + 'default_object_column' => [ + 'type' => 'string', + 'cast' => 'object', + 'length' => 255, + 'default' => '{"a":"c"}', + ], + 'list_column' => [ + 'type' => 'string', + 'cast' => 'list', + 'length' => 255, + 'default' => [ + ], + ], + 'nullable_list_column' => [ + 'type' => 'string', + 'cast' => 'string', + 'length' => 255, + 'nullable' => true, + ], + 'list2_column' => [ + 'type' => 'string', + 'cast' => [ + 0 => 'list', + 'type' => 'int', + 'separator' => '|', + ], + 'length' => 255, + 'default' => [], + ], + 'decimal_column' => [ + 'type' => 'decimal', + 'cast' => 'decimal', + 'unsigned' => false, + 'length' => 10, + 'scale' => 2, + ], + 'nullable_decimal_column' => [ + 'type' => 'decimal', + 'cast' => 'decimal', + 'unsigned' => false, + 'length' => 10, + 'scale' => 2, + 'nullable' => true, + ], + ], $columns); + } }