From ed690492cb611e461adb68f2d640c2c593fc59fa Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 3 Nov 2025 16:45:46 +0100 Subject: [PATCH 1/5] changeColumn() defaulting. --- docs/en/writing-migrations.rst | 85 +++++++++- src/Db/Table.php | 146 +++++++++++++++++- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 128 +++++++++++++++ 3 files changed, 355 insertions(+), 4 deletions(-) diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst index 308962c48..8df520ec8 100644 --- a/docs/en/writing-migrations.rst +++ b/docs/en/writing-migrations.rst @@ -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.:: + + 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:: table('users'); - $users->changeColumn('email', 'string', ['limit' => 255]) + // Must specify all attributes + $users->changeColumn('email', 'string', [ + 'limit' => 255, + 'null' => true, + 'default' => null, + ]) ->save(); } @@ -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 -------------------- diff --git a/src/Db/Table.php b/src/Db/Table.php index 0ff5ea968..68fd68153 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -389,19 +389,71 @@ 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 $options Options + * @return $this + */ + public function updateColumn(string $columnName, string|Column|null $newColumnType, array $options = []) + { + // Force preservation for updateColumn + $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 $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) { $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); @@ -829,4 +881,94 @@ 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 $options New options + * @return array 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 && !isset($options['limit']) && !isset($options['length'])) { + $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 (!isset($options['scale']) && !isset($options['precision'])) { + $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 (!isset($options['comment'])) { + $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; + } + } + + // Preserve values (for enum/set) if not explicitly set + if (!isset($options['values'])) { + $values = $existingColumn->getValues(); + if ($values !== null) { + $existingOptions['values'] = $values; + } + } + + // New options override existing ones + return array_merge($existingOptions, $options); + } } diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 27f688690..df33003b1 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -22,6 +22,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; +use RuntimeException; class MysqlAdapterTest extends TestCase { @@ -954,6 +955,133 @@ public function testChangeColumnDefaultToNull() $this->assertNull($rows[1]['Default']); } + public function testChangeColumnPreservesDefaultValue() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'original_default', 'null' => false, 'limit' => 100]) + ->save(); + + // Use updateColumn which preserves by default + $table->updateColumn('column1', 'string', ['null' => true])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('original_default', $rows[1]['Default']); + $this->assertEquals('YES', $rows[1]['Null']); + $this->assertEquals('varchar(100)', $rows[1]['Type']); + } + + public function testChangeColumnPreservesDefaultValueWithDifferentType() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'integer', ['default' => 42, 'null' => false]) + ->save(); + + // Use updateColumn to preserve default when changing type + $table->updateColumn('column1', 'biginteger', [])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('42', $rows[1]['Default']); + $this->assertEquals('NO', $rows[1]['Null']); + } + + public function testChangeColumnCanExplicitlyOverrideDefault() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'original_default']) + ->save(); + + // Explicitly change the default + $table->changeColumn('column1', 'string', ['default' => 'new_default'])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('new_default', $rows[1]['Default']); + } + + public function testChangeColumnCanDisablePreserveUnspecified() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'original_default', 'limit' => 100]) + ->save(); + + // Disable preservation, default should be removed + $table->changeColumn('column1', 'string', ['null' => true, 'preserveUnspecified' => false])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertNull($rows[1]['Default']); + } + + public function testChangeColumnWithNullTypePreservesType() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100]) + ->save(); + + // Use updateColumn with null type to preserve everything + $table->updateColumn('column1', null, ['null' => true])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('varchar(100)', $rows[1]['Type']); + $this->assertEquals('test', $rows[1]['Default']); + $this->assertEquals('YES', $rows[1]['Null']); + } + + public function testChangeColumnWithNullTypeOnNonExistentColumnThrows() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Cannot preserve column type for 'nonexistent'"); + + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string')->save(); + + // Try to use null type on non-existent column + $table->changeColumn('nonexistent', null, ['null' => true])->save(); + } + + public function testUpdateColumnPreservesAttributes() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100, 'null' => false]) + ->save(); + + // updateColumn should preserve by default + $table->updateColumn('column1', null, ['null' => true])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('varchar(100)', $rows[1]['Type']); + $this->assertEquals('test', $rows[1]['Default']); + $this->assertEquals('YES', $rows[1]['Null']); + } + + public function testChangeColumnDoesNotPreserveByDefault() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100]) + ->save(); + + // changeColumn should NOT preserve by default (backwards compatible) + $table->changeColumn('column1', 'string', ['null' => true])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + // Default should be lost + $this->assertNull($rows[1]['Default']); + $this->assertEquals('YES', $rows[1]['Null']); + } + + public function testChangeColumnWithPreserveUnspecifiedTrue() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100]) + ->save(); + + // changeColumn with explicit preserveUnspecified => true + $table->changeColumn('column1', 'string', ['null' => true, 'preserveUnspecified' => true])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + // Default should be preserved + $this->assertEquals('test', $rows[1]['Default']); + $this->assertEquals('YES', $rows[1]['Null']); + } + public function testChangeColumnEnum() { $table = new Table('t', [], $this->adapter); From 7509effd9764b08f982bbc2bc9843b82e63d3f5b Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 9 Nov 2025 09:09:00 +0100 Subject: [PATCH 2/5] Add test for Column object with updateColumn() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tests that updateColumn() works correctly when passed a Column object - Addresses review feedback to ensure Column type support is tested 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Db/Table.php | 17 +++++---- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 37 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Db/Table.php b/src/Db/Table.php index 68fd68153..04dc2885d 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -423,6 +423,15 @@ public function updateColumn(string $columnName, string|Column|null $newColumnTy public function changeColumn(string $columnName, string|Column|null $newColumnType, array $options = []) { if ($newColumnType instanceof Column) { + // Remove preserveUnspecified flag before checking if options are present + unset($options['preserveUnspecified']); + + 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 @@ -960,13 +969,7 @@ protected function mergeColumnOptions(Column $existingColumn, string $newColumnT } } - // Preserve values (for enum/set) if not explicitly set - if (!isset($options['values'])) { - $values = $existingColumn->getValues(); - if ($values !== null) { - $existingOptions['values'] = $values; - } - } + // Note: enum/set values are not preserved as schema reflection doesn't populate them // New options override existing ones return array_merge($existingOptions, $options); diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index df33003b1..0e8f3dd0f 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -1082,6 +1082,43 @@ public function testChangeColumnWithPreserveUnspecifiedTrue() $this->assertEquals('YES', $rows[1]['Null']); } + public function testUpdateColumnWithColumnObject() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100, 'null' => false]) + ->save(); + + // Use updateColumn with a Column object + $newColumn = new Column(); + $newColumn->setName('column1') + ->setType('string') + ->setLimit(255) + ->setNull(true); + $table->updateColumn('column1', $newColumn)->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('varchar(255)', $rows[1]['Type']); + $this->assertEquals('YES', $rows[1]['Null']); + } + + public function testUpdateColumnWithColumnObjectAndOptionsThrows() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot specify options array when passing a Column object'); + + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100]) + ->save(); + + // Passing both Column object and options array should throw an exception + $newColumn = new Column(); + $newColumn->setName('column1') + ->setType('string') + ->setLimit(200); + + $table->updateColumn('column1', $newColumn, ['limit' => 500]); + } + public function testChangeColumnEnum() { $table = new Table('t', [], $this->adapter); From c5aa8cefab7d3ee89fe367ad0c1a68c2116e42d2 Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 10 Nov 2025 10:38:20 +0100 Subject: [PATCH 3/5] Cleanup. --- src/Db/Table.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Db/Table.php b/src/Db/Table.php index 04dc2885d..76667b27a 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -403,8 +403,9 @@ public function renameColumn(string $oldName, string $newName) */ public function updateColumn(string $columnName, string|Column|null $newColumnType, array $options = []) { - // Force preservation for updateColumn - $options['preserveUnspecified'] = true; + if (!($newColumnType instanceof Column)) { + $options['preserveUnspecified'] = true; + } return $this->changeColumn($columnName, $newColumnType, $options); } @@ -423,9 +424,6 @@ public function updateColumn(string $columnName, string|Column|null $newColumnTy public function changeColumn(string $columnName, string|Column|null $newColumnType, array $options = []) { if ($newColumnType instanceof Column) { - // Remove preserveUnspecified flag before checking if options are present - unset($options['preserveUnspecified']); - if ($options) { throw new InvalidArgumentException( 'Cannot specify options array when passing a Column object. ' . From 7409a3c8f3772667c30c786ab950da95d3532298 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 11 Nov 2025 19:05:46 +0100 Subject: [PATCH 4/5] Add test for resetting. --- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 0e8f3dd0f..1e72f5df2 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -1119,6 +1119,26 @@ public function testUpdateColumnWithColumnObjectAndOptionsThrows() $table->updateColumn('column1', $newColumn, ['limit' => 500]); } + public function testUpdateColumnCanRemoveLengthConstraint() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['limit' => 100, 'default' => 'test']) + ->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('varchar(100)', $rows[1]['Type']); + $this->assertEquals('test', $rows[1]['Default']); + + // Try to remove length constraint by passing limit => null + $table->updateColumn('column1', 'text', ['limit' => null])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + // TEXT type in MySQL doesn't have a length specifier + $this->assertEquals('text', $rows[1]['Type']); + // TEXT columns in MySQL quote the default value + $this->assertStringContainsString('test', $rows[1]['Default']); // Default should be preserved + } + public function testChangeColumnEnum() { $table = new Table('t', [], $this->adapter); From 6f4121ca25277dd97b5f8d335b441ec87bd9512e Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 11 Nov 2025 20:20:28 +0100 Subject: [PATCH 5/5] Adjust as per review. --- src/Db/Table.php | 6 +- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 65 ++++++++++++++++++- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/Db/Table.php b/src/Db/Table.php index 76667b27a..852d1ecd3 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -909,7 +909,7 @@ protected function mergeColumnOptions(Column $existingColumn, string $newColumnT $existingOptions = []; // Only preserve limit if type is not changing or limit is not explicitly set - if (!$typeChanging && !isset($options['limit']) && !isset($options['length'])) { + if (!$typeChanging && !array_key_exists('limit', $options) && !array_key_exists('length', $options)) { $limit = $existingColumn->getLimit(); if ($limit !== null) { $existingOptions['limit'] = $limit; @@ -927,7 +927,7 @@ protected function mergeColumnOptions(Column $existingColumn, string $newColumnT } // Preserve scale/precision if not explicitly set - if (!isset($options['scale']) && !isset($options['precision'])) { + if (!array_key_exists('scale', $options) && !array_key_exists('precision', $options)) { $scale = $existingColumn->getScale(); if ($scale !== null) { $existingOptions['scale'] = $scale; @@ -939,7 +939,7 @@ protected function mergeColumnOptions(Column $existingColumn, string $newColumnT } // Preserve comment if not explicitly set - if (!isset($options['comment'])) { + if (!array_key_exists('comment', $options)) { $comment = $existingColumn->getComment(); if ($comment !== null) { $existingOptions['comment'] = $comment; diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 1e72f5df2..699ff7c9f 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -1119,7 +1119,7 @@ public function testUpdateColumnWithColumnObjectAndOptionsThrows() $table->updateColumn('column1', $newColumn, ['limit' => 500]); } - public function testUpdateColumnCanRemoveLengthConstraint() + public function testUpdateColumnWithTypeChangeToText() { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['limit' => 100, 'default' => 'test']) @@ -1129,8 +1129,8 @@ public function testUpdateColumnCanRemoveLengthConstraint() $this->assertEquals('varchar(100)', $rows[1]['Type']); $this->assertEquals('test', $rows[1]['Default']); - // Try to remove length constraint by passing limit => null - $table->updateColumn('column1', 'text', ['limit' => null])->save(); + // Change type to text (limit doesn't apply to TEXT types) + $table->updateColumn('column1', 'text')->save(); $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); // TEXT type in MySQL doesn't have a length specifier @@ -1139,6 +1139,65 @@ public function testUpdateColumnCanRemoveLengthConstraint() $this->assertStringContainsString('test', $rows[1]['Default']); // Default should be preserved } + public function testUpdateColumnCanRemoveLengthConstraintWithoutChangingType() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['limit' => 100, 'default' => 'test']) + ->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('varchar(100)', $rows[1]['Type']); + $this->assertEquals('test', $rows[1]['Default']); + + // Try to remove length constraint without changing type by passing length => null + // This tests the array_key_exists fix - isset() would fail here + $table->updateColumn('column1', 'string', ['length' => null])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + // Without explicit length, MySQL uses default varchar(255) + $this->assertEquals('varchar(255)', $rows[1]['Type']); + $this->assertEquals('test', $rows[1]['Default']); // Default should be preserved + } + + public function testUpdateColumnCanRemoveScaleAndPrecision() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'decimal', ['precision' => 10, 'scale' => 2, 'default' => '123.45']) + ->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('decimal(10,2)', $rows[1]['Type']); + $this->assertEquals('123.45', $rows[1]['Default']); + + // Try to remove scale/precision by passing null + $table->updateColumn('column1', 'decimal', ['precision' => null, 'scale' => null])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + // Without explicit precision/scale, MySQL uses default decimal(10,0) + $this->assertEquals('decimal(10,0)', $rows[1]['Type']); + $this->assertEquals('123', $rows[1]['Default']); // Default should be preserved (truncated to integer) + } + + public function testUpdateColumnCanRemoveComment() + { + $table = new Table('t', [], $this->adapter); + $table->addColumn('column1', 'string', ['limit' => 100, 'comment' => 'Original comment', 'default' => 'test']) + ->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + $this->assertEquals('varchar(100)', $rows[1]['Type']); + $this->assertEquals('test', $rows[1]['Default']); + // MySQL doesn't show comments in SHOW COLUMNS, but we can verify it was set + + // Try to remove comment by passing null + $table->updateColumn('column1', 'string', ['comment' => null])->save(); + + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM t'); + // Verify limit and default are preserved + $this->assertEquals('varchar(100)', $rows[1]['Type']); + $this->assertEquals('test', $rows[1]['Default']); + } + public function testChangeColumnEnum() { $table = new Table('t', [], $this->adapter);