diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d259b91..31602ddd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ - Enh #386: Remove `getCacheKey()` and `getCacheTag()` methods from `Schema` class (@Tigrov) - Bug #388: Set empty `comment` and `extra` properties to `null` when loading table columns (@Tigrov) - Enh #389, #390: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov) +- New #387: Realize `Schema::loadResultColumn()` method (@Tigrov) ## 1.2.0 March 21, 2024 diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index 53a8d5e4f..03abb2141 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -46,6 +46,8 @@ final class ColumnFactory extends AbstractColumnFactory 'binary' => ColumnType::BINARY, 'varbinary' => ColumnType::BINARY, 'blob' => ColumnType::BINARY, + 'tinyblob' => ColumnType::BINARY, + 'mediumblob' => ColumnType::BINARY, 'longblob' => ColumnType::BINARY, 'year' => ColumnType::DATE, 'date' => ColumnType::DATE, diff --git a/src/Schema.php b/src/Schema.php index d84f84b64..48a6594ee 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -29,9 +29,12 @@ use function str_contains; use function str_ireplace; use function str_starts_with; +use function strtolower; use function substr; use function trim; +use const PHP_INT_SIZE; + /** * Implements MySQL, MariaDB specific schema, supporting MySQL Server 5.7, MariaDB Server 10.4 and higher. * @@ -359,6 +362,76 @@ protected function getCreateTableSql(TableSchemaInterface $table): string return $sql; } + /** + * @psalm-param array{ + * native_type: string, + * pdo_type: int, + * flags: string[], + * table: string, + * name: string, + * len: int, + * precision: int, + * } $metadata + * + * @psalm-suppress MoreSpecificImplementedParamType + */ + protected function loadResultColumn(array $metadata): ColumnInterface|null + { + if (empty($metadata['native_type']) || $metadata['native_type'] === 'NULL') { + return null; + } + + $dbType = match ($metadata['native_type']) { + 'TINY' => 'tinyint', + 'SHORT' => 'smallint', + 'INT24' => 'mediumint', + 'LONG' => 'int', + 'LONGLONG' => $metadata['len'] < 10 ? 'int' : 'bigint', + 'NEWDECIMAL' => 'decimal', + 'STRING' => 'char', + 'VAR_STRING' => 'varchar', + 'BLOB' => match ($metadata['len']) { + 255 => 'tinyblob', + 510, 765, 1020 => 'tinytext', + // 65535 => 'blob', + 131070, 196605, 262140 => 'text', + 16777215 => 'mediumblob', + 33554430, 50331645, 67108860 => 'mediumtext', + 4294967295 => 'longblob', + default => 'blob', + }, + default => strtolower($metadata['native_type']), + }; + + $columnInfo = []; + + if (!empty($metadata['table'])) { + $columnInfo['table'] = $metadata['table']; + $columnInfo['name'] = $metadata['name']; + } elseif (!empty($metadata['name'])) { + $columnInfo['name'] = $metadata['name']; + } + + if (!empty($metadata['len'])) { + $columnInfo['size'] = match ($dbType) { + 'decimal' => $metadata['len'] - ($metadata['precision'] === 0 ? 1 : 2), + 'time', 'datetime', 'timestamp' => $metadata['precision'], + default => $metadata['len'], + }; + } + + match ($dbType) { + 'float', 'double', 'decimal' => $columnInfo['scale'] = $metadata['precision'], + 'bigint' => $metadata['len'] === 20 ? $columnInfo['unsigned'] = true : null, + 'int' => $metadata['len'] === 10 && PHP_INT_SIZE !== 8 ? $columnInfo['unsigned'] = true : null, + default => null, + }; + + $columnInfo['notNull'] = in_array('not_null', $metadata['flags'], true); + + return $this->db->getColumnFactory()->fromDbType($dbType, $columnInfo); + } + /** * Loads the column information into a {@see ColumnInterface} object. * diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php index cdcac9dd3..98f910887 100644 --- a/tests/ColumnTest.php +++ b/tests/ColumnTest.php @@ -7,6 +7,7 @@ use Throwable; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Mysql\Column\ColumnBuilder; +use Yiisoft\Db\Mysql\Connection; use Yiisoft\Db\Mysql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Schema\Column\BinaryColumn; @@ -28,6 +29,127 @@ final class ColumnTest extends AbstractColumnTest { use TestTrait; + private function insertTypeValues(Connection $db): void + { + $db->createCommand()->insert( + 'type', + [ + 'int_col' => 1, + 'bigunsigned_col' => '12345678901234567890', + 'char_col' => str_repeat('x', 100), + 'char_col3' => null, + 'float_col' => 1.234, + 'blob_col' => "\x10\x11\x12", + 'time' => '2023-07-11 14:50:23', + 'bool_col' => false, + 'bit_col' => 0b0110_0100, // 100 + 'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], + ] + )->execute(); + } + + private function assertResultValues(array $result): void + { + $this->assertSame(1, $result['int_col']); + $this->assertSame('12345678901234567890', $result['bigunsigned_col']); + $this->assertSame(str_repeat('x', 100), $result['char_col']); + $this->assertNull($result['char_col3']); + $this->assertSame(1.234, $result['float_col']); + $this->assertSame("\x10\x11\x12", $result['blob_col']); + $this->assertSame('2023-07-11 14:50:23', $result['time']); + $this->assertFalse($result['bool_col']); + $this->assertSame(0b0110_0100, $result['bit_col']); + $this->assertJsonStringEqualsJsonString('[{"a":1,"b":null,"c":[1,3,5]}]', $result['json_col']); + } + + public function testQueryTypecasting(): void + { + $db = $this->getConnection(true); + + $this->insertTypeValues($db); + + $query = (new Query($db))->from('type')->withTypecasting(); + + $result = $query->one(); + + $this->assertResultValues($result); + + $result = $query->all(); + + $this->assertResultValues($result[0]); + + $db->close(); + } + + public function testCommandWithPhpTypecasting(): void + { + $db = $this->getConnection(true); + + $this->insertTypeValues($db); + + $command = $db->createCommand('SELECT * FROM type')->withPhpTypecasting(); + + $result = $command->queryOne(); + + $this->assertResultValues($result); + + $result = $command->queryAll(); + + $this->assertResultValues($result[0]); + + $db->close(); + } + + public function testSelectWithPhpTypecasting(): void + { + $db = $this->getConnection(); + + $sql = << null, + 1 => 1, + '2.5' => 2.5, + 'true' => 1, + 'false' => 0, + 'string' => 'string', + ]; + + $result = $db->createCommand($sql) + ->withPhpTypecasting() + ->queryOne(); + + $this->assertSame($expected, $result); + + $result = $db->createCommand($sql) + ->withPhpTypecasting() + ->queryAll(); + + $this->assertSame([$expected], $result); + + $result = $db->createCommand('SELECT 2.5') + ->withPhpTypecasting() + ->queryScalar(); + + $this->assertSame(2.5, $result); + + $result = $db->createCommand('SELECT 2.5 UNION SELECT 3.3') + ->withPhpTypecasting() + ->queryColumn(); + + $this->assertSame([2.5, 3.3], $result); + + $db->close(); + } + /** * @dataProvider \Yiisoft\Db\Mysql\Tests\Provider\ColumnProvider::bigIntValue * @@ -51,30 +173,12 @@ public function testColumnBigInt(string $bigint): void public function testPhpTypeCast(): void { $db = $this->getConnection(true); - - $command = $db->createCommand(); $schema = $db->getSchema(); $tableSchema = $schema->getTableSchema('type'); - $command->insert( - 'type', - [ - 'int_col' => 1, - 'bigunsigned_col' => 1234567890, - 'char_col' => str_repeat('x', 100), - 'char_col3' => null, - 'float_col' => 1.234, - 'blob_col' => "\x10\x11\x12", - 'time' => '2023-07-11 14:50:23', - 'bool_col' => false, - 'bit_col' => 0b0110_0100, // 100 - 'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], - ] - ); - $command->execute(); - $query = (new Query($db))->from('type')->one(); + $this->insertTypeValues($db); - $this->assertNotNull($tableSchema); + $query = (new Query($db))->from('type')->one(); $intColPhpType = $tableSchema->getColumn('int_col')?->phpTypecast($query['int_col']); $bigUnsignedColPhpType = $tableSchema->getColumn('bigunsigned_col')?->phpTypecast($query['bigunsigned_col']); @@ -88,7 +192,7 @@ public function testPhpTypeCast(): void $jsonColPhpType = $tableSchema->getColumn('json_col')?->phpTypecast($query['json_col']); $this->assertSame(1, $intColPhpType); - $this->assertSame('1234567890', $bigUnsignedColPhpType); + $this->assertSame('12345678901234567890', $bigUnsignedColPhpType); $this->assertSame(str_repeat('x', 100), $charColPhpType); $this->assertNull($charCol3PhpType); $this->assertSame(1.234, $floatColPhpType); diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 7a12490ee..d25ef7b04 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -97,11 +97,14 @@ public function testDropDefaultValue(): void public function testDropTableCascade(): void { - $command = $this->getConnection()->createCommand(); + $db = $this->getConnection(); + $command = $db->createCommand(); $this->expectException(NotSupportedException::class); $this->expectExceptionMessage('MySQL doesn\'t support cascade drop table.'); $command->dropTable('{{table}}', cascade: true); + + $db->close(); } /** diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index ce57356fc..deb5b4467 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -53,6 +53,10 @@ public static function columns(): array size: 1, defaultValue: 1, ), + 'mediumint_col' => new IntegerColumn( + dbType: 'mediumint', + size: 9, + ), 'char_col' => new StringColumn( ColumnType::CHAR, dbType: 'char', @@ -121,6 +125,20 @@ enumValues: ['a', 'B', 'c,D'], size: 8, defaultValue: 130, // b'10000010' ), + 'tinyblob_col' => new BinaryColumn( + dbType: 'tinyblob', + ), + 'tinytext_col' => new StringColumn( + ColumnType::TEXT, + dbType: 'tinytext', + ), + 'mediumblob_col' => new BinaryColumn( + dbType: 'mediumblob', + ), + 'mediumtext_col' => new StringColumn( + ColumnType::TEXT, + dbType: 'mediumtext', + ), 'json_col' => new JsonColumn( dbType: 'json', ), @@ -234,6 +252,167 @@ public static function constraints(): array return $constraints; } + public static function resultColumns(): array + { + return [ + [null, []], + [null, ['native_type' => 'NULL']], + [new IntegerColumn(dbType: 'int', name: 'int_col', notNull: true, size: 11), [ + 'native_type' => 'LONG', + 'pdo_type' => 1, + 'flags' => ['not_null'], + 'table' => 'type', + 'name' => 'int_col', + 'len' => 11, + 'precision' => 0, + ]], + [new BigIntColumn(dbType: 'bigint', name: 'bigunsigned_col', notNull: false, size: 20, unsigned: true), [ + 'native_type' => 'LONGLONG', + 'pdo_type' => 1, + 'flags' => [], + 'table' => 'type', + 'name' => 'bigunsigned_col', + 'len' => 20, + 'precision' => 0, + ]], + [new IntegerColumn(ColumnType::TINYINT, dbType: 'tinyint', name: 'tinyint_col', notNull: false, size: 3), [ + 'native_type' => 'TINY', + 'pdo_type' => 1, + 'flags' => [], + 'table' => 'type', + 'name' => 'tinyint_col', + 'len' => 3, + 'precision' => 0, + ]], + [new IntegerColumn(ColumnType::SMALLINT, dbType: 'smallint', name: 'smallint_col', notNull: false, size: 1), [ + 'native_type' => 'SHORT', + 'pdo_type' => 1, + 'flags' => [], + 'table' => 'type', + 'name' => 'smallint_col', + 'len' => 1, + 'precision' => 0, + ]], + [new StringColumn(ColumnType::CHAR, dbType: 'char', name: 'char_col', notNull: true, size: 400), [ + 'native_type' => 'STRING', + 'pdo_type' => 2, + 'flags' => ['not_null'], + 'table' => 'type', + 'name' => 'char_col', + 'len' => 400, + 'precision' => 0, + ]], + [new StringColumn(dbType: 'varchar', name: 'char_col2', notNull: false, size: 400), [ + 'native_type' => 'VAR_STRING', + 'pdo_type' => 2, + 'flags' => [], + 'table' => 'type', + 'name' => 'char_col2', + 'len' => 400, + 'precision' => 0, + ]], + [new StringColumn(ColumnType::TEXT, dbType: 'text', name: 'char_col3', notNull: false, size: 262140), [ + 'native_type' => 'BLOB', + 'pdo_type' => 2, + 'flags' => ['blob'], + 'table' => 'type', + 'name' => 'char_col3', + 'len' => 262140, + 'precision' => 0, + ]], + [new DoubleColumn(dbType: 'double', name: 'float_col', notNull: true, size: 4, scale: 3), [ + 'native_type' => 'DOUBLE', + 'pdo_type' => 2, + 'flags' => ['not_null'], + 'table' => 'type', + 'name' => 'float_col', + 'len' => 4, + 'precision' => 3, + ]], + [new DoubleColumn(ColumnType::DECIMAL, dbType: 'decimal', name: 'numeric_col', notNull: false, size: 5, scale: 2), [ + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => 2, + 'flags' => [], + 'table' => 'type', + 'name' => 'numeric_col', + 'len' => 7, + 'precision' => 2, + ]], + [new StringColumn(ColumnType::TIMESTAMP, dbType: 'timestamp', name: 'time', notNull: true, size: 0), [ + 'native_type' => 'TIMESTAMP', + 'pdo_type' => 2, + 'flags' => ['not_null'], + 'table' => 'type', + 'name' => 'time', + 'len' => 19, + 'precision' => 0, + ]], + [new BooleanColumn(dbType: 'bit', name: 'bool_col', notNull: true, size: 1), [ + 'native_type' => 'BIT', + 'pdo_type' => 1, + 'flags' => ['not_null'], + 'table' => 'type', + 'name' => 'bool_col', + 'len' => 1, + 'precision' => 0, + ]], + [new BitColumn(dbType: 'bit', name: 'bit_col', notNull: true, size: 8), [ + 'native_type' => 'BIT', + 'pdo_type' => 1, + 'flags' => ['not_null'], + 'table' => 'type', + 'name' => 'bit_col', + 'len' => 8, + 'precision' => 0, + ]], + [new IntegerColumn(dbType: 'int', name: '1', size: 1, notNull: true), [ + 'native_type' => 'LONG', + 'pdo_type' => 1, + 'flags' => ['not_null'], + 'table' => '', + 'name' => '1', + 'len' => 1, + 'precision' => 0, + ]], + [new IntegerColumn(dbType: 'int', name: '1', size: 1, notNull: true), [ + 'native_type' => 'LONGLONG', + 'pdo_type' => 1, + 'flags' => ['not_null'], + 'table' => '', + 'name' => '1', + 'len' => 1, + 'precision' => 0, + ]], + [new DoubleColumn(ColumnType::DECIMAL, dbType: 'decimal', name: '2.5', notNull: true, size: 2, scale: 1), [ + 'native_type' => 'NEWDECIMAL', + 'pdo_type' => 2, + 'flags' => ['not_null'], + 'table' => '', + 'name' => '2.5', + 'len' => 4, + 'precision' => 1, + ]], + [new StringColumn(dbType: 'varchar', name: 'string', notNull: true, size: 24), [ + 'native_type' => 'VAR_STRING', + 'pdo_type' => 2, + 'flags' => ['not_null'], + 'table' => '', + 'name' => 'string', + 'len' => 24, + 'precision' => 39, + ]], + [new StringColumn(ColumnType::DATETIME, dbType: 'datetime', name: 'CURRENT_TIMESTAMP(3)', notNull: true, size: 3), [ + 'native_type' => 'DATETIME', + 'pdo_type' => 2, + 'flags' => ['not_null'], + 'table' => '', + 'name' => 'CURRENT_TIMESTAMP(3)', + 'len' => 23, + 'precision' => 3, + ]], + ]; + } + public static function tableSchemaWithDbSchemes(): array { return [ diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index feaab94d0..717d56e3c 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -755,8 +755,10 @@ public function testJsonOverlapsCondition(iterable|ExpressionInterface $values, $serverVersion = $db->getServerInfo()->getVersion(); if (str_contains($serverVersion, 'MariaDB') && version_compare($serverVersion, '10.9', '<')) { + $db->close(); self::markTestSkipped('MariaDB < 10.9 does not support JSON_OVERLAPS() function.'); } elseif (version_compare($serverVersion, '8', '<')) { + $db->close(); self::markTestSkipped('MySQL < 8 does not support JSON_OVERLAPS() function.'); } @@ -777,8 +779,10 @@ public function testJsonOverlapsConditionOperator(iterable|ExpressionInterface $ $serverVersion = $db->getServerInfo()->getVersion(); if (str_contains($serverVersion, 'MariaDB') && version_compare($serverVersion, '10.9', '<')) { + $db->close(); self::markTestSkipped('MariaDB < 10.9 does not support JSON_OVERLAPS() function.'); } elseif (version_compare($serverVersion, '8', '<')) { + $db->close(); self::markTestSkipped('MySQL < 8 does not support JSON_OVERLAPS() function.'); } diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 3a452c2c8..5465dfef3 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Mysql\Tests; +use PHPUnit\Framework\Attributes\DataProviderExternal; use Throwable; use Yiisoft\Db\Command\CommandInterface; use Yiisoft\Db\Connection\ConnectionInterface; @@ -15,8 +16,10 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Mysql\Column\ColumnBuilder; use Yiisoft\Db\Mysql\Schema; +use Yiisoft\Db\Mysql\Tests\Provider\SchemaProvider; use Yiisoft\Db\Mysql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; +use Yiisoft\Db\Schema\Column\ColumnInterface; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Tests\Common\CommonSchemaTest; use Yiisoft\Db\Tests\Support\DbHelper; @@ -53,6 +56,7 @@ public function testColumns(array $columns, string $tableName): void $columns['bigunsigned_col']->size(null); $columns['tinyint_col']->size(null); $columns['smallint_col']->size(null); + $columns['mediumint_col']->size(null); } if ($tableName === 'animal') { @@ -485,4 +489,10 @@ public function testInsertDefaultValues() $db->close(); } + + #[DataProviderExternal(SchemaProvider::class, 'resultColumns')] + public function testGetResultColumn(ColumnInterface|null $expected, array $info): void + { + parent::testGetResultColumn($expected, $info); + } } diff --git a/tests/Support/Fixture/mysql.sql b/tests/Support/Fixture/mysql.sql index 77ca45fe7..5b14bd901 100644 --- a/tests/Support/Fixture/mysql.sql +++ b/tests/Support/Fixture/mysql.sql @@ -152,6 +152,7 @@ CREATE TABLE `type` ( `bigunsigned_col` bigint unsigned DEFAULT '12345678901234567890', `tinyint_col` tinyint(3) DEFAULT '1', `smallint_col` smallint(1) DEFAULT '1', + `mediumint_col` mediumint, `char_col` char(100) NOT NULL, `char_col2` varchar(100) DEFAULT 'something', `char_col3` text, @@ -165,6 +166,10 @@ CREATE TABLE `type` ( `tiny_col` tinyint(1) DEFAULT '2', `ts_default` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `bit_col` BIT(8) NOT NULL DEFAULT b'10000010', + `tinyblob_col` tinyblob, + `tinytext_col` tinytext, + `mediumblob_col` mediumblob, + `mediumtext_col` mediumtext, `json_col` json ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index aec72d3ab..d15fd1af3 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -4,8 +4,6 @@ namespace Yiisoft\Db\Mysql\Tests\Support; -use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; -use Yiisoft\Db\Driver\Pdo\PdoDriverInterface; use Yiisoft\Db\Mysql\Connection; use Yiisoft\Db\Mysql\Driver; use Yiisoft\Db\Mysql\Dsn; @@ -15,7 +13,7 @@ trait TestTrait { private string $dsn = ''; - protected function getConnection(bool $fixture = false): PdoConnectionInterface + protected function getConnection(bool $fixture = false): Connection { $db = new Connection($this->getDriver(), DbHelper::getSchemaCache()); @@ -26,7 +24,7 @@ protected function getConnection(bool $fixture = false): PdoConnectionInterface return $db; } - protected static function getDb(): PdoConnectionInterface + protected static function getDb(): Connection { $dsn = (new Dsn( host: self::getHost(), @@ -71,7 +69,7 @@ public static function setUpBeforeClass(): void $db->close(); } - protected function getDriver(): PdoDriverInterface + protected function getDriver(): Driver { return new Driver($this->getDsn(), self::getUsername(), self::getPassword()); }