Skip to content
Open
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
5 changes: 3 additions & 2 deletions config/app.example.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
],
];
16 changes: 16 additions & 0 deletions docs/en/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ your application in your **config/app.php** file as explained in the `Database
Configuration section
<https://book.cakephp.org/5/en/orm/database-basics.html#database-configuration>`__.

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
========

Expand Down Expand Up @@ -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()``.
Expand All @@ -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
============================================

Expand Down
140 changes: 140 additions & 0 deletions docs/en/upgrading.rst
Original file line number Diff line number Diff line change
@@ -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
6 changes: 1 addition & 5 deletions src/Command/BakeMigrationDiffCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions src/Db/Adapter/MysqlAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -609,9 +609,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);
}
Expand Down
92 changes: 79 additions & 13 deletions src/Db/Table/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -493,6 +513,63 @@ public function getComment(): ?string
return $this->comment;
}

/**
* Gets whether field should 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
* - If integer type and Migrations.unsigned_ints is true, returns true
* - Otherwise defaults to false (signed)
*
* @return bool
*/
public function getUnsigned(): bool
{
// If explicitly set, use that value
if ($this->unsigned !== null) {
return $this->unsigned;
}

$integerTypes = [
self::INTEGER,
self::BIGINTEGER,
self::SMALLINTEGER,
self::TINYINTEGER,
];

// 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;
}

/**
* 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.
*
Expand All @@ -515,18 +592,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.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should deprecate this on the Cake ORM maybe?
We dont need isUnsigned() and isSigned() at the same time, do we?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should deprecate this on the Cake ORM maybe?

The ORM API is part of the additions in 5.3. If we deprecate that it requires changes to cakephp/database and the orm. I thought we could align on what we already have in the core.

Copy link
Member Author

@dereuromark dereuromark Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you propose we do?
Since it we shouldnt have both positive and negative form at the same time here in the new major.

Or in other words: Wouldn't it be cleaner to only have one side, and not the other on top?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or in other words: Wouldn't it be cleaner to only have one side, and not the other on top?

Yes of course, one way would be preferable. However migrations and cake started in opposite directions, and in order for us to provide backwards compatibility we have to support both for a while.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thus my proposal to deprecate but keep around.
otherwise feel free to modify as needed so we can merge and move forward

*/
public function isSigned(): bool
{
return $this->getSigned();
return !$this->isUnsigned();
}

/**
Expand Down Expand Up @@ -826,7 +892,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,
Expand Down
8 changes: 8 additions & 0 deletions src/View/Helper/MigrationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,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 the default for integer columns
// Only output explicit 'signed' => false for unsigned columns
unset($columnOptions['signed']);
}

if (($isMysql || $isSqlserver) && !empty($columnOptions['collate'])) {
Expand Down Expand Up @@ -530,6 +534,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;
Expand Down
3 changes: 3 additions & 0 deletions tests/TestCase/Command/BakeMigrationDiffCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ public function testBakingDiff()
{
$this->skipIf(!env('DB_URL_COMPARE'));

Configure::write('Migrations.unsigned_primary_keys', true);
Configure::write('Migrations.unsigned_ints', true);

$this->runDiffBakingTest('Default');
}

Expand Down
Loading