Skip to content

Commit 93089cc

Browse files
authored
changeColumn() defaulting. (#941)
* changeColumn() defaulting. * Add test for Column object with updateColumn() - Tests that updateColumn() works correctly when passed a Column object - Addresses review feedback to ensure Column type support is tested * Cleanup. * Add test for resetting. * Adjust as per review.
1 parent d21076b commit 93089cc

File tree

3 files changed

+472
-4
lines changed

3 files changed

+472
-4
lines changed

docs/en/writing-migrations.rst

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,69 @@ You can limit the maximum length of a column by using the ``limit`` option::
706706
Changing Column Attributes
707707
--------------------------
708708

709-
To change column type or options on an existing column, use the ``changeColumn()`` method.
709+
There are two methods for modifying existing columns:
710+
711+
Updating Columns (Recommended)
712+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
713+
714+
To modify specific column attributes while preserving others, use the ``updateColumn()`` method.
715+
This method automatically preserves unspecified attributes like defaults, nullability, limits, etc.::
716+
717+
<?php
718+
719+
use Migrations\BaseMigration;
720+
721+
class MyNewMigration extends BaseMigration
722+
{
723+
/**
724+
* Migrate Up.
725+
*/
726+
public function up(): void
727+
{
728+
$users = $this->table('users');
729+
// Make email nullable, preserving all other attributes
730+
$users->updateColumn('email', null, ['null' => true])
731+
->save();
732+
}
733+
734+
/**
735+
* Migrate Down.
736+
*/
737+
public function down(): void
738+
{
739+
$users = $this->table('users');
740+
$users->updateColumn('email', null, ['null' => false])
741+
->save();
742+
}
743+
}
744+
745+
You can pass ``null`` as the column type to preserve the existing type, or specify a new type::
746+
747+
// Preserve type and other attributes, only change nullability
748+
$table->updateColumn('email', null, ['null' => true]);
749+
750+
// Change type to biginteger, preserve default and other attributes
751+
$table->updateColumn('user_id', 'biginteger');
752+
753+
// Change default value, preserve everything else
754+
$table->updateColumn('status', null, ['default' => 'active']);
755+
756+
The following attributes are automatically preserved by ``updateColumn()``:
757+
758+
- Default values
759+
- NULL/NOT NULL constraint
760+
- Column limit/length
761+
- Decimal scale/precision
762+
- Comments
763+
- Signed/unsigned (for numeric types)
764+
- Collation and encoding
765+
- Enum/set values
766+
767+
Changing Columns (Traditional)
768+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
769+
770+
To completely replace a column definition, use the ``changeColumn()`` method.
771+
This method requires you to specify all desired column attributes.
710772
See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values::
711773

712774
<?php
@@ -721,7 +783,12 @@ See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values::
721783
public function up(): void
722784
{
723785
$users = $this->table('users');
724-
$users->changeColumn('email', 'string', ['limit' => 255])
786+
// Must specify all attributes
787+
$users->changeColumn('email', 'string', [
788+
'limit' => 255,
789+
'null' => true,
790+
'default' => null,
791+
])
725792
->save();
726793
}
727794

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

804+
You can enable attribute preservation with ``changeColumn()`` by passing
805+
``'preserveUnspecified' => true`` in the options::
806+
807+
$table->changeColumn('email', 'string', [
808+
'null' => true,
809+
'preserveUnspecified' => true,
810+
]);
811+
812+
.. note::
813+
814+
For most use cases, ``updateColumn()`` is recommended as it is safer and requires
815+
less code. Use ``changeColumn()`` when you need to completely redefine a column
816+
or when working with legacy code that expects the traditional behavior.
817+
737818
Working With Indexes
738819
--------------------
739820

src/Db/Table.php

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,19 +389,78 @@ public function renameColumn(string $oldName, string $newName)
389389
return $this;
390390
}
391391

392+
/**
393+
* Update a table column, preserving unspecified attributes.
394+
*
395+
* This is the recommended method for modifying columns as it automatically
396+
* preserves existing column attributes (default, null, limit, etc.) unless
397+
* explicitly overridden.
398+
*
399+
* @param string $columnName Column Name
400+
* @param string|\Migrations\Db\Table\Column|null $newColumnType New Column Type (pass null to preserve existing type)
401+
* @param array<string, mixed> $options Options
402+
* @return $this
403+
*/
404+
public function updateColumn(string $columnName, string|Column|null $newColumnType, array $options = [])
405+
{
406+
if (!($newColumnType instanceof Column)) {
407+
$options['preserveUnspecified'] = true;
408+
}
409+
410+
return $this->changeColumn($columnName, $newColumnType, $options);
411+
}
412+
392413
/**
393414
* Change a table column type.
394415
*
416+
* Note: This method replaces the column definition. Consider using updateColumn()
417+
* instead, which preserves unspecified attributes by default.
418+
*
395419
* @param string $columnName Column Name
396-
* @param string|\Migrations\Db\Table\Column $newColumnType New Column Type
420+
* @param string|\Migrations\Db\Table\Column|null $newColumnType New Column Type (pass null to preserve existing type)
397421
* @param array<string, mixed> $options Options
398422
* @return $this
399423
*/
400-
public function changeColumn(string $columnName, string|Column $newColumnType, array $options = [])
424+
public function changeColumn(string $columnName, string|Column|null $newColumnType, array $options = [])
401425
{
402426
if ($newColumnType instanceof Column) {
427+
if ($options) {
428+
throw new InvalidArgumentException(
429+
'Cannot specify options array when passing a Column object. ' .
430+
'Set all properties directly on the Column object instead.',
431+
);
432+
}
403433
$action = new ChangeColumn($this->table, $columnName, $newColumnType);
404434
} else {
435+
// Check if we should preserve existing column attributes
436+
$preserveUnspecified = $options['preserveUnspecified'] ?? false; // Default to false for BC
437+
unset($options['preserveUnspecified']);
438+
439+
// If type is null, preserve the existing type
440+
if ($newColumnType === null) {
441+
if (!$this->hasColumn($columnName)) {
442+
throw new RuntimeException(
443+
"Cannot preserve column type for '$columnName' - column does not exist in table '{$this->getName()}'",
444+
);
445+
}
446+
$existingColumn = $this->getColumn($columnName);
447+
if ($existingColumn === null) {
448+
throw new RuntimeException(
449+
"Cannot retrieve column definition for '$columnName' in table '{$this->getName()}'",
450+
);
451+
}
452+
$newColumnType = $existingColumn->getType();
453+
}
454+
455+
if ($preserveUnspecified && $this->hasColumn($columnName)) {
456+
// Get existing column definition
457+
$existingColumn = $this->getColumn($columnName);
458+
if ($existingColumn !== null) {
459+
// Merge existing attributes with new ones
460+
$options = $this->mergeColumnOptions($existingColumn, $newColumnType, $options);
461+
}
462+
}
463+
405464
$action = ChangeColumn::build($this->table, $columnName, $newColumnType, $options);
406465
}
407466
$this->actions->addAction($action);
@@ -829,4 +888,88 @@ protected function executeActions(bool $exists): void
829888
$plan = new Plan($this->actions);
830889
$plan->execute($this->getAdapter());
831890
}
891+
892+
/**
893+
* Merges existing column options with new options.
894+
* Only attributes that are explicitly specified in the new options will override existing ones.
895+
*
896+
* @param \Migrations\Db\Table\Column $existingColumn Existing column definition
897+
* @param string $newColumnType New column type
898+
* @param array<string, mixed> $options New options
899+
* @return array<string, mixed> Merged options
900+
*/
901+
protected function mergeColumnOptions(Column $existingColumn, string $newColumnType, array $options): array
902+
{
903+
// Determine if type is changing
904+
$newTypeString = (string)$newColumnType;
905+
$existingTypeString = (string)$existingColumn->getType();
906+
$typeChanging = $newTypeString !== $existingTypeString;
907+
908+
// Build array of existing column attributes
909+
$existingOptions = [];
910+
911+
// Only preserve limit if type is not changing or limit is not explicitly set
912+
if (!$typeChanging && !array_key_exists('limit', $options) && !array_key_exists('length', $options)) {
913+
$limit = $existingColumn->getLimit();
914+
if ($limit !== null) {
915+
$existingOptions['limit'] = $limit;
916+
}
917+
}
918+
919+
// Preserve default if not explicitly set
920+
if (!array_key_exists('default', $options)) {
921+
$existingOptions['default'] = $existingColumn->getDefault();
922+
}
923+
924+
// Preserve null if not explicitly set
925+
if (!isset($options['null'])) {
926+
$existingOptions['null'] = $existingColumn->getNull();
927+
}
928+
929+
// Preserve scale/precision if not explicitly set
930+
if (!array_key_exists('scale', $options) && !array_key_exists('precision', $options)) {
931+
$scale = $existingColumn->getScale();
932+
if ($scale !== null) {
933+
$existingOptions['scale'] = $scale;
934+
}
935+
$precision = $existingColumn->getPrecision();
936+
if ($precision !== null) {
937+
$existingOptions['precision'] = $precision;
938+
}
939+
}
940+
941+
// Preserve comment if not explicitly set
942+
if (!array_key_exists('comment', $options)) {
943+
$comment = $existingColumn->getComment();
944+
if ($comment !== null) {
945+
$existingOptions['comment'] = $comment;
946+
}
947+
}
948+
949+
// Preserve signed if not explicitly set (always has a value)
950+
if (!isset($options['signed'])) {
951+
$existingOptions['signed'] = $existingColumn->getSigned();
952+
}
953+
954+
// Preserve collation if not explicitly set
955+
if (!isset($options['collation'])) {
956+
$collation = $existingColumn->getCollation();
957+
if ($collation !== null) {
958+
$existingOptions['collation'] = $collation;
959+
}
960+
}
961+
962+
// Preserve encoding if not explicitly set
963+
if (!isset($options['encoding'])) {
964+
$encoding = $existingColumn->getEncoding();
965+
if ($encoding !== null) {
966+
$existingOptions['encoding'] = $encoding;
967+
}
968+
}
969+
970+
// Note: enum/set values are not preserved as schema reflection doesn't populate them
971+
972+
// New options override existing ones
973+
return array_merge($existingOptions, $options);
974+
}
832975
}

0 commit comments

Comments
 (0)