Skip to content

Commit 81beb29

Browse files
committed
Fix CI failures on MySQL/MariaDB
1. Handle uninitialized Column::$fixed property in BakeMigrationDiffCommand When TableSchema::getColumn() is called on cached/serialized schema objects, the Column::$fixed property may not be initialized, causing an Error. Added safeGetColumn() helper that catches this error and uses reflection to initialize uninitialized properties before retry. 2. Fix CURRENT_TIMESTAMP test assertion for MySQL/MariaDB Different versions of MySQL and MariaDB return CURRENT_TIMESTAMP in different formats (CURRENT_TIMESTAMP, current_timestamp(), CURRENT_TIMESTAMP()). Changed the test to use a regex that accepts all valid formats case-insensitively. Fixes #1033
1 parent 3d97a9b commit 81beb29

File tree

2 files changed

+79
-10
lines changed

2 files changed

+79
-10
lines changed

src/Command/BakeMigrationDiffCommand.php

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@
2727
use Cake\Database\Schema\ForeignKey;
2828
use Cake\Database\Schema\Index;
2929
use Cake\Database\Schema\TableSchema;
30+
use Cake\Database\Schema\TableSchemaInterface;
3031
use Cake\Database\Schema\UniqueKey;
3132
use Cake\Datasource\ConnectionManager;
3233
use Cake\Event\Event;
3334
use Cake\Event\EventManager;
35+
use Error;
3436
use Migrations\Migration\ManagerFactory;
3537
use Migrations\Util\TableFinder;
3638
use Migrations\Util\UtilTrait;
39+
use ReflectionException;
40+
use ReflectionProperty;
3741

3842
/**
3943
* Task class for generating migration diff files.
@@ -259,7 +263,7 @@ protected function getColumns(): void
259263
// brand new columns
260264
$addedColumns = array_diff($currentColumns, $oldColumns);
261265
foreach ($addedColumns as $columnName) {
262-
$column = $currentSchema->getColumn($columnName);
266+
$column = $this->safeGetColumn($currentSchema, $columnName);
263267
/** @var int $key */
264268
$key = array_search($columnName, $currentColumns);
265269
if ($key > 0) {
@@ -274,8 +278,8 @@ protected function getColumns(): void
274278

275279
// changes in columns meta-data
276280
foreach ($currentColumns as $columnName) {
277-
$column = $currentSchema->getColumn($columnName);
278-
$oldColumn = $this->dumpSchema[$table]->getColumn($columnName);
281+
$column = $this->safeGetColumn($currentSchema, $columnName);
282+
$oldColumn = $this->safeGetColumn($this->dumpSchema[$table], $columnName);
279283
unset(
280284
$column['collate'],
281285
$column['fixed'],
@@ -351,7 +355,7 @@ protected function getColumns(): void
351355
$removedColumns = array_diff($oldColumns, $currentColumns);
352356
if ($removedColumns) {
353357
foreach ($removedColumns as $columnName) {
354-
$column = $this->dumpSchema[$table]->getColumn($columnName);
358+
$column = $this->safeGetColumn($this->dumpSchema[$table], $columnName);
355359
/** @var int $key */
356360
$key = array_search($columnName, $oldColumns);
357361
if ($key > 0) {
@@ -621,6 +625,67 @@ public function template(): string
621625
return 'Migrations.config/diff';
622626
}
623627

628+
/**
629+
* Safely get column information from a TableSchema.
630+
*
631+
* This method handles the case where Column::$fixed property may not be
632+
* initialized (e.g., when loaded from a cached/serialized schema).
633+
*
634+
* @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema
635+
* @param string $columnName The column name
636+
* @return array<string, mixed>|null Column data array or null if column doesn't exist
637+
*/
638+
protected function safeGetColumn(TableSchemaInterface $schema, string $columnName): ?array
639+
{
640+
try {
641+
return $schema->getColumn($columnName);
642+
} catch (Error $e) {
643+
// Handle uninitialized typed property errors (e.g., Column::$fixed)
644+
// This can happen with cached/serialized schema objects
645+
if (str_contains($e->getMessage(), 'must not be accessed before initialization')) {
646+
// Initialize uninitialized properties using reflection and retry
647+
$this->initializeColumnProperties($schema, $columnName);
648+
649+
return $schema->getColumn($columnName);
650+
}
651+
throw $e;
652+
}
653+
}
654+
655+
/**
656+
* Initialize potentially uninitialized Column properties using reflection.
657+
*
658+
* @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema
659+
* @param string $columnName The column name
660+
* @return void
661+
*/
662+
protected function initializeColumnProperties(TableSchemaInterface $schema, string $columnName): void
663+
{
664+
// Access the internal columns array via reflection
665+
$reflection = new ReflectionProperty($schema, '_columns');
666+
$columns = $reflection->getValue($schema);
667+
668+
if (!isset($columns[$columnName]) || !($columns[$columnName] instanceof Column)) {
669+
return;
670+
}
671+
672+
$column = $columns[$columnName];
673+
674+
// List of nullable properties that might not be initialized
675+
$nullableProperties = ['fixed', 'collate', 'unsigned', 'generated', 'srid', 'onUpdate'];
676+
677+
foreach ($nullableProperties as $propertyName) {
678+
try {
679+
$propReflection = new ReflectionProperty(Column::class, $propertyName);
680+
if (!$propReflection->isInitialized($column)) {
681+
$propReflection->setValue($column, null);
682+
}
683+
} catch (Error | ReflectionException) {
684+
// Property doesn't exist or can't be accessed, skip it
685+
}
686+
}
687+
}
688+
624689
/**
625690
* Gets the option parser instance and configures it.
626691
*

tests/TestCase/MigrationsTest.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
use Cake\Core\Configure;
1717
use Cake\Core\Plugin;
18-
use Cake\Database\Driver\Mysql;
1918
use Cake\Database\Driver\Sqlserver;
2019
use Cake\Datasource\ConnectionManager;
2120
use Cake\TestSuite\TestCase;
@@ -218,14 +217,19 @@ public function testMigrateAndRollback()
218217
$expected = ['id', 'name', 'created', 'updated'];
219218
$this->assertEquals($expected, $columns);
220219
$createdColumn = $storesTable->getSchema()->getColumn('created');
221-
$expected = 'CURRENT_TIMESTAMP';
222220
$driver = $this->Connection->getDriver();
223-
if ($driver instanceof Mysql && $driver->isMariadb()) {
224-
$expected = 'current_timestamp()';
225-
} elseif ($driver instanceof Sqlserver) {
221+
if ($driver instanceof Sqlserver) {
226222
$expected = 'getdate()';
223+
$this->assertEquals($expected, $createdColumn['default']);
224+
} else {
225+
// MySQL and MariaDB may return CURRENT_TIMESTAMP in different formats
226+
// depending on version: CURRENT_TIMESTAMP, current_timestamp(), CURRENT_TIMESTAMP()
227+
$this->assertMatchesRegularExpression(
228+
'/^current_timestamp(\(\))?$/i',
229+
$createdColumn['default'],
230+
'Default value should be CURRENT_TIMESTAMP in some form',
231+
);
227232
}
228-
$this->assertEquals($expected, $createdColumn['default']);
229233

230234
// Rollback last
231235
$rollback = $this->migrations->rollback();

0 commit comments

Comments
 (0)