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
85 changes: 83 additions & 2 deletions docs/en/writing-migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,69 @@ You can limit the maximum length of a column by using the ``limit`` option::
Changing Column Attributes
--------------------------

To change column type or options on an existing column, use the ``changeColumn()`` method.
There are two methods for modifying existing columns:

Updating Columns (Recommended)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To modify specific column attributes while preserving others, use the ``updateColumn()`` method.
This method automatically preserves unspecified attributes like defaults, nullability, limits, etc.::

<?php

use Migrations\BaseMigration;

class MyNewMigration extends BaseMigration
{
/**
* Migrate Up.
*/
public function up(): void
{
$users = $this->table('users');
// Make email nullable, preserving all other attributes
$users->updateColumn('email', null, ['null' => true])
->save();
}

/**
* Migrate Down.
*/
public function down(): void
{
$users = $this->table('users');
$users->updateColumn('email', null, ['null' => false])
->save();
}
}

You can pass ``null`` as the column type to preserve the existing type, or specify a new type::

// Preserve type and other attributes, only change nullability
$table->updateColumn('email', null, ['null' => true]);

// Change type to biginteger, preserve default and other attributes
$table->updateColumn('user_id', 'biginteger');

// Change default value, preserve everything else
$table->updateColumn('status', null, ['default' => 'active']);

The following attributes are automatically preserved by ``updateColumn()``:

- Default values
- NULL/NOT NULL constraint
- Column limit/length
- Decimal scale/precision
- Comments
- Signed/unsigned (for numeric types)
- Collation and encoding
- Enum/set values

Changing Columns (Traditional)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To completely replace a column definition, use the ``changeColumn()`` method.
This method requires you to specify all desired column attributes.
See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values::

<?php
Expand All @@ -721,7 +783,12 @@ See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values::
public function up(): void
{
$users = $this->table('users');
$users->changeColumn('email', 'string', ['limit' => 255])
// Must specify all attributes
$users->changeColumn('email', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
])
->save();
}

Expand All @@ -734,6 +801,20 @@ See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values::
}
}

You can enable attribute preservation with ``changeColumn()`` by passing
``'preserveUnspecified' => true`` in the options::

$table->changeColumn('email', 'string', [
'null' => true,
'preserveUnspecified' => true,
]);

.. note::

For most use cases, ``updateColumn()`` is recommended as it is safer and requires
less code. Use ``changeColumn()`` when you need to completely redefine a column
or when working with legacy code that expects the traditional behavior.

Working With Indexes
--------------------

Expand Down
147 changes: 145 additions & 2 deletions src/Db/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,19 +389,78 @@ public function renameColumn(string $oldName, string $newName)
return $this;
}

/**
* Update a table column, preserving unspecified attributes.
*
* This is the recommended method for modifying columns as it automatically
* preserves existing column attributes (default, null, limit, etc.) unless
* explicitly overridden.
*
* @param string $columnName Column Name
* @param string|\Migrations\Db\Table\Column|null $newColumnType New Column Type (pass null to preserve existing type)
* @param array<string, mixed> $options Options
* @return $this
*/
public function updateColumn(string $columnName, string|Column|null $newColumnType, array $options = [])
{
if (!($newColumnType instanceof Column)) {
$options['preserveUnspecified'] = true;
}

return $this->changeColumn($columnName, $newColumnType, $options);
}

/**
* Change a table column type.
*
* Note: This method replaces the column definition. Consider using updateColumn()
* instead, which preserves unspecified attributes by default.
*
* @param string $columnName Column Name
* @param string|\Migrations\Db\Table\Column $newColumnType New Column Type
* @param string|\Migrations\Db\Table\Column|null $newColumnType New Column Type (pass null to preserve existing type)
* @param array<string, mixed> $options Options
* @return $this
*/
public function changeColumn(string $columnName, string|Column $newColumnType, array $options = [])
public function changeColumn(string $columnName, string|Column|null $newColumnType, array $options = [])
{
if ($newColumnType instanceof Column) {
if ($options) {
throw new InvalidArgumentException(
'Cannot specify options array when passing a Column object. ' .
'Set all properties directly on the Column object instead.',
);
}
$action = new ChangeColumn($this->table, $columnName, $newColumnType);
} else {
// Check if we should preserve existing column attributes
$preserveUnspecified = $options['preserveUnspecified'] ?? false; // Default to false for BC
unset($options['preserveUnspecified']);

// If type is null, preserve the existing type
if ($newColumnType === null) {
if (!$this->hasColumn($columnName)) {
throw new RuntimeException(
"Cannot preserve column type for '$columnName' - column does not exist in table '{$this->getName()}'",
);
}
$existingColumn = $this->getColumn($columnName);
if ($existingColumn === null) {
throw new RuntimeException(
"Cannot retrieve column definition for '$columnName' in table '{$this->getName()}'",
);
}
$newColumnType = $existingColumn->getType();
}

if ($preserveUnspecified && $this->hasColumn($columnName)) {
// Get existing column definition
$existingColumn = $this->getColumn($columnName);
if ($existingColumn !== null) {
// Merge existing attributes with new ones
$options = $this->mergeColumnOptions($existingColumn, $newColumnType, $options);
}
}

$action = ChangeColumn::build($this->table, $columnName, $newColumnType, $options);
}
$this->actions->addAction($action);
Expand Down Expand Up @@ -829,4 +888,88 @@ protected function executeActions(bool $exists): void
$plan = new Plan($this->actions);
$plan->execute($this->getAdapter());
}

/**
* Merges existing column options with new options.
* Only attributes that are explicitly specified in the new options will override existing ones.
*
* @param \Migrations\Db\Table\Column $existingColumn Existing column definition
* @param string $newColumnType New column type
* @param array<string, mixed> $options New options
* @return array<string, mixed> Merged options
*/
protected function mergeColumnOptions(Column $existingColumn, string $newColumnType, array $options): array
{
// Determine if type is changing
$newTypeString = (string)$newColumnType;
$existingTypeString = (string)$existingColumn->getType();
$typeChanging = $newTypeString !== $existingTypeString;

// Build array of existing column attributes
$existingOptions = [];

// Only preserve limit if type is not changing or limit is not explicitly set
if (!$typeChanging && !array_key_exists('limit', $options) && !array_key_exists('length', $options)) {
$limit = $existingColumn->getLimit();
if ($limit !== null) {
$existingOptions['limit'] = $limit;
}
}

// Preserve default if not explicitly set
if (!array_key_exists('default', $options)) {
$existingOptions['default'] = $existingColumn->getDefault();
}

// Preserve null if not explicitly set
if (!isset($options['null'])) {
$existingOptions['null'] = $existingColumn->getNull();
}

// Preserve scale/precision if not explicitly set
if (!array_key_exists('scale', $options) && !array_key_exists('precision', $options)) {
$scale = $existingColumn->getScale();
if ($scale !== null) {
$existingOptions['scale'] = $scale;
}
$precision = $existingColumn->getPrecision();
if ($precision !== null) {
$existingOptions['precision'] = $precision;
}
}

// Preserve comment if not explicitly set
if (!array_key_exists('comment', $options)) {
$comment = $existingColumn->getComment();
if ($comment !== null) {
$existingOptions['comment'] = $comment;
}
}

// Preserve signed if not explicitly set (always has a value)
if (!isset($options['signed'])) {
$existingOptions['signed'] = $existingColumn->getSigned();
}

// Preserve collation if not explicitly set
if (!isset($options['collation'])) {
$collation = $existingColumn->getCollation();
if ($collation !== null) {
$existingOptions['collation'] = $collation;
}
}

// Preserve encoding if not explicitly set
if (!isset($options['encoding'])) {
$encoding = $existingColumn->getEncoding();
if ($encoding !== null) {
$existingOptions['encoding'] = $encoding;
}
}

// Note: enum/set values are not preserved as schema reflection doesn't populate them

// New options override existing ones
return array_merge($existingOptions, $options);
}
}
Loading
Loading