From 8cdb289f0da6b72f84797ccc5554f08ebe01cae1 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 11 Nov 2025 01:09:04 +0100 Subject: [PATCH 01/10] Make unsigned the default for int columns. --- src/Db/Table/Column.php | 65 ++++++++++++--- tests/TestCase/Db/Table/ColumnTest.php | 107 +++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 13 deletions(-) diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index 7d8733fe..e8a15cfc 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -483,6 +483,56 @@ public function getComment(): ?string return $this->comment; } + /** + * Gets whether field should be unsigned. + * + * Returns the explicit unsigned setting, or null if not set. + * This preserves compatibility with database dialects that don't support + * the UNSIGNED keyword (e.g., SQLite, PostgreSQL). + * + * @return bool|null + */ + public function getUnsigned(): ?bool + { + return $this->unsigned; + } + + /** + * Should the column be unsigned? + * + * Integer types (integer, biginteger, smallinteger, tinyinteger) default to unsigned + * when the unsigned property is not explicitly set. + * + * @return bool + */ + public function isUnsigned(): bool + { + // If explicitly set, use that value + if ($this->unsigned !== null) { + return $this->unsigned; + } + + // Default integer types to unsigned + $integerTypes = [ + self::INTEGER, + self::BIGINTEGER, + self::SMALLINTEGER, + self::TINYINTEGER, + ]; + + return in_array($this->type, $integerTypes, true); + } + + /** + * Should the column be signed? + * + * @return bool + */ + public function isSigned(): bool + { + return !$this->isUnsigned(); + } + /** * Sets whether field should be signed. * @@ -505,18 +555,7 @@ public function setSigned(bool $signed) */ public function getSigned(): bool { - return $this->unsigned === null ? true : !$this->unsigned; - } - - /** - * Should the column be signed? - * - * @return bool - * @deprecated 5.0 Use isUnsigned() instead. - */ - public function isSigned(): bool - { - return $this->getSigned(); + return !$this->isUnsigned(); } /** @@ -768,7 +807,7 @@ public function toArray(): array 'null' => $this->getNull(), 'default' => $default, 'generated' => $this->getGenerated(), - 'unsigned' => !$this->getSigned(), + 'unsigned' => $this->getUnsigned(), 'onUpdate' => $this->getUpdate(), 'collate' => $this->getCollation(), 'precision' => $precision, diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php index 9db0c746..1ad02ed4 100644 --- a/tests/TestCase/Db/Table/ColumnTest.php +++ b/tests/TestCase/Db/Table/ColumnTest.php @@ -72,4 +72,111 @@ public function testToArrayDefaultLiteralValue(): void $this->assertInstanceOf(QueryExpression::class, $result['default']); $this->assertEquals('CURRENT_TIMESTAMP', $result['default']->sql(new ValueBinder())); } + + public function testIntegerColumnDefaultsToUnsigned(): void + { + $column = new Column(); + $column->setName('user_id')->setType('integer'); + + $this->assertTrue($column->isUnsigned()); + $this->assertFalse($column->isSigned()); + $this->assertNull($column->getUnsigned()); + } + + public function testBigIntegerColumnDefaultsToUnsigned(): void + { + $column = new Column(); + $column->setName('big_id')->setType('biginteger'); + + $this->assertTrue($column->isUnsigned()); + $this->assertFalse($column->isSigned()); + $this->assertNull($column->getUnsigned()); + } + + public function testSmallIntegerColumnDefaultsToUnsigned(): void + { + $column = new Column(); + $column->setName('small_id')->setType('smallinteger'); + + $this->assertTrue($column->isUnsigned()); + $this->assertFalse($column->isSigned()); + $this->assertNull($column->getUnsigned()); + } + + public function testTinyIntegerColumnDefaultsToUnsigned(): void + { + $column = new Column(); + $column->setName('tiny_id')->setType('tinyinteger'); + + $this->assertTrue($column->isUnsigned()); + $this->assertFalse($column->isSigned()); + $this->assertNull($column->getUnsigned()); + } + + public function testNonIntegerColumnDoesNotDefaultToUnsigned(): void + { + $stringColumn = new Column(); + $stringColumn->setName('name')->setType('string'); + $this->assertNull($stringColumn->getUnsigned()); + $this->assertFalse($stringColumn->isUnsigned()); + + $dateColumn = new Column(); + $dateColumn->setName('created')->setType('datetime'); + $this->assertNull($dateColumn->getUnsigned()); + $this->assertFalse($dateColumn->isUnsigned()); + + $decimalColumn = new Column(); + $decimalColumn->setName('price')->setType('decimal'); + $this->assertNull($decimalColumn->getUnsigned()); + $this->assertFalse($decimalColumn->isUnsigned()); + } + + public function testExplicitSignedOverridesDefault(): void + { + $column = new Column(); + $column->setName('counter')->setType('integer')->setSigned(true); + + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); + $this->assertFalse($column->getUnsigned()); + } + + public function testExplicitUnsignedIsPreserved(): void + { + $column = new Column(); + $column->setName('age')->setType('integer')->setUnsigned(true); + + $this->assertTrue($column->isUnsigned()); + $this->assertFalse($column->isSigned()); + $this->assertTrue($column->getUnsigned()); + } + + public function testToArrayReturnsNullUnsignedForIntegersByDefault(): void + { + $column = new Column(); + $column->setName('user_id')->setType('integer'); + + $result = $column->toArray(); + // getUnsigned() returns null for integer types to maintain compatibility + // with database dialects that don't support UNSIGNED keyword + $this->assertNull($result['unsigned']); + } + + public function testToArrayReturnsNullForNonIntegerTypes(): void + { + $column = new Column(); + $column->setName('title')->setType('string'); + + $result = $column->toArray(); + $this->assertNull($result['unsigned']); + } + + public function testToArrayRespectsExplicitSigned(): void + { + $column = new Column(); + $column->setName('offset')->setType('integer')->setSigned(true); + + $result = $column->toArray(); + $this->assertFalse($result['unsigned']); + } } From 0503b5c249017a4efaad21479a4534c643d281f7 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 19 Nov 2025 14:12:57 +0100 Subject: [PATCH 02/10] Opt-in for unsigned column defaults. --- src/Db/Table/Column.php | 46 +++++++++- tests/TestCase/Db/Table/ColumnTest.php | 118 ++++++++++++++++++++++--- 2 files changed, 148 insertions(+), 16 deletions(-) diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index e8a15cfc..79cd8744 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -18,6 +18,26 @@ /** * This object is based loosely on: https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html. + * + * ## Configuration + * + * The following configuration options can be set in your application's config: + * + * - `Migrations.unsigned_primary_keys` (bool): When true, identity columns default to unsigned. + * Default: false + * + * - `Migrations.unsigned_ints` (bool): When true, all integer columns default to unsigned. + * Default: false + * + * Example configuration in config/app.php: + * ```php + * 'Migrations' => [ + * 'unsigned_primary_keys' => true, + * 'unsigned_ints' => true, + * ] + * ``` + * + * Note: Explicitly calling setUnsigned() or setSigned() on a column will override these defaults. */ class Column extends DatabaseColumn { @@ -500,8 +520,11 @@ public function getUnsigned(): ?bool /** * Should the column be unsigned? * - * Integer types (integer, biginteger, smallinteger, tinyinteger) default to unsigned - * when the unsigned property is not explicitly set. + * Checks configuration options to determine unsigned behavior: + * - If explicitly set via setUnsigned/setSigned, uses that value + * - If identity column and Migrations.unsigned_primary_keys is true, returns true + * - If integer type and Migrations.unsigned_ints is true, returns true + * - Otherwise defaults to false (signed) * * @return bool */ @@ -512,7 +535,6 @@ public function isUnsigned(): bool return $this->unsigned; } - // Default integer types to unsigned $integerTypes = [ self::INTEGER, self::BIGINTEGER, @@ -520,7 +542,23 @@ public function isUnsigned(): bool self::TINYINTEGER, ]; - return in_array($this->type, $integerTypes, true); + // Only apply configuration to integer types + if (!in_array($this->type, $integerTypes, true)) { + return false; + } + + // Check if this is a primary key/identity column + if ($this->identity && Configure::read('Migrations.unsigned_primary_keys')) { + return true; + } + + // Check general integer configuration + if (Configure::read('Migrations.unsigned_ints')) { + return true; + } + + // Default to signed for backward compatibility + return false; } /** diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php index 1ad02ed4..1c3c861b 100644 --- a/tests/TestCase/Db/Table/ColumnTest.php +++ b/tests/TestCase/Db/Table/ColumnTest.php @@ -73,43 +73,43 @@ public function testToArrayDefaultLiteralValue(): void $this->assertEquals('CURRENT_TIMESTAMP', $result['default']->sql(new ValueBinder())); } - public function testIntegerColumnDefaultsToUnsigned(): void + public function testIntegerColumnDefaultsToSigned(): void { $column = new Column(); $column->setName('user_id')->setType('integer'); - $this->assertTrue($column->isUnsigned()); - $this->assertFalse($column->isSigned()); + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); $this->assertNull($column->getUnsigned()); } - public function testBigIntegerColumnDefaultsToUnsigned(): void + public function testBigIntegerColumnDefaultsToSigned(): void { $column = new Column(); $column->setName('big_id')->setType('biginteger'); - $this->assertTrue($column->isUnsigned()); - $this->assertFalse($column->isSigned()); + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); $this->assertNull($column->getUnsigned()); } - public function testSmallIntegerColumnDefaultsToUnsigned(): void + public function testSmallIntegerColumnDefaultsToSigned(): void { $column = new Column(); $column->setName('small_id')->setType('smallinteger'); - $this->assertTrue($column->isUnsigned()); - $this->assertFalse($column->isSigned()); + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); $this->assertNull($column->getUnsigned()); } - public function testTinyIntegerColumnDefaultsToUnsigned(): void + public function testTinyIntegerColumnDefaultsToSigned(): void { $column = new Column(); $column->setName('tiny_id')->setType('tinyinteger'); - $this->assertTrue($column->isUnsigned()); - $this->assertFalse($column->isSigned()); + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); $this->assertNull($column->getUnsigned()); } @@ -179,4 +179,98 @@ public function testToArrayRespectsExplicitSigned(): void $result = $column->toArray(); $this->assertFalse($result['unsigned']); } + + #[RunInSeparateProcess] + public function testUnsignedIntsConfiguration(): void + { + // Without configuration, integers default to signed + Configure::write('Migrations.unsigned_ints', false); + $column = new Column(); + $column->setName('count')->setType('integer'); + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); + + // With configuration enabled, integers default to unsigned + Configure::write('Migrations.unsigned_ints', true); + $column = new Column(); + $column->setName('count')->setType('integer'); + $this->assertTrue($column->isUnsigned()); + $this->assertFalse($column->isSigned()); + + // Explicit signed overrides configuration + $column = new Column(); + $column->setName('offset')->setType('integer')->setSigned(true); + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); + } + + #[RunInSeparateProcess] + public function testUnsignedPrimaryKeysConfiguration(): void + { + // Without configuration, identity columns default to signed + Configure::write('Migrations.unsigned_primary_keys', false); + $column = new Column(); + $column->setName('id')->setType('integer')->setIdentity(true); + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); + + // With configuration enabled, identity columns default to unsigned + Configure::write('Migrations.unsigned_primary_keys', true); + $column = new Column(); + $column->setName('id')->setType('integer')->setIdentity(true); + $this->assertTrue($column->isUnsigned()); + $this->assertFalse($column->isSigned()); + + // Non-identity columns are not affected by unsigned_primary_keys + $column = new Column(); + $column->setName('user_id')->setType('integer'); + $this->assertFalse($column->isUnsigned()); + + // Explicit signed overrides configuration + $column = new Column(); + $column->setName('id')->setType('integer')->setIdentity(true)->setSigned(true); + $this->assertFalse($column->isUnsigned()); + $this->assertTrue($column->isSigned()); + } + + #[RunInSeparateProcess] + public function testBothUnsignedConfigurationsWork(): void + { + Configure::write('Migrations.unsigned_primary_keys', true); + Configure::write('Migrations.unsigned_ints', true); + + // Identity columns use unsigned_primary_keys configuration + $identityColumn = new Column(); + $identityColumn->setName('id')->setType('integer')->setIdentity(true); + $this->assertTrue($identityColumn->isUnsigned()); + + // Regular integer columns use unsigned_ints configuration + $intColumn = new Column(); + $intColumn->setName('count')->setType('integer'); + $this->assertTrue($intColumn->isUnsigned()); + + // Non-integer columns are not affected + $stringColumn = new Column(); + $stringColumn->setName('name')->setType('string'); + $this->assertFalse($stringColumn->isUnsigned()); + } + + #[RunInSeparateProcess] + public function testUnsignedConfigurationDoesNotAffectNonIntegerTypes(): void + { + Configure::write('Migrations.unsigned_ints', true); + Configure::write('Migrations.unsigned_primary_keys', true); + + $stringColumn = new Column(); + $stringColumn->setName('name')->setType('string'); + $this->assertFalse($stringColumn->isUnsigned()); + + $dateColumn = new Column(); + $dateColumn->setName('created')->setType('datetime'); + $this->assertFalse($dateColumn->isUnsigned()); + + $decimalColumn = new Column(); + $decimalColumn->setName('price')->setType('decimal'); + $this->assertFalse($decimalColumn->isUnsigned()); + } } From 3b452cd50cbd10a2ef6e8c1684851fee77a41647 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 19 Nov 2025 14:23:03 +0100 Subject: [PATCH 03/10] Opt-in for unsigned column defaults. --- src/Db/Table/Column.php | 26 +---------------------- tests/TestCase/Db/Table/ColumnTest.php | 29 +++++++++++++------------- 2 files changed, 15 insertions(+), 40 deletions(-) diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index 79cd8744..ea6c812e 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -506,20 +506,6 @@ public function getComment(): ?string /** * Gets whether field should be unsigned. * - * Returns the explicit unsigned setting, or null if not set. - * This preserves compatibility with database dialects that don't support - * the UNSIGNED keyword (e.g., SQLite, PostgreSQL). - * - * @return bool|null - */ - public function getUnsigned(): ?bool - { - return $this->unsigned; - } - - /** - * Should the column be unsigned? - * * Checks configuration options to determine unsigned behavior: * - If explicitly set via setUnsigned/setSigned, uses that value * - If identity column and Migrations.unsigned_primary_keys is true, returns true @@ -528,7 +514,7 @@ public function getUnsigned(): ?bool * * @return bool */ - public function isUnsigned(): bool + public function getUnsigned(): bool { // If explicitly set, use that value if ($this->unsigned !== null) { @@ -561,16 +547,6 @@ public function isUnsigned(): bool return false; } - /** - * Should the column be signed? - * - * @return bool - */ - public function isSigned(): bool - { - return !$this->isUnsigned(); - } - /** * Sets whether field should be signed. * diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php index 1c3c861b..6400417e 100644 --- a/tests/TestCase/Db/Table/ColumnTest.php +++ b/tests/TestCase/Db/Table/ColumnTest.php @@ -80,7 +80,7 @@ public function testIntegerColumnDefaultsToSigned(): void $this->assertFalse($column->isUnsigned()); $this->assertTrue($column->isSigned()); - $this->assertNull($column->getUnsigned()); + $this->assertFalse($column->getUnsigned()); } public function testBigIntegerColumnDefaultsToSigned(): void @@ -90,7 +90,7 @@ public function testBigIntegerColumnDefaultsToSigned(): void $this->assertFalse($column->isUnsigned()); $this->assertTrue($column->isSigned()); - $this->assertNull($column->getUnsigned()); + $this->assertFalse($column->getUnsigned()); } public function testSmallIntegerColumnDefaultsToSigned(): void @@ -100,7 +100,7 @@ public function testSmallIntegerColumnDefaultsToSigned(): void $this->assertFalse($column->isUnsigned()); $this->assertTrue($column->isSigned()); - $this->assertNull($column->getUnsigned()); + $this->assertFalse($column->getUnsigned()); } public function testTinyIntegerColumnDefaultsToSigned(): void @@ -110,24 +110,24 @@ public function testTinyIntegerColumnDefaultsToSigned(): void $this->assertFalse($column->isUnsigned()); $this->assertTrue($column->isSigned()); - $this->assertNull($column->getUnsigned()); + $this->assertFalse($column->getUnsigned()); } public function testNonIntegerColumnDoesNotDefaultToUnsigned(): void { $stringColumn = new Column(); $stringColumn->setName('name')->setType('string'); - $this->assertNull($stringColumn->getUnsigned()); + $this->assertFalse($stringColumn->getUnsigned()); $this->assertFalse($stringColumn->isUnsigned()); $dateColumn = new Column(); $dateColumn->setName('created')->setType('datetime'); - $this->assertNull($dateColumn->getUnsigned()); + $this->assertFalse($dateColumn->getUnsigned()); $this->assertFalse($dateColumn->isUnsigned()); $decimalColumn = new Column(); $decimalColumn->setName('price')->setType('decimal'); - $this->assertNull($decimalColumn->getUnsigned()); + $this->assertFalse($decimalColumn->getUnsigned()); $this->assertFalse($decimalColumn->isUnsigned()); } @@ -151,24 +151,23 @@ public function testExplicitUnsignedIsPreserved(): void $this->assertTrue($column->getUnsigned()); } - public function testToArrayReturnsNullUnsignedForIntegersByDefault(): void + public function testToArrayReturnsFalseForIntegersByDefault(): void { $column = new Column(); $column->setName('user_id')->setType('integer'); $result = $column->toArray(); - // getUnsigned() returns null for integer types to maintain compatibility - // with database dialects that don't support UNSIGNED keyword - $this->assertNull($result['unsigned']); + // getUnsigned() returns false for integer types by default (signed) + $this->assertFalse($result['unsigned']); } - public function testToArrayReturnsNullForNonIntegerTypes(): void + public function testToArrayReturnsFalseForNonIntegerTypes(): void { $column = new Column(); $column->setName('title')->setType('string'); $result = $column->toArray(); - $this->assertNull($result['unsigned']); + $this->assertFalse($result['unsigned']); } public function testToArrayRespectsExplicitSigned(): void @@ -184,7 +183,7 @@ public function testToArrayRespectsExplicitSigned(): void public function testUnsignedIntsConfiguration(): void { // Without configuration, integers default to signed - Configure::write('Migrations.unsigned_ints', false); + Configure::delete('Migrations.unsigned_ints'); $column = new Column(); $column->setName('count')->setType('integer'); $this->assertFalse($column->isUnsigned()); @@ -208,7 +207,7 @@ public function testUnsignedIntsConfiguration(): void public function testUnsignedPrimaryKeysConfiguration(): void { // Without configuration, identity columns default to signed - Configure::write('Migrations.unsigned_primary_keys', false); + Configure::delete('Migrations.unsigned_primary_keys'); $column = new Column(); $column->setName('id')->setType('integer')->setIdentity(true); $this->assertFalse($column->isUnsigned()); From b9ad72a1ea39242d401e775e979173714d1e0655 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 19 Nov 2025 15:05:36 +0100 Subject: [PATCH 04/10] Opt-in for unsigned column defaults. --- .../schema-dump-test_comparisons_mysql.lock | Bin 4781 -> 4781 bytes .../Diff/default/the_diff_default_mysql.php | 27 +++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock index 0cc5d0bb41d162e6eef0dde360321c8becc689fb..44bd4f7e03e25184786827905e43407c12ad09f4 100644 GIT binary patch delta 54 zcmZ3hx>j|j|<0Y<}(2W?py4JRHnpIpzXyg8Ch4a_T null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->changeColumn('title', 'text', [ 'default' => null, @@ -53,7 +52,6 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->update(); @@ -63,10 +61,16 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->update(); - $this->table('categories') + $this->table('categories', ['id' => false, 'primary_key' => ['id']]) + ->addColumn('id', 'integer', [ + 'autoIncrement' => true, + 'default' => null, + 'limit' => null, + 'null' => false, + 'signed' => false, + ]) ->addColumn('name', 'string', [ 'default' => null, 'limit' => 255, @@ -141,6 +145,21 @@ public function up(): void ->update(); $this->table('tags')->drop()->save(); + + $this->table('tags', ['id' => false, 'primary_key' => ['id']]) + ->addColumn('id', 'integer', [ + 'autoIncrement' => true, + 'default' => null, + 'limit' => 11, + 'null' => false, + 'signed' => true, + ]) + ->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]) + ->create(); } /** From d9e4fbd1cd8001adeb8bc6c66f4ebdba2df53406 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 19 Nov 2025 22:56:22 +0100 Subject: [PATCH 05/10] Opt-in for unsigned column defaults. --- src/Db/Adapter/MysqlAdapter.php | 5 +- src/Db/Table/Column.php | 13 ++++ src/View/Helper/MigrationHelper.php | 8 +++ .../Command/BakeMigrationDiffCommandTest.php | 3 + .../addRemove/the_diff_add_remove_mysql.php | 1 - .../Diff/default/the_diff_default_mysql.php | 64 +++++-------------- .../Diff/simple/the_diff_simple_mysql.php | 1 - ...incompatible_signed_primary_keys_mysql.php | 3 +- ...to_id_incompatible_signed_primary_keys.php | 1 - ...st_snapshot_with_non_default_collation.php | 2 +- ...0190928205056_first_fk_index_migration.php | 5 ++ 11 files changed, 49 insertions(+), 57 deletions(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index ca79798f..fe73932d 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -538,9 +538,8 @@ public function getColumns(string $tableName): array ->setScale($record['precision'] ?? null) ->setComment($record['comment']); - if ($record['unsigned'] ?? false) { - $column->setSigned(!$record['unsigned']); - } + // Always set unsigned property based on unsigned flag + $column->setUnsigned($record['unsigned'] ?? false); if ($record['autoIncrement'] ?? false) { $column->setIdentity(true); } diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index ea6c812e..f380dd36 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -547,6 +547,19 @@ public function getUnsigned(): bool return false; } + /** + * Sets whether field should be unsigned. + * + * @param bool $unsigned Unsigned + * @return $this + */ + public function setUnsigned(bool $unsigned) + { + $this->unsigned = $unsigned; + + return $this; + } + /** * Sets whether field should be signed. * diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index 42b3dbfa..6aa06152 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -405,6 +405,10 @@ public function getColumnOption(array $options): array if (!$isMysql) { unset($columnOptions['signed']); + } elseif (isset($columnOptions['signed']) && $columnOptions['signed'] === true) { + // Remove 'signed' => true since signed is now the default for integer columns + // Only output explicit 'signed' => false for unsigned columns + unset($columnOptions['signed']); } if (($isMysql || $isSqlserver) && !empty($columnOptions['collate'])) { @@ -526,6 +530,10 @@ public function attributes(TableSchemaInterface|string $table, string $column): $isMysql = $connection->getDriver() instanceof Mysql; if (!$isMysql) { unset($attributes['signed']); + } elseif (isset($attributes['signed']) && $attributes['signed'] === true) { + // Remove 'signed' => true since signed is now the default for integer columns + // Only output explicit 'signed' => false for unsigned columns + unset($attributes['signed']); } $defaultCollation = $tableSchema->getOptions()['collation'] ?? null; diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php index 070c65ec..902d6542 100644 --- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php @@ -174,6 +174,9 @@ public function testBakingDiff() { $this->skipIf(!env('DB_URL_COMPARE')); + // TODO: Fix FK constraint issue with signed/unsigned column compatibility + $this->markTestSkipped('FK constraint error - needs investigation'); + $this->runDiffBakingTest('Default'); } diff --git a/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php b/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php index d2da6226..a6dd8bf6 100644 --- a/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php +++ b/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php @@ -23,7 +23,6 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->update(); diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index a65056a7..a8baabab 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -55,7 +55,7 @@ public function up(): void ]) ->update(); - $this->table('users') + $this->table('tags') ->changeColumn('id', 'integer', [ 'default' => null, 'length' => null, @@ -63,14 +63,16 @@ public function up(): void 'null' => false, ]) ->update(); - $this->table('categories', ['id' => false, 'primary_key' => ['id']]) - ->addColumn('id', 'integer', [ - 'autoIncrement' => true, + + $this->table('users') + ->changeColumn('id', 'integer', [ 'default' => null, + 'length' => null, 'limit' => null, 'null' => false, - 'signed' => false, ]) + ->update(); + $this->table('categories') ->addColumn('name', 'string', [ 'default' => null, 'limit' => 255, @@ -80,7 +82,6 @@ public function up(): void 'default' => null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->addIndex( $this->index('user_id') @@ -109,7 +110,6 @@ public function up(): void 'default' => null, 'length' => null, 'null' => false, - 'signed' => false, ]) ->addColumn('average_note', 'decimal', [ 'after' => 'category_id', @@ -117,7 +117,6 @@ public function up(): void 'null' => true, 'precision' => 5, 'scale' => 5, - 'signed' => true, ]) ->addIndex( $this->index('slug') @@ -132,34 +131,6 @@ public function up(): void ->setName('rating_index') ) ->update(); - - $this->table('articles') - ->addForeignKey( - $this->foreignKey('category_id') - ->setReferencedTable('categories') - ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') - ->setName('articles_ibfk_1') - ) - ->update(); - - $this->table('tags')->drop()->save(); - - $this->table('tags', ['id' => false, 'primary_key' => ['id']]) - ->addColumn('id', 'integer', [ - 'autoIncrement' => true, - 'default' => null, - 'limit' => 11, - 'null' => false, - 'signed' => true, - ]) - ->addColumn('name', 'string', [ - 'default' => null, - 'limit' => 255, - 'null' => false, - ]) - ->create(); } /** @@ -177,18 +148,6 @@ public function down(): void 'user_id' )->save(); - $this->table('articles') - ->dropForeignKey( - 'category_id' - )->save(); - $this->table('tags') - ->addColumn('name', 'string', [ - 'default' => null, - 'limit' => 255, - 'null' => false, - ]) - ->create(); - $this->table('articles') ->removeIndexByName('UNIQUE_SLUG') ->removeIndexByName('category_id') @@ -248,6 +207,15 @@ public function down(): void ) ->update(); + $this->table('tags') + ->changeColumn('id', 'integer', [ + 'autoIncrement' => true, + 'default' => null, + 'length' => 11, + 'null' => false, + ]) + ->update(); + $this->table('users') ->changeColumn('id', 'integer', [ 'autoIncrement' => true, diff --git a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php index 9af124d4..93edeb6d 100644 --- a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php +++ b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php @@ -22,7 +22,6 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->changeColumn('rating', 'integer', [ 'default' => null, diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php index 2a82dde2..1a645545 100644 --- a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php +++ b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php @@ -23,7 +23,7 @@ public function up(): void 'default' => null, 'limit' => null, 'null' => false, - 'signed' => true, + 'signed' => false, ]) ->addPrimaryKey(['id']) ->create(); @@ -47,7 +47,6 @@ public function down(): void 'default' => null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->addPrimaryKey(['id']) ->create(); diff --git a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php index b831161a..82000913 100644 --- a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php +++ b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php @@ -23,7 +23,6 @@ public function up(): void 'default' => null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->addPrimaryKey(['id']) ->addColumn('title', 'string', [ diff --git a/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php b/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php index f27cecbd..4ab29f04 100644 --- a/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php +++ b/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php @@ -17,7 +17,7 @@ public function up(): void { $this->table('events') ->addColumn('title', 'string', [ - 'collation' => 'utf8mb3_hungarian_ci', + 'collation' => 'utf8_hungarian_ci', 'default' => null, 'limit' => 255, 'null' => true, diff --git a/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php b/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php index 5e9ea776..a24c27de 100644 --- a/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php +++ b/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php @@ -19,6 +19,7 @@ public function up() 'null' => false, 'limit' => 20, 'identity' => true, + 'signed' => false, ]) ->create(); @@ -35,6 +36,7 @@ public function up() 'null' => false, 'limit' => 20, 'identity' => true, + 'signed' => false, ]) ->create(); @@ -51,11 +53,13 @@ public function up() 'null' => false, 'limit' => 20, 'identity' => true, + 'signed' => false, ]) ->addColumn('table2_id', 'integer', [ 'null' => true, 'limit' => 20, 'after' => 'id', + 'signed' => false, ]) ->addIndex(['table2_id'], [ 'name' => 'table1_table2_id', @@ -69,6 +73,7 @@ public function up() ->addColumn('table3_id', 'integer', [ 'null' => true, 'limit' => 20, + 'signed' => false, ]) ->addIndex(['table3_id'], [ 'name' => 'table1_table3_id', From 22b59366776b0c20f211092a6643ac85d8c93358 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 20 Nov 2025 01:36:01 +0100 Subject: [PATCH 06/10] Fix tests. --- .../Command/BakeMigrationDiffCommandTest.php | 4 ++-- .../schema-dump-test_comparisons_mysql.lock | Bin 4781 -> 8605 bytes .../Diff/default/the_diff_default_mysql.php | 6 ++++++ .../20151218183450_CreateArticlesDefault.php | 2 +- .../20160128183952_CreateUsersDefault.php | 2 +- .../20160414193900_CreateTagsDefault.php | 2 +- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php index 902d6542..a0d3becf 100644 --- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php @@ -174,8 +174,8 @@ public function testBakingDiff() { $this->skipIf(!env('DB_URL_COMPARE')); - // TODO: Fix FK constraint issue with signed/unsigned column compatibility - $this->markTestSkipped('FK constraint error - needs investigation'); + Configure::write('Migrations.unsigned_primary_keys', true); + Configure::write('Migrations.unsigned_ints', true); $this->runDiffBakingTest('Default'); } diff --git a/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock index 44bd4f7e03e25184786827905e43407c12ad09f4..10092ee02d595b356d45800afedae1bd4fa91e8b 100644 GIT binary patch literal 8605 zcmeHMTW{Mo6z*@)4ko3lWYjKde~Aw6bK+N673L`DUXuY1VR4$zQZHZlO1u?`w6)F(RVCx!#qp6yPURPMbtY?hEh}|(ncb*`y!s@s z3&p<^@rG``6UmF8UZglysASxuREg1u_p`ES76!y6c;Olt=$u#`z4->KQ+Fc~qt|pE zr@+R@g=5m*_RIC%(kR(h{N&6v89!9w zL?lpgNd4Y_BLg=Dbh@@ zEV2ckN<ouuUMk~$)t;JKT}zwv{gKKAIFC^0pS&mV??cFce=uFtF^PAH>LV|_9Y!{HR{i$ zRhW1<(cf(l9k8vD+bTY+7#wj;18mNiA*s;%#NV>e`TMNKfXDg7i4QY4oyjVb6caE$ z!VN?PtEgtm&~%`F#1alws-C!(T8m>oWJ;z-hh7mU0Ebg&emn)i*nYVx6wQVloxsfR zHtpo7zdJg1o`-3dvX;klu1n{jWocQNOyzE-N5o-8G78kge2$~t>J*RToa!OMdLMlt zNO_QzmdjwGBSH%9fZ3WhPJH~iFs3~FKrEme*FA1##)e)Sln`asBdyh9Sz7o0Xq<-~ z%42d4nf97k1}ZBhAeT=kPKj30fi-V?+dnv;gWwY(dHTIwdy^k)S{YK_Kp?_>Z*C6- z{L=S0ers1G4qc+RU>psiS~q@3&d8DTN5Di-L{TNGD7ifQMy5t+a3fC96`74O(tScY#K_KvmjCOE4zfEX@x7*L9U49$9%AL=3~FHPrL3y2Xf>&j8T380 zL*oG0V}6K>0(q3&;t1GJZoStOB{oBUjcMOFFik{{RhZJRp1Jpa?r qM~(-!s>nxc)WC#$mZ*3Sok|A-m=;#TYmP$NzqG6o8c&zLAN~Qiz{HRM literal 4781 zcmdT{%TC-d6lLAd$g(Q}<>gtGf-0m!)zVp1LW(>Y&tQ=_shy~U5dYqD?L3^vv=qc+ zR!m}iALrhC?g?kR?9Q-57I0n2I1wh8d}X^kEcnE4#MKEe_>3EIbs1lalwbYevqZSx z0Xv|ZM=aQSzZK!%5)n*#mJAC+cOGX+nJO6P@P%t|Am{XSL{A@BP~7JB{s9hDA(ldu zwS5*OLM@ByU?O45*)NrmOC@l9#3I#e$bf5lk*{jORE@Q=#x|*ftC3 zPq>SW%OX2du@>%Z#wz=i@DCdfT}Ohs7O^xkQ)KYouswt;3(9vGKO#1TmTEZ&e`jwG z<{lDklJLTU5yDehEDockP(t%0iY1vH%?Ks@d*C+ig?fO@3AYRuyd{Y}ldF_=@qdAjNCjx0=4VlIb$ zvspu18lj_CWzR)dHNyyv9pncjOCYO2t6;$``+7FZKFEy46`l^Uc}}*`2iO z&SOBwhxLZ!SZ;Usc!%hYe?}J{&rcueqJd+*gC_kdx&O<-$8XdJUefX-X#u`O)j4{i zg%bC3EmN*<$1?O!*TPu!+o*TfrPGVEZ`0H0^2_w|m>1NcRPtA8H#J;(5~rzE(?9Fn zAjevGMRK;di9&LSXW;Lfs^Gw?wAR&koYC^t*uO~^^#!8!nS6iDzh1hVD02q(FfLRU9 z8fV{9?a(-H3c+vp1 zW=1E-esON}1VR50fo}X1+0XTy8}mEUqhI47uvT$(*zApc&3Xgl@qyoIeLm>au)_&G zR61v#pNH~=B_-^I=j01VR+V6xUY>qmC;m{we~Ok4GDz5~dnGYF6kd*=?%(|ZG$Gg6 diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index a8baabab..e91e1877 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -29,6 +29,7 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, + 'signed' => false, ]) ->changeColumn('title', 'text', [ 'default' => null, @@ -52,6 +53,7 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, + 'signed' => false, ]) ->update(); @@ -61,6 +63,7 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, + 'signed' => false, ]) ->update(); @@ -70,6 +73,7 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, + 'signed' => false, ]) ->update(); $this->table('categories') @@ -82,6 +86,7 @@ public function up(): void 'default' => null, 'limit' => null, 'null' => false, + 'signed' => false, ]) ->addIndex( $this->index('user_id') @@ -110,6 +115,7 @@ public function up(): void 'default' => null, 'length' => null, 'null' => false, + 'signed' => false, ]) ->addColumn('average_note', 'decimal', [ 'after' => 'category_id', diff --git a/tests/test_app/config/MigrationsDiffDefault/20151218183450_CreateArticlesDefault.php b/tests/test_app/config/MigrationsDiffDefault/20151218183450_CreateArticlesDefault.php index d561bb83..e05ac0cf 100644 --- a/tests/test_app/config/MigrationsDiffDefault/20151218183450_CreateArticlesDefault.php +++ b/tests/test_app/config/MigrationsDiffDefault/20151218183450_CreateArticlesDefault.php @@ -6,7 +6,7 @@ class CreateArticlesDefault extends BaseMigration { public function change(): void { - $table = $this->table('articles'); + $table = $this->table('articles', ['signed' => false]); $table ->addColumn('title', 'string', [ 'default' => null, diff --git a/tests/test_app/config/MigrationsDiffDefault/20160128183952_CreateUsersDefault.php b/tests/test_app/config/MigrationsDiffDefault/20160128183952_CreateUsersDefault.php index 6fd76720..f1e3b568 100644 --- a/tests/test_app/config/MigrationsDiffDefault/20160128183952_CreateUsersDefault.php +++ b/tests/test_app/config/MigrationsDiffDefault/20160128183952_CreateUsersDefault.php @@ -14,7 +14,7 @@ class CreateUsersDefault extends BaseMigration */ public function change(): void { - $table = $this->table('users'); + $table = $this->table('users', ['signed' => false]); $table->addColumn('username', 'string', [ 'default' => null, 'limit' => 255, diff --git a/tests/test_app/config/MigrationsDiffDefault/20160414193900_CreateTagsDefault.php b/tests/test_app/config/MigrationsDiffDefault/20160414193900_CreateTagsDefault.php index e1f87ef2..e4776347 100644 --- a/tests/test_app/config/MigrationsDiffDefault/20160414193900_CreateTagsDefault.php +++ b/tests/test_app/config/MigrationsDiffDefault/20160414193900_CreateTagsDefault.php @@ -14,7 +14,7 @@ class CreateTagsDefault extends BaseMigration */ public function change(): void { - $table = $this->table('tags'); + $table = $this->table('tags', ['signed' => false]); $table->addColumn('name', 'string', [ 'default' => null, 'limit' => 255, From deecb9d69ada5ed5413f93f5deafad836fe8cf1c Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 4 Dec 2025 18:24:28 +0100 Subject: [PATCH 07/10] Fix tests. --- src/Command/BakeMigrationDiffCommand.php | 6 +----- src/View/Helper/MigrationHelper.php | 2 +- .../decimalChange/mysql/the_diff_decimal_change_mysql.php | 1 - tests/comparisons/Diff/default/the_diff_default_mysql.php | 4 ---- ...snapshot_with_auto_id_compatible_signed_primary_keys.php | 1 - 5 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index e835dd06..ec2e2ef1 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -290,14 +290,10 @@ protected function getColumns(): void } } + // Only convert unsigned to signed if it actually changed if (isset($changedAttributes['unsigned'])) { $changedAttributes['signed'] = !$changedAttributes['unsigned']; unset($changedAttributes['unsigned']); - } else { - // badish hack - if (isset($column['unsigned']) && $column['unsigned'] === true) { - $changedAttributes['signed'] = false; - } } // For decimal columns, handle CakePHP schema -> migration attribute mapping diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index 35880441..0547c30d 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -407,7 +407,7 @@ public function getColumnOption(array $options): array if (!$isMysql) { unset($columnOptions['signed']); } elseif (isset($columnOptions['signed']) && $columnOptions['signed'] === true) { - // Remove 'signed' => true since signed is now the default for integer columns + // Remove 'signed' => true since signed is the default for integer columns // Only output explicit 'signed' => false for unsigned columns unset($columnOptions['signed']); } diff --git a/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php b/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php index a64755d9..a32449e1 100644 --- a/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php +++ b/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php @@ -22,7 +22,6 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => false, ]) ->changeColumn('amount', 'decimal', [ 'default' => null, diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index e91e1877..a889d9e7 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -29,7 +29,6 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => false, ]) ->changeColumn('title', 'text', [ 'default' => null, @@ -53,7 +52,6 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => false, ]) ->update(); @@ -63,7 +61,6 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => false, ]) ->update(); @@ -73,7 +70,6 @@ public function up(): void 'length' => null, 'limit' => null, 'null' => false, - 'signed' => false, ]) ->update(); $this->table('categories') diff --git a/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php b/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php index fed9f8d7..df2ea8ba 100644 --- a/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php +++ b/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php @@ -142,7 +142,6 @@ public function up(): void 'default' => null, 'limit' => null, 'null' => false, - 'signed' => true, ]) ->addPrimaryKey(['id']) ->addColumn('title', 'string', [ From 6c618250bb8f760d14336ac13bfc5f12f2aa22d0 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 4 Dec 2025 19:44:52 +0100 Subject: [PATCH 08/10] Add migration guide. --- config/app.example.php | 5 +- docs/en/index.rst | 16 +++++ docs/en/upgrading.rst | 140 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 docs/en/upgrading.rst diff --git a/config/app.example.php b/config/app.example.php index 7a83dd3f..3fc952b0 100644 --- a/config/app.example.php +++ b/config/app.example.php @@ -6,7 +6,8 @@ return [ 'Migrations' => [ - 'unsigned_primary_keys' => null, - 'column_null_default' => null, + 'unsigned_primary_keys' => null, // Default false + 'unsigned_ints' => null, // Default false, make sure this is aligned with the above config + 'column_null_default' => null, // Default false ], ]; diff --git a/docs/en/index.rst b/docs/en/index.rst index 0eff6a92..5db4fe1a 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -43,6 +43,12 @@ your application in your **config/app.php** file as explained in the `Database Configuration section `__. +Upgrading from 4.x +================== + +If you are upgrading from Migrations 4.x, please see the :doc:`upgrading` guide +for breaking changes and migration steps. + Overview ======== @@ -841,6 +847,7 @@ Feature Flags Migrations offers a few feature flags for compatibility. These features are disabled by default but can be enabled if required: * ``unsigned_primary_keys``: Should Migrations create primary keys as unsigned integers? (default: ``false``) +* ``unsigned_ints``: Should Migrations create all integer columns as unsigned? (default: ``false``) * ``column_null_default``: Should Migrations create columns as null by default? (default: ``false``) * ``add_timestamps_use_datetime``: Should Migrations use ``DATETIME`` type columns for the columns added by ``addTimestamps()``. @@ -849,9 +856,18 @@ Set them via Configure to enable (e.g. in ``config/app.php``):: 'Migrations' => [ 'unsigned_primary_keys' => true, + 'unsigned_ints' => true, 'column_null_default' => true, ], +.. note:: + + The ``unsigned_primary_keys`` and ``unsigned_ints`` options only affect MySQL databases. + When generating migrations with ``bake migration_snapshot`` or ``bake migration_diff``, + the ``signed`` attribute will only be included in the output for unsigned columns + (as ``'signed' => false``). Signed is the default for integer columns in MySQL, so + ``'signed' => true`` is never output. + Skipping the ``schema.lock`` file generation ============================================ diff --git a/docs/en/upgrading.rst b/docs/en/upgrading.rst new file mode 100644 index 00000000..23ab2e11 --- /dev/null +++ b/docs/en/upgrading.rst @@ -0,0 +1,140 @@ +Upgrading from 4.x to 5.x +######################### + +Migrations 5.x includes significant changes from 4.x. This guide outlines +the breaking changes and what you need to update when upgrading. + +Requirements +============ + +- **PHP 8.2+** is now required (was PHP 8.1+) +- **CakePHP 5.3+** is now required +- **Phinx has been removed** - The builtin backend is now the only supported backend + +If you were already using the builtin backend in 4.x (introduced in 4.3, default in 4.4), +the upgrade should be straightforward. See :doc:`upgrading-to-builtin-backend` for more +details on API differences between the phinx and builtin backends. + +Command Changes +=============== + +The phinx wrapper commands have been removed. The new command structure is: + +Migrations +---------- + +The migration commands remain unchanged: + +.. code-block:: bash + + bin/cake migrations migrate + bin/cake migrations rollback + bin/cake migrations status + bin/cake migrations mark_migrated + bin/cake migrations dump + +Seeds +----- + +Seed commands have changed: + +.. code-block:: bash + + # 4.x # 5.x + bin/cake migrations seed bin/cake seeds run + bin/cake migrations seed --seed X bin/cake seeds run X + +The new seed commands are: + +- ``bin/cake seeds run`` - Run seed classes +- ``bin/cake seeds run SeedName`` - Run a specific seed +- ``bin/cake seeds status`` - Show seed execution status +- ``bin/cake seeds reset`` - Reset seed execution tracking + +Maintaining Backward Compatibility +---------------------------------- + +If you need to maintain the old ``migrations seed`` command for existing scripts or +CI/CD pipelines, you can add command aliases in your ``src/Application.php``:: + + public function console(CommandCollection $commands): CommandCollection + { + $commands = $this->addConsoleCommands($commands); + + // Add backward compatibility alias + $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); + + return $commands; + } + +Removed Classes and Namespaces +============================== + +The following have been removed in 5.x: + +- ``Migrations\Command\Phinx\*`` - All phinx wrapper commands +- ``Migrations\Command\MigrationsCommand`` - Use ``bin/cake migrations`` entry point +- ``Migrations\Command\MigrationsSeedCommand`` - Use ``bin/cake seeds run`` +- ``Migrations\Command\MigrationsCacheBuildCommand`` - Schema cache is managed differently +- ``Migrations\Command\MigrationsCacheClearCommand`` - Schema cache is managed differently +- ``Migrations\Command\MigrationsCreateCommand`` - Use ``bin/cake bake migration`` + +If you have code that directly references any of these classes, you will need to update it. + +API Changes +=========== + +Adapter Query Results +--------------------- + +If your migrations use ``AdapterInterface::query()`` to fetch rows, the return type has +changed from a phinx result to ``Cake\Database\StatementInterface``:: + + // 4.x (phinx) + $stmt = $this->getAdapter()->query('SELECT * FROM articles'); + $rows = $stmt->fetchAll(); + $row = $stmt->fetch(); + + // 5.x (builtin) + $stmt = $this->getAdapter()->query('SELECT * FROM articles'); + $rows = $stmt->fetchAll('assoc'); + $row = $stmt->fetch('assoc'); + +New Features in 5.x +=================== + +5.x includes several new features: + +Seed Tracking +------------- + +Seeds are now tracked in a ``cake_seeds`` table by default, preventing accidental re-runs. +Use ``--force`` to run a seed again, or ``bin/cake seeds reset`` to clear tracking. +See :doc:`seeding` for more details. + +Check Constraints +----------------- + +Support for database check constraints via ``addCheckConstraint()``. +See :doc:`writing-migrations` for usage details. + +MySQL ALTER Options +------------------- + +Support for ``ALGORITHM`` and ``LOCK`` options on MySQL ALTER TABLE operations, +allowing control over how MySQL performs schema changes. + +insertOrSkip() for Seeds +------------------------ + +New ``insertOrSkip()`` method for seeds to insert records only if they don't already exist, +making seeds more idempotent. + +Migration File Compatibility +============================ + +Your existing migration files should work without changes in most cases. The builtin backend +provides the same API as phinx for common operations. + +If you encounter issues with existing migrations, please report them at +https://github.com/cakephp/migrations/issues From f26f86c7b0ec200d926dbd318a51ea5339d38173 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 11 Dec 2025 21:10:45 +0100 Subject: [PATCH 09/10] Remove RunInSeparateProcess from ColumnTest Use tearDown() to restore Configure defaults instead of running tests in separate processes. This improves test performance. --- tests/TestCase/Db/Table/ColumnTest.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php index 6400417e..0652149f 100644 --- a/tests/TestCase/Db/Table/ColumnTest.php +++ b/tests/TestCase/Db/Table/ColumnTest.php @@ -8,12 +8,20 @@ use Cake\Database\ValueBinder; use Migrations\Db\Literal; use Migrations\Db\Table\Column; -use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; use RuntimeException; class ColumnTest extends TestCase { + protected function tearDown(): void + { + parent::tearDown(); + // Restore bootstrap defaults + Configure::write('Migrations.unsigned_primary_keys', true); + Configure::write('Migrations.column_null_default', true); + Configure::delete('Migrations.unsigned_ints'); + } + public function testNullConstructorParameter() { $column = new Column(name: 'title'); @@ -51,7 +59,6 @@ public function testSetOptionsIdentity() $this->assertTrue($column->isIdentity()); } - #[RunInSeparateProcess] public function testColumnNullFeatureFlag() { $column = new Column(); @@ -179,7 +186,6 @@ public function testToArrayRespectsExplicitSigned(): void $this->assertFalse($result['unsigned']); } - #[RunInSeparateProcess] public function testUnsignedIntsConfiguration(): void { // Without configuration, integers default to signed @@ -203,7 +209,6 @@ public function testUnsignedIntsConfiguration(): void $this->assertTrue($column->isSigned()); } - #[RunInSeparateProcess] public function testUnsignedPrimaryKeysConfiguration(): void { // Without configuration, identity columns default to signed @@ -232,7 +237,6 @@ public function testUnsignedPrimaryKeysConfiguration(): void $this->assertTrue($column->isSigned()); } - #[RunInSeparateProcess] public function testBothUnsignedConfigurationsWork(): void { Configure::write('Migrations.unsigned_primary_keys', true); @@ -254,7 +258,6 @@ public function testBothUnsignedConfigurationsWork(): void $this->assertFalse($stringColumn->isUnsigned()); } - #[RunInSeparateProcess] public function testUnsignedConfigurationDoesNotAffectNonIntegerTypes(): void { Configure::write('Migrations.unsigned_ints', true); From 5c4bb5f59291234e24424ada51f01170da8372e5 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 11 Dec 2025 23:53:44 +0100 Subject: [PATCH 10/10] Fix MigrationHelperTest failing when run with adapter tests The adapter tests (MysqlAdapterTest, etc.) drop and recreate the database in their setUp method, which destroys the schema tables created by SchemaLoader. MigrationHelperTest then fails because the users and special_tags tables no longer exist. Use #[RunTestsInSeparateProcesses] to ensure MigrationHelperTest runs in isolation with its own fresh schema. --- tests/TestCase/View/Helper/MigrationHelperTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/TestCase/View/Helper/MigrationHelperTest.php b/tests/TestCase/View/Helper/MigrationHelperTest.php index 0b26b934..42a4d008 100644 --- a/tests/TestCase/View/Helper/MigrationHelperTest.php +++ b/tests/TestCase/View/Helper/MigrationHelperTest.php @@ -20,10 +20,15 @@ use Cake\TestSuite\TestCase; use Cake\View\View; use Migrations\View\Helper\MigrationHelper; +use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the ConfigurationTrait + * + * Note: This test must run in a separate process because adapter tests earlier + * in the test suite drop and recreate the database, destroying the schema. */ +#[RunTestsInSeparateProcesses] class MigrationHelperTest extends TestCase { /**