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