diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index faecf718..f0ed4419 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -454,6 +454,7 @@ public function truncateTable(string $tableName): void * Convert from cakephp/database conventions to migrations\column * * - converts datetimefractional -> datetime + length + * - converts binary types to mysql blob type constants. * * @param array $columnData The cakephp/database column data to transform * @return array The extracted/converted type and length. @@ -469,6 +470,28 @@ protected function mapColumnType(array $columnData): array } elseif ($type === TableSchema::TYPE_TIMESTAMP_FRACTIONAL) { $type = 'timestamp'; $length = $columnData['precision'] ?? $length; + } elseif ($type === TableSchema::TYPE_BINARY) { + // CakePHP returns BLOB columns as 'binary' with specific lengths + // Check the raw MySQL type to distinguish BLOB from BINARY columns + $rawType = $columnData['rawType'] ?? ''; + if (str_contains($rawType, 'blob')) { + // Map BLOB columns back to the appropriate BLOB types + if (str_contains($rawType, 'tinyblob')) { + $type = static::PHINX_TYPE_TINYBLOB; + $length = static::BLOB_TINY; + } elseif (str_contains($rawType, 'mediumblob')) { + $type = static::PHINX_TYPE_MEDIUMBLOB; + $length = static::BLOB_MEDIUM; + } elseif (str_contains($rawType, 'longblob')) { + $type = static::PHINX_TYPE_LONGBLOB; + $length = static::BLOB_LONG; + } else { + // Regular BLOB + $type = static::PHINX_TYPE_BLOB; + $length = static::BLOB_REGULAR; + } + } + // else: keep as binary or varbinary (actual BINARY/VARBINARY column) } return [$type, $length]; @@ -481,8 +504,17 @@ public function getColumns(string $tableName): array { $dialect = $this->getSchemaDialect(); $columnRecords = $dialect->describeColumns($tableName); + + // Fetch raw column types to distinguish BLOB from BINARY columns + $rawTypes = []; + $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); + foreach ($rows as $row) { + $rawTypes[$row['Field']] = strtolower($row['Type']); + } + $columns = []; foreach ($columnRecords as $record) { + $record['rawType'] = $rawTypes[$record['name']] ?? null; [$type, $length] = $this->mapColumnType($record); $column = (new Column()) diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 546a80e6..4e77d523 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -1069,11 +1069,12 @@ public function testTinyTextColumn() public static function binaryToBlobAutomaticConversionData() { return [ - // limit, expected type, expected limit - [null, 'binary', null], - [64, 'binary', 255], + // When creating binary with limit > 255, MySQL auto-converts to BLOB + // input limit, expected SQL type name, expected column limit after round-trip + [null, 'blob', MysqlAdapter::BLOB_REGULAR], // binary(null) becomes BLOB + [64, 'tinyblob', MysqlAdapter::BLOB_TINY], // binary(64) becomes TINYBLOB [MysqlAdapter::BLOB_REGULAR - 20, 'mediumblob', MysqlAdapter::BLOB_MEDIUM], - [MysqlAdapter::BLOB_REGULAR, 'binary', null], + [MysqlAdapter::BLOB_REGULAR, 'blob', MysqlAdapter::BLOB_REGULAR], [MysqlAdapter::BLOB_REGULAR + 20, 'mediumblob', MysqlAdapter::BLOB_MEDIUM], [MysqlAdapter::BLOB_MEDIUM, 'mediumblob', MysqlAdapter::BLOB_MEDIUM], [MysqlAdapter::BLOB_MEDIUM + 20, 'longblob', MysqlAdapter::BLOB_LONG], @@ -1096,11 +1097,12 @@ public function testBinaryToBlobAutomaticConversion(?int $limit, string $expecte public static function varbinaryToBlobAutomaticConversionData() { return [ - // limit, expected type, expected limit - [null, 'binary', null], - [64, 'binary', 255], + // When creating varbinary with limit > 255, MySQL auto-converts to BLOB + // input limit, expected SQL type name, expected column limit after round-trip + [null, 'blob', MysqlAdapter::BLOB_REGULAR], // varbinary(null) becomes BLOB + [64, 'tinyblob', MysqlAdapter::BLOB_TINY], // varbinary(64) becomes TINYBLOB [MysqlAdapter::BLOB_REGULAR - 20, 'mediumblob', MysqlAdapter::BLOB_MEDIUM], - [MysqlAdapter::BLOB_REGULAR, 'binary', null], + [MysqlAdapter::BLOB_REGULAR, 'blob', MysqlAdapter::BLOB_REGULAR], [MysqlAdapter::BLOB_REGULAR + 20, 'mediumblob', MysqlAdapter::BLOB_MEDIUM], [MysqlAdapter::BLOB_MEDIUM, 'mediumblob', MysqlAdapter::BLOB_MEDIUM], [MysqlAdapter::BLOB_MEDIUM + 20, 'longblob', MysqlAdapter::BLOB_LONG], @@ -1123,28 +1125,29 @@ public function testVarbinaryToBlobAutomaticConversion(?int $limit, string $expe public static function blobColumnsData() { return [ - // type, expected type, limit, expected limit + // BLOB columns with various limits - MySQL auto-selects appropriate BLOB subtype + // input type, expected SQL type, input limit, expected column limit after round-trip // Tiny blobs - ['tinyblob', 'binary', null, MysqlAdapter::BLOB_TINY], - ['tinyblob', 'binary', MysqlAdapter::BLOB_TINY, MysqlAdapter::BLOB_TINY], + ['tinyblob', 'tinyblob', null, MysqlAdapter::BLOB_TINY], + ['tinyblob', 'tinyblob', MysqlAdapter::BLOB_TINY, MysqlAdapter::BLOB_TINY], ['tinyblob', 'mediumblob', MysqlAdapter::BLOB_TINY + 20, MysqlAdapter::BLOB_MEDIUM], ['tinyblob', 'mediumblob', MysqlAdapter::BLOB_MEDIUM, MysqlAdapter::BLOB_MEDIUM], ['tinyblob', 'longblob', MysqlAdapter::BLOB_LONG, MysqlAdapter::BLOB_LONG], - // // Regular blobs - ['blob', 'binary', MysqlAdapter::BLOB_TINY, MysqlAdapter::BLOB_TINY], - ['blob', 'binary', null, null], - ['blob', 'binary', MysqlAdapter::BLOB_REGULAR, null], + // Regular blobs + ['blob', 'tinyblob', MysqlAdapter::BLOB_TINY, MysqlAdapter::BLOB_TINY], + ['blob', 'blob', null, MysqlAdapter::BLOB_REGULAR], + ['blob', 'blob', MysqlAdapter::BLOB_REGULAR, MysqlAdapter::BLOB_REGULAR], ['blob', 'mediumblob', MysqlAdapter::BLOB_MEDIUM, MysqlAdapter::BLOB_MEDIUM], ['blob', 'longblob', MysqlAdapter::BLOB_LONG, MysqlAdapter::BLOB_LONG], - // // medium blobs - ['mediumblob', 'binary', MysqlAdapter::BLOB_TINY, MysqlAdapter::BLOB_TINY], - ['mediumblob', 'binary', MysqlAdapter::BLOB_REGULAR, null], + // Medium blobs + ['mediumblob', 'tinyblob', MysqlAdapter::BLOB_TINY, MysqlAdapter::BLOB_TINY], + ['mediumblob', 'blob', MysqlAdapter::BLOB_REGULAR, MysqlAdapter::BLOB_REGULAR], ['mediumblob', 'mediumblob', null, MysqlAdapter::BLOB_MEDIUM], ['mediumblob', 'mediumblob', MysqlAdapter::BLOB_MEDIUM, MysqlAdapter::BLOB_MEDIUM], ['mediumblob', 'longblob', MysqlAdapter::BLOB_LONG, MysqlAdapter::BLOB_LONG], - // long blobs - ['longblob', 'binary', MysqlAdapter::BLOB_TINY, MysqlAdapter::BLOB_TINY], - ['longblob', 'binary', MysqlAdapter::BLOB_REGULAR, null], + // Long blobs + ['longblob', 'tinyblob', MysqlAdapter::BLOB_TINY, MysqlAdapter::BLOB_TINY], + ['longblob', 'blob', MysqlAdapter::BLOB_REGULAR, MysqlAdapter::BLOB_REGULAR], ['longblob', 'mediumblob', MysqlAdapter::BLOB_MEDIUM, MysqlAdapter::BLOB_MEDIUM], ['longblob', 'longblob', null, MysqlAdapter::BLOB_LONG], ['longblob', 'longblob', MysqlAdapter::BLOB_LONG, MysqlAdapter::BLOB_LONG], @@ -1163,6 +1166,42 @@ public function testblobColumns(string $type, string $expectedType, ?int $limit, $this->assertSame($expectedLimit, $columns[1]->getLimit()); } + public static function blobRoundTripData() + { + return [ + // type, limit, expected type after round-trip, expected limit after round-trip + ['blob', null, 'blob', MysqlAdapter::BLOB_REGULAR], + ['blob', MysqlAdapter::BLOB_REGULAR, 'blob', MysqlAdapter::BLOB_REGULAR], + ['tinyblob', null, 'tinyblob', MysqlAdapter::BLOB_TINY], + ['mediumblob', null, 'mediumblob', MysqlAdapter::BLOB_MEDIUM], + ['longblob', null, 'longblob', MysqlAdapter::BLOB_LONG], + ]; + } + + #[DataProvider('blobRoundTripData')] + public function testBlobRoundTrip(string $type, ?int $limit, string $expectedType, int $expectedLimit) + { + // Create a table with a BLOB column + $table = new Table('blob_round_trip_test', [], $this->adapter); + $table->addColumn('blob_col', $type, ['limit' => $limit]) + ->save(); + + // Read the column back from the database + $columns = $this->adapter->getColumns('blob_round_trip_test'); + + $blobColumn = $columns[1]; + $this->assertNotNull($blobColumn, 'BLOB column not found'); + $this->assertSame($expectedType, $blobColumn->getType(), 'Type mismatch after round-trip'); + $this->assertSame($expectedLimit, $blobColumn->getLimit(), 'Limit mismatch after round-trip'); + + // Verify that the SQL type is correct + $sqlType = $this->adapter->getSqlType($blobColumn->getType(), $blobColumn->getLimit()); + $this->assertSame($type, $sqlType['name']); + + // Clean up + $this->adapter->dropTable('blob_round_trip_test'); + } + public function testBigIntegerColumn() { $table = new Table('t', [], $this->adapter); @@ -1289,7 +1328,7 @@ public static function columnsProvider() ['column9', 'time', []], ['column10', 'timestamp', []], ['column11', 'date', []], - ['column12', 'binary', []], + ['column12', 'blob', []], // binary with no limit becomes BLOB in MySQL ['column13', 'boolean', ['comment' => 'Lorem ipsum']], ['column14', 'string', ['limit' => 10]], ['column16', 'geometry', []],