Skip to content

Commit 66e53bd

Browse files
authored
Merge pull request #1043 from cakephp/5.x
Merge 5.x into 5.next
2 parents debd054 + 612764c commit 66e53bd

File tree

14 files changed

+438
-18
lines changed

14 files changed

+438
-18
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"require": {
2525
"php": ">=8.2",
2626
"cakephp/cache": "^5.3.0",
27-
"cakephp/database": "^5.3.0",
27+
"cakephp/database": "^5.3.2",
2828
"cakephp/orm": "^5.3.0"
2929
},
3030
"require-dev": {

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
*

src/Command/UpgradeCommand.php

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Cake\Console\Arguments;
1818
use Cake\Console\ConsoleIo;
1919
use Cake\Console\ConsoleOptionParser;
20+
use Cake\Core\Plugin;
2021
use Cake\Database\Connection;
2122
use Cake\Database\Exception\QueryException;
2223
use Cake\Datasource\ConnectionManager;
@@ -156,10 +157,13 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
156157
$io->success('Upgrade complete!');
157158
$io->out('');
158159
$io->out('Next steps:');
159-
$io->out(' 1. Set <info>\'Migrations\' => [\'legacyTables\' => false]</info> in your config');
160-
$io->out(' 2. Test your application');
161-
if (!$dropTables) {
162-
$io->out(' 3. Optionally drop the empty phinxlog tables (re-run `bin/cake migrations upgrade --drop-tables`)');
160+
if ($dropTables) {
161+
$io->out(' 1. Set <info>\'Migrations\' => [\'legacyTables\' => false]</info> in your config');
162+
$io->out(' 2. Test your application');
163+
} else {
164+
$io->out(' 1. Test your application');
165+
$io->out(' 2. Drop the phinxlog tables (re-run `bin/cake migrations upgrade --drop-tables`)');
166+
$io->out(' 3. Set <info>\'Migrations\' => [\'legacyTables\' => false]</info> in your config');
163167
}
164168
} else {
165169
$io->out('');
@@ -181,20 +185,51 @@ protected function findLegacyTables(Connection $connection): array
181185
$tables = $schema->listTables();
182186
$legacyTables = [];
183187

188+
// Build a map of expected table prefixes to plugin names for loaded plugins
189+
// This allows matching plugins with special characters like CakeDC/Users
190+
$pluginPrefixMap = $this->buildPluginPrefixMap();
191+
184192
foreach ($tables as $table) {
185193
if ($table === 'phinxlog') {
186194
$legacyTables[$table] = null;
187195
} elseif (str_ends_with($table, '_phinxlog')) {
188196
// Extract plugin name from table name
189197
$prefix = substr($table, 0, -9); // Remove '_phinxlog'
190-
$plugin = Inflector::camelize($prefix);
198+
199+
// Try to match against loaded plugins first
200+
if (isset($pluginPrefixMap[$prefix])) {
201+
$plugin = $pluginPrefixMap[$prefix];
202+
} else {
203+
// Fall back to camelizing the prefix
204+
$plugin = Inflector::camelize($prefix);
205+
}
191206
$legacyTables[$table] = $plugin;
192207
}
193208
}
194209

195210
return $legacyTables;
196211
}
197212

213+
/**
214+
* Build a map of table prefixes to plugin names for all loaded plugins.
215+
*
216+
* This handles plugins with special characters like CakeDC/Users where
217+
* the table prefix is cake_d_c_users but the plugin name is CakeDC/Users.
218+
*
219+
* @return array<string, string> Map of table prefix => plugin name
220+
*/
221+
protected function buildPluginPrefixMap(): array
222+
{
223+
$map = [];
224+
foreach (Plugin::loaded() as $plugin) {
225+
$prefix = Inflector::underscore($plugin);
226+
$prefix = str_replace(['\\', '/', '.'], '_', $prefix);
227+
$map[$prefix] = $plugin;
228+
}
229+
230+
return $map;
231+
}
232+
198233
/**
199234
* Check if a table exists.
200235
*

src/Db/Adapter/AbstractAdapter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ protected function getUpsertClause(?InsertMode $mode, ?array $updateColumns, ?ar
745745
return '';
746746
}
747747

748-
if ($conflictColumns !== null) {
748+
if ($conflictColumns !== null && $conflictColumns !== []) {
749749
trigger_error(
750750
'The $conflictColumns parameter is ignored by MySQL. ' .
751751
'MySQL\'s ON DUPLICATE KEY UPDATE applies to all unique constraints on the table.',

src/Db/Adapter/AdapterInterface.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ interface AdapterInterface
6363

6464
// only for mysql so far
6565
public const TYPE_YEAR = TableSchemaInterface::TYPE_YEAR;
66+
public const TYPE_BIT = TableSchemaInterface::TYPE_BIT;
6667

6768
// only for postgresql so far
6869
public const TYPE_CIDR = TableSchemaInterface::TYPE_CIDR;

src/Db/Adapter/MysqlAdapter.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,20 @@ protected function mapColumnType(array $columnData): array
594594
}
595595
}
596596
// else: keep as binary or varbinary (actual BINARY/VARBINARY column)
597+
} elseif ($type === TableSchema::TYPE_TEXT) {
598+
// CakePHP returns TEXT columns as 'text' with specific lengths
599+
// Check the raw MySQL type to distinguish TEXT variants
600+
$rawType = $columnData['rawType'] ?? '';
601+
if (str_contains($rawType, 'tinytext')) {
602+
$length = static::TEXT_TINY;
603+
} elseif (str_contains($rawType, 'mediumtext')) {
604+
$length = static::TEXT_MEDIUM;
605+
} elseif (str_contains($rawType, 'longtext')) {
606+
$length = static::TEXT_LONG;
607+
} else {
608+
// Regular TEXT - use null to indicate default TEXT type
609+
$length = null;
610+
}
597611
}
598612

599613
return [$type, $length];
@@ -637,6 +651,9 @@ public function getColumns(string $tableName): array
637651
if ($record['onUpdate'] ?? false) {
638652
$column->setUpdate($record['onUpdate']);
639653
}
654+
if ($record['fixed'] ?? false) {
655+
$column->setFixed(true);
656+
}
640657

641658
$columns[] = $column;
642659
}

src/Db/Adapter/PostgresAdapter.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,12 @@ protected function getChangeColumnInstructions(
476476
$quotedColumnName,
477477
);
478478
}
479+
if (in_array($newColumn->getType(), ['json'])) {
480+
$sql .= sprintf(
481+
' USING (%s::jsonb)',
482+
$quotedColumnName,
483+
);
484+
}
479485
// NULL and DEFAULT cannot be set while changing column type
480486
$sql = preg_replace('/ NOT NULL/', '', $sql);
481487
$sql = preg_replace('/ DEFAULT NULL/', '', $sql);

src/Db/Table/Column.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ class Column extends DatabaseColumn
117117
*/
118118
protected ?string $lock = null;
119119

120+
/**
121+
* @var bool|null
122+
*/
123+
protected ?bool $fixed = null;
124+
120125
/**
121126
* Column constructor
122127
*
@@ -772,6 +777,31 @@ public function getLock(): ?string
772777
return $this->lock;
773778
}
774779

780+
/**
781+
* Sets whether field should use fixed-length storage (for binary columns).
782+
*
783+
* When true, binary columns will use BINARY(n) instead of VARBINARY(n).
784+
*
785+
* @param bool $fixed Fixed
786+
* @return $this
787+
*/
788+
public function setFixed(bool $fixed)
789+
{
790+
$this->fixed = $fixed;
791+
792+
return $this;
793+
}
794+
795+
/**
796+
* Gets whether field should use fixed-length storage.
797+
*
798+
* @return bool|null
799+
*/
800+
public function getFixed(): ?bool
801+
{
802+
return $this->fixed;
803+
}
804+
775805
/**
776806
* Gets all allowed options. Each option must have a corresponding `setFoo` method.
777807
*
@@ -802,6 +832,7 @@ protected function getValidOptions(): array
802832
'generated',
803833
'algorithm',
804834
'lock',
835+
'fixed',
805836
];
806837
}
807838

@@ -894,6 +925,7 @@ public function toArray(): array
894925
'default' => $default,
895926
'generated' => $this->getGenerated(),
896927
'unsigned' => $this->getUnsigned(),
928+
'fixed' => $this->getFixed(),
897929
'onUpdate' => $this->getUpdate(),
898930
'collate' => $this->getCollation(),
899931
'precision' => $precision,

src/View/Helper/MigrationHelper.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ public function getColumnOption(array $options): array
389389
'scale',
390390
'after',
391391
'collate',
392+
'fixed',
392393
]);
393394
$columnOptions = array_intersect_key($options, $wantedOptions);
394395
if (empty($columnOptions['comment'])) {
@@ -495,7 +496,7 @@ public function attributes(TableSchemaInterface|string $table, string $column):
495496
'comment', 'unsigned',
496497
'signed', 'properties',
497498
'autoIncrement', 'unique',
498-
'collate',
499+
'collate', 'fixed',
499500
];
500501

501502
$attributes = [];

0 commit comments

Comments
 (0)