diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index 68bcd71b..d0490daf 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -27,13 +27,17 @@ use Cake\Database\Schema\ForeignKey; use Cake\Database\Schema\Index; use Cake\Database\Schema\TableSchema; +use Cake\Database\Schema\TableSchemaInterface; use Cake\Database\Schema\UniqueKey; use Cake\Datasource\ConnectionManager; use Cake\Event\Event; use Cake\Event\EventManager; +use Error; use Migrations\Migration\ManagerFactory; use Migrations\Util\TableFinder; use Migrations\Util\UtilTrait; +use ReflectionException; +use ReflectionProperty; /** * Task class for generating migration diff files. @@ -259,7 +263,7 @@ protected function getColumns(): void // brand new columns $addedColumns = array_diff($currentColumns, $oldColumns); foreach ($addedColumns as $columnName) { - $column = $currentSchema->getColumn($columnName); + $column = $this->safeGetColumn($currentSchema, $columnName); /** @var int $key */ $key = array_search($columnName, $currentColumns); if ($key > 0) { @@ -274,8 +278,8 @@ protected function getColumns(): void // changes in columns meta-data foreach ($currentColumns as $columnName) { - $column = $currentSchema->getColumn($columnName); - $oldColumn = $this->dumpSchema[$table]->getColumn($columnName); + $column = $this->safeGetColumn($currentSchema, $columnName); + $oldColumn = $this->safeGetColumn($this->dumpSchema[$table], $columnName); unset( $column['collate'], $column['fixed'], @@ -351,7 +355,7 @@ protected function getColumns(): void $removedColumns = array_diff($oldColumns, $currentColumns); if ($removedColumns) { foreach ($removedColumns as $columnName) { - $column = $this->dumpSchema[$table]->getColumn($columnName); + $column = $this->safeGetColumn($this->dumpSchema[$table], $columnName); /** @var int $key */ $key = array_search($columnName, $oldColumns); if ($key > 0) { @@ -621,6 +625,67 @@ public function template(): string return 'Migrations.config/diff'; } + /** + * Safely get column information from a TableSchema. + * + * This method handles the case where Column::$fixed property may not be + * initialized (e.g., when loaded from a cached/serialized schema). + * + * @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema + * @param string $columnName The column name + * @return array|null Column data array or null if column doesn't exist + */ + protected function safeGetColumn(TableSchemaInterface $schema, string $columnName): ?array + { + try { + return $schema->getColumn($columnName); + } catch (Error $e) { + // Handle uninitialized typed property errors (e.g., Column::$fixed) + // This can happen with cached/serialized schema objects + if (str_contains($e->getMessage(), 'must not be accessed before initialization')) { + // Initialize uninitialized properties using reflection and retry + $this->initializeColumnProperties($schema, $columnName); + + return $schema->getColumn($columnName); + } + throw $e; + } + } + + /** + * Initialize potentially uninitialized Column properties using reflection. + * + * @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema + * @param string $columnName The column name + * @return void + */ + protected function initializeColumnProperties(TableSchemaInterface $schema, string $columnName): void + { + // Access the internal columns array via reflection + $reflection = new ReflectionProperty($schema, '_columns'); + $columns = $reflection->getValue($schema); + + if (!isset($columns[$columnName]) || !($columns[$columnName] instanceof Column)) { + return; + } + + $column = $columns[$columnName]; + + // List of nullable properties that might not be initialized + $nullableProperties = ['fixed', 'collate', 'unsigned', 'generated', 'srid', 'onUpdate']; + + foreach ($nullableProperties as $propertyName) { + try { + $propReflection = new ReflectionProperty(Column::class, $propertyName); + if (!$propReflection->isInitialized($column)) { + $propReflection->setValue($column, null); + } + } catch (Error | ReflectionException) { + // Property doesn't exist or can't be accessed, skip it + } + } + } + /** * Gets the option parser instance and configures it. * diff --git a/tests/TestCase/MigrationsTest.php b/tests/TestCase/MigrationsTest.php index 5601a4ff..2830b88c 100644 --- a/tests/TestCase/MigrationsTest.php +++ b/tests/TestCase/MigrationsTest.php @@ -15,7 +15,6 @@ use Cake\Core\Configure; use Cake\Core\Plugin; -use Cake\Database\Driver\Mysql; use Cake\Database\Driver\Sqlserver; use Cake\Datasource\ConnectionManager; use Cake\TestSuite\TestCase; @@ -218,14 +217,19 @@ public function testMigrateAndRollback() $expected = ['id', 'name', 'created', 'updated']; $this->assertEquals($expected, $columns); $createdColumn = $storesTable->getSchema()->getColumn('created'); - $expected = 'CURRENT_TIMESTAMP'; $driver = $this->Connection->getDriver(); - if ($driver instanceof Mysql && $driver->isMariadb()) { - $expected = 'current_timestamp()'; - } elseif ($driver instanceof Sqlserver) { + if ($driver instanceof Sqlserver) { $expected = 'getdate()'; + $this->assertEquals($expected, $createdColumn['default']); + } else { + // MySQL and MariaDB may return CURRENT_TIMESTAMP in different formats + // depending on version: CURRENT_TIMESTAMP, current_timestamp(), CURRENT_TIMESTAMP() + $this->assertMatchesRegularExpression( + '/^current_timestamp(\(\))?$/i', + $createdColumn['default'], + 'Default value should be CURRENT_TIMESTAMP in some form', + ); } - $this->assertEquals($expected, $createdColumn['default']); // Rollback last $rollback = $this->migrations->rollback();