diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e20be6..5a5ca1f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Chg #339: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) - New #342: Add JSON overlaps condition builder (@Tigrov) - Enh #344: Update `bit` type according to main PR yiisoft/db#860 (@Tigrov) -- New #346: Implement `ColumnFactory` class (@Tigrov) +- New #346, #361: Implement `ColumnFactory` class (@Tigrov) - Enh #347, #353: Raise minimum PHP version to `^8.1` with minor refactoring (@Tigrov) - Bug #349, #352: Restore connection if closed by connection timeout (@Tigrov) - Enh #354: Separate column type constants (@Tigrov) @@ -16,6 +16,7 @@ - Enh #357: Update according changes in `ColumnSchemaInterface` (@Tigrov) - New #358: Add `ColumnDefinitionBuilder` class (@Tigrov) - Enh #359: Refactor `Dsn` class (@Tigrov) +- Enh #361: Refactor `Schema::findColumns()` method (@Tigrov) ## 1.2.0 March 21, 2024 diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index a4044fa5..df1cf580 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -13,10 +13,9 @@ final class ColumnFactory extends AbstractColumnFactory * Mapping from physical column types (keys) to abstract column types (values). * * @var string[] - * - * @psalm-suppress MissingClassConstType + * @psalm-var array */ - private const TYPE_MAP = [ + protected const TYPE_MAP = [ 'bit' => ColumnType::BIT, 'tinyint' => ColumnType::TINYINT, 'smallint' => ColumnType::SMALLINT, @@ -50,17 +49,10 @@ final class ColumnFactory extends AbstractColumnFactory protected function getType(string $dbType, array $info = []): string { - $type = self::TYPE_MAP[$dbType] ?? ColumnType::STRING; - - if ($type === ColumnType::BIT && isset($info['size']) && $info['size'] === 1) { + if ($dbType === 'bit' && isset($info['size']) && $info['size'] === 1) { return ColumnType::BOOLEAN; } - return $type; - } - - protected function isDbType(string $dbType): bool - { - return isset(self::TYPE_MAP[$dbType]); + return parent::getType($dbType, $info); } } diff --git a/src/Schema.php b/src/Schema.php index eac4de34..f6a54885 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -33,27 +33,26 @@ use function preg_match_all; use function preg_match; use function serialize; -use function stripos; +use function str_contains; +use function str_ireplace; +use function str_starts_with; use function strtolower; +use function substr; use function trim; /** * Implements MySQL, MariaDB specific schema, supporting MySQL Server 5.7, MariaDB Server 10.4 and higher. * - * @psalm-type ColumnInfoArray = array{ - * field: string, - * type: string, - * collation: string|null, - * null: string, - * key: string, - * default: string|null, + * @psalm-type ColumnArray = array{ + * column_name: string, + * column_default: string|null, + * is_nullable: string, + * column_type: string, + * column_key: string, * extra: string, - * extra_default_value: string|null, - * privileges: string, - * comment: string, - * enum_values?: string[], - * size?: int, - * scale?: int, + * column_comment: string, + * schema: string, + * table: string * } * @psalm-type RowConstraint = array{ * constraint_name: string, @@ -140,63 +139,56 @@ public function findUniqueIndexes(TableSchemaInterface $table): array */ protected function findColumns(TableSchemaInterface $table): bool { - $tableName = $table->getFullName() ?? ''; - $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName); - - try { - $columns = $this->db->createCommand($sql)->queryAll(); - // Chapter 1: crutches for MariaDB. {@see https://github.com/yiisoft/yii2/issues/19747} - $columnsExtra = []; - if (str_contains($this->db->getServerVersion(), 'MariaDB')) { - $rows = $this->db->createCommand( - << $table->getSchemaName(), - ':tableName' => $table->getName(), - ] - )->queryAll(); - /** @psalm-var string[] $cols */ - foreach ($rows as $cols) { - $columnsExtra[$cols['name']] = $cols['default_value']; - } - } - } catch (Exception $e) { - $previous = $e->getPrevious(); - - if ($previous && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) { - /** - * The table doesn't exist. - * - * @link https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error - */ - return false; - } - - throw $e; + $schemaName = $table->getSchemaName(); + $tableName = $table->getName(); + + $columns = $this->db->createCommand( + << $schemaName, + ':tableName' => $tableName, + ] + )->queryAll(); + + if (empty($columns)) { + return false; } $jsonColumns = $this->getJsonColumns($table); + $isMariaDb = str_contains($this->db->getServerVersion(), 'MariaDB'); - /** @psalm-var ColumnInfoArray $info */ foreach ($columns as $info) { - /** @psalm-var ColumnInfoArray $info */ $info = array_change_key_case($info); - $info['extra_default_value'] = $columnsExtra[$info['field']] ?? ''; + $info['schema'] = $schemaName; + $info['table'] = $tableName; - if (in_array($info['field'], $jsonColumns, true)) { - $info['type'] = ColumnType::JSON; + if (in_array($info['column_name'], $jsonColumns, true)) { + $info['column_type'] = ColumnType::JSON; } + if ($isMariaDb && $info['column_default'] === 'NULL') { + $info['column_default'] = null; + } + + /** @psalm-var ColumnArray $info */ $column = $this->loadColumnSchema($info); - $table->column($info['field'], $column); + $table->column($info['column_name'], $column); if ($column->isPrimaryKey()) { - $table->primaryKey($info['field']); + $table->primaryKey($info['column_name']); if ($column->isAutoIncrement()) { $table->sequenceName(''); } @@ -415,43 +407,28 @@ protected function getCreateTableSql(TableSchemaInterface $table): string * * @return ColumnSchemaInterface The column schema object. * - * @psalm-param ColumnInfoArray $info The column information. + * @psalm-param ColumnArray $info The column information. */ private function loadColumnSchema(array $info): ColumnSchemaInterface { - $columnFactory = $this->getColumnFactory(); - - $dbType = $info['type']; - /** @psalm-var ColumnInfoArray $info */ - $column = $columnFactory->fromDefinition($dbType); - /** @psalm-suppress DeprecatedMethod */ - $column->name($info['field']); - $column->notNull($info['null'] !== 'YES'); - $column->primaryKey($info['key'] === 'PRI'); - $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false); - $column->unique($info['key'] === 'UNI'); - $column->comment($info['comment']); - $column->dbType($dbType); - - // Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747} - $extra = $info['extra']; - if ( - empty($extra) - && !empty($info['extra_default_value']) - && !str_starts_with($info['extra_default_value'], '\'') - && in_array($column->getType(), [ - ColumnType::CHAR, ColumnType::STRING, ColumnType::TEXT, - ColumnType::DATETIME, ColumnType::TIMESTAMP, ColumnType::TIME, ColumnType::DATE, - ], true) - ) { - $extra = 'DEFAULT_GENERATED'; - } - - $column->extra($extra); - $column->defaultValue($this->normalizeDefaultValue($info['default'], $column)); + $extra = trim(str_ireplace('auto_increment', '', $info['extra'], $autoIncrement)); + + $column = $this->getColumnFactory()->fromDefinition($info['column_type'], [ + 'autoIncrement' => $autoIncrement > 0, + 'comment' => $info['column_comment'], + 'extra' => $extra, + 'name' => $info['column_name'], + 'notNull' => $info['is_nullable'] !== 'YES', + 'primaryKey' => $info['column_key'] === 'PRI', + 'schema' => $info['schema'], + 'table' => $info['table'], + 'unique' => $info['column_key'] === 'UNI', + ]); + + $column->defaultValue($this->normalizeDefaultValue($info['column_default'], $column)); if (str_starts_with($extra, 'DEFAULT_GENERATED')) { - $column->extra(trim(strtoupper(substr($extra, 18)))); + $column->extra(trim(substr($extra, 18))); } return $column; @@ -490,6 +467,10 @@ private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterf return $column->phpTypecast(bindec(trim($defaultValue, "b'"))); } + if ($defaultValue[0] === "'" && $defaultValue[-1] === "'") { + return $column->phpTypecast(substr($defaultValue, 1, -1)); + } + return $column->phpTypecast($defaultValue); } @@ -820,7 +801,7 @@ private function getJsonColumns(TableSchemaInterface $table): array { $sql = $this->getCreateTableSql($table); $result = []; - $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi'; + $regexp = '/json_valid\([`"](.+)[`"]\s*\)/mi'; if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER) > 0) { foreach ($matches as $match) { diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 218df99f..82c0bb15 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -18,7 +18,7 @@ public static function columns(): array [ 'int_col' => [ 'type' => 'integer', - 'dbType' => 'int(11)', + 'dbType' => 'int', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => true, @@ -30,7 +30,7 @@ public static function columns(): array ], 'int_col2' => [ 'type' => 'integer', - 'dbType' => 'int(11)', + 'dbType' => 'int', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => false, @@ -42,7 +42,7 @@ public static function columns(): array ], 'bigunsigned_col' => [ 'type' => 'bigint', - 'dbType' => 'bigint(20) unsigned', + 'dbType' => 'bigint', 'phpType' => 'string', 'primaryKey' => false, 'notNull' => false, @@ -55,7 +55,7 @@ public static function columns(): array ], 'tinyint_col' => [ 'type' => 'tinyint', - 'dbType' => 'tinyint(3)', + 'dbType' => 'tinyint', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => false, @@ -67,7 +67,7 @@ public static function columns(): array ], 'smallint_col' => [ 'type' => 'smallint', - 'dbType' => 'smallint(1)', + 'dbType' => 'smallint', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => false, @@ -79,7 +79,7 @@ public static function columns(): array ], 'char_col' => [ 'type' => 'char', - 'dbType' => 'char(100)', + 'dbType' => 'char', 'phpType' => 'string', 'primaryKey' => false, 'notNull' => true, @@ -91,7 +91,7 @@ public static function columns(): array ], 'char_col2' => [ 'type' => 'string', - 'dbType' => 'varchar(100)', + 'dbType' => 'varchar', 'phpType' => 'string', 'primaryKey' => false, 'notNull' => false, @@ -115,7 +115,7 @@ public static function columns(): array ], 'enum_col' => [ 'type' => 'string', - 'dbType' => "enum('a','B','c,D')", + 'dbType' => 'enum', 'phpType' => 'string', 'primaryKey' => false, 'notNull' => false, @@ -127,7 +127,7 @@ public static function columns(): array ], 'float_col' => [ 'type' => 'double', - 'dbType' => 'double(4,3)', + 'dbType' => 'double', 'phpType' => 'float', 'primaryKey' => false, 'notNull' => true, @@ -163,7 +163,7 @@ public static function columns(): array ], 'numeric_col' => [ 'type' => 'decimal', - 'dbType' => 'decimal(5,2)', + 'dbType' => 'decimal', 'phpType' => 'float', 'primaryKey' => false, 'notNull' => false, @@ -187,7 +187,7 @@ public static function columns(): array ], 'bool_col' => [ 'type' => 'boolean', - 'dbType' => 'bit(1)', + 'dbType' => 'bit', 'phpType' => 'bool', 'primaryKey' => false, 'notNull' => true, @@ -199,7 +199,7 @@ public static function columns(): array ], 'tiny_col' => [ 'type' => 'tinyint', - 'dbType' => 'tinyint(1)', + 'dbType' => 'tinyint', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => false, @@ -223,7 +223,7 @@ public static function columns(): array ], 'bit_col' => [ 'type' => 'bit', - 'dbType' => 'bit(8)', + 'dbType' => 'bit', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => true, @@ -252,7 +252,7 @@ public static function columns(): array [ 'id' => [ 'type' => 'integer', - 'dbType' => 'int(11)', + 'dbType' => 'int', 'phpType' => 'int', 'primaryKey' => true, 'notNull' => true, @@ -264,7 +264,7 @@ public static function columns(): array ], 'type' => [ 'type' => 'string', - 'dbType' => 'varchar(255)', + 'dbType' => 'varchar', 'phpType' => 'string', 'primaryKey' => false, 'notNull' => true, @@ -281,7 +281,7 @@ public static function columns(): array [ 'C_id' => [ 'type' => 'integer', - 'dbType' => 'int(11)', + 'dbType' => 'int', 'phpType' => 'int', 'primaryKey' => true, 'notNull' => true, @@ -294,7 +294,7 @@ public static function columns(): array ], 'C_not_null' => [ 'type' => 'integer', - 'dbType' => 'int(11)', + 'dbType' => 'int', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => true, @@ -307,7 +307,7 @@ public static function columns(): array ], 'C_check' => [ 'type' => 'string', - 'dbType' => 'varchar(255)', + 'dbType' => 'varchar', 'phpType' => 'string', 'primaryKey' => false, 'notNull' => false, @@ -320,7 +320,7 @@ public static function columns(): array ], 'C_unique' => [ 'type' => 'integer', - 'dbType' => 'int(11)', + 'dbType' => 'int', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => true, @@ -333,7 +333,7 @@ public static function columns(): array ], 'C_default' => [ 'type' => 'integer', - 'dbType' => 'int(11)', + 'dbType' => 'int', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => true, @@ -357,7 +357,7 @@ public static function columnsTypeBit(): array [ 'bit_col_1' => [ 'type' => 'boolean', - 'dbType' => 'bit(1)', + 'dbType' => 'bit', 'phpType' => 'bool', 'primaryKey' => false, 'notNull' => true, @@ -369,7 +369,7 @@ public static function columnsTypeBit(): array ], 'bit_col_2' => [ 'type' => 'boolean', - 'dbType' => 'bit(1)', + 'dbType' => 'bit', 'phpType' => 'bool', 'primaryKey' => false, 'notNull' => false, @@ -381,7 +381,7 @@ public static function columnsTypeBit(): array ], 'bit_col_3' => [ 'type' => 'bit', - 'dbType' => 'bit(32)', + 'dbType' => 'bit', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => true, @@ -393,7 +393,7 @@ public static function columnsTypeBit(): array ], 'bit_col_4' => [ 'type' => 'bit', - 'dbType' => 'bit(32)', + 'dbType' => 'bit', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => false, @@ -405,7 +405,7 @@ public static function columnsTypeBit(): array ], 'bit_col_5' => [ 'type' => 'bit', - 'dbType' => 'bit(64)', + 'dbType' => 'bit', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => true, @@ -417,7 +417,7 @@ public static function columnsTypeBit(): array ], 'bit_col_6' => [ 'type' => 'bit', - 'dbType' => 'bit(64)', + 'dbType' => 'bit', 'phpType' => 'int', 'primaryKey' => false, 'notNull' => false, diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 538460b3..15ad616c 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Db\Mysql\Tests; -use ReflectionException; use Throwable; use Yiisoft\Db\Command\CommandInterface; use Yiisoft\Db\Connection\ConnectionInterface; @@ -21,10 +20,8 @@ use Yiisoft\Db\Mysql\Schema; use Yiisoft\Db\Mysql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; -use Yiisoft\Db\Schema\Column\StringColumnSchema; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Tests\Common\CommonSchemaTest; -use Yiisoft\Db\Tests\Support\Assert; use Yiisoft\Db\Tests\Support\DbHelper; use function version_compare; @@ -38,71 +35,6 @@ final class SchemaTest extends CommonSchemaTest { use TestTrait; - /** - * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed as - * CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3. - * - * {@link https://mariadb.com/kb/en/library/now/#description} - * {@link https://github.com/yiisoft/yii2/issues/15167} - * - * @throws ReflectionException - */ - public function testAlternativeDisplayOfDefaultCurrentTimestampInMariaDB(): void - { - /** - * We do not have a real database MariaDB >= 10.2.3 for tests, so we emulate the information that database - * returns in response to the query `SHOW FULL COLUMNS FROM ...` - */ - $db = $this->getConnection(); - - $schema = new Schema($db, DbHelper::getSchemaCache()); - - $column = Assert::invokeMethod($schema, 'loadColumnSchema', [[ - 'field' => 'emulated_MariaDB_field', - 'type' => 'timestamp', - 'collation' => null, - 'null' => 'NO', - 'key' => '', - 'default' => 'current_timestamp()', - 'extra' => '', - 'extra_default_value' => 'current_timestamp()', - 'privileges' => 'select,insert,update,references', - 'comment' => '', - ]]); - - $this->assertInstanceOf(StringColumnSchema::class, $column); - $this->assertInstanceOf(Expression::class, $column->getDefaultValue()); - $this->assertEquals('CURRENT_TIMESTAMP', $column->getDefaultValue()); - } - - /** - * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is provided - * as NULL. - * - * @see https://github.com/yiisoft/yii2/issues/19047 - */ - public function testAlternativeDisplayOfDefaultCurrentTimestampAsNullInMariaDB(): void - { - $db = $this->getConnection(); - - $schema = new Schema($db, DbHelper::getSchemaCache()); - - $column = Assert::invokeMethod($schema, 'loadColumnSchema', [[ - 'field' => 'emulated_MariaDB_field', - 'type' => 'timestamp', - 'collation' => null, - 'null' => 'NO', - 'key' => '', - 'default' => null, - 'extra' => '', - 'privileges' => 'select,insert,update,references', - 'comment' => '', - ]]); - - $this->assertInstanceOf(StringColumnSchema::class, $column); - $this->assertEquals(null, $column->getDefaultValue()); - } - /** * @dataProvider \Yiisoft\Db\Mysql\Tests\Provider\SchemaProvider::columns * @@ -117,43 +49,28 @@ public function testColumnSchema(array $columns, string $tableName): void !str_contains($db->getServerVersion(), 'MariaDB') ) { if ($tableName === 'type') { - // int_col Mysql 8.0.17+. - $columns['int_col']['dbType'] = 'int'; $columns['int_col']['size'] = null; - // int_col2 Mysql 8.0.17+. - $columns['int_col2']['dbType'] = 'int'; $columns['int_col2']['size'] = null; - // bigunsigned_col Mysql 8.0.17+. - $columns['bigunsigned_col']['dbType'] = 'bigint unsigned'; $columns['bigunsigned_col']['size'] = null; - // tinyint_col Mysql 8.0.17+. - $columns['tinyint_col']['dbType'] = 'tinyint'; $columns['tinyint_col']['size'] = null; - // smallint_col Mysql 8.0.17+. - $columns['smallint_col']['dbType'] = 'smallint'; $columns['smallint_col']['size'] = null; } if ($tableName === 'animal') { - $columns['id']['dbType'] = 'int'; $columns['id']['size'] = null; } if ($tableName === 'T_constraints_1') { - $columns['C_id']['dbType'] = 'int'; $columns['C_id']['size'] = null; - $columns['C_not_null']['dbType'] = 'int'; $columns['C_not_null']['size'] = null; - $columns['C_unique']['dbType'] = 'int'; $columns['C_unique']['size'] = null; - $columns['C_default']['dbType'] = 'int'; $columns['C_default']['size'] = null; } } @@ -296,18 +213,6 @@ public function testGetSchemaNames(): void $this->assertSame(['yiitest'], $schema->getSchemaNames()); } - /** - * @dataProvider \Yiisoft\Db\Mysql\Tests\Provider\SchemaProvider::columnsTypeChar - */ - public function testGetStringFieldsSize( - string $columnName, - string $columnType, - int|null $columnSize, - string $columnDbType - ): void { - parent::testGetStringFieldsSize($columnName, $columnType, $columnSize, $columnDbType); - } - public function testGetTableChecks(): void { $this->expectException(NotSupportedException::class);