Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions src/Command/BakeMigrationDiffCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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'],
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string, mixed>|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.
*
Expand Down
16 changes: 10 additions & 6 deletions tests/TestCase/MigrationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down