feat: Detect column renames in migrations to prevent data loss#4636
feat: Detect column renames in migrations to prevent data loss#4636
Conversation
- Add renameColumns field to TableMigration protocol - Detect column renames when same type columns are added/removed - Generate ALTER TABLE RENAME COLUMN statements in PostgreSQL - Handle renamed columns with nullability/default changes - Add comprehensive tests for rename detection and SQL generation Co-authored-by: marcelomendoncasoares <6807059+marcelomendoncasoares@users.noreply.github.com>
- Add warnings for deleted columns only once (after rename detection) - Ensure warnings for type-changed columns are still generated - All 139 migration tests now pass Co-authored-by: marcelomendoncasoares <6807059+marcelomendoncasoares@users.noreply.github.com>
Co-authored-by: marcelomendoncasoares <6807059+marcelomendoncasoares@users.noreply.github.com>
marcelomendoncasoares
left a comment
There was a problem hiding this comment.
@copilot On top of the comments, please also add two tests:
- A migration e2e test that assert the correct behavior of the feature with a real database.
- A deserialization test that assert that a previous
migration.jsonwith no rename definition can be deserialized correctly.
| addColumns: List<ColumnDefinition> | ||
| deleteColumns: List<String> | ||
| modifyColumns: List<ColumnMigration> | ||
| renameColumns: Map<String, String> |
There was a problem hiding this comment.
This should be nullable to avoid breaking the deserialization of older migration.json files.
There was a problem hiding this comment.
Made renameColumns nullable in the protocol definition (commit 1d0f012). This ensures backwards compatibility with older migration.json files that don't have this field.
There was a problem hiding this comment.
This file is broken. Re-run the util/generate_all after the next changes and await as long as needed for the code generation to finish properly.
There was a problem hiding this comment.
Regenerated the chat protocol file successfully (commit 1d0f012). The file now includes the updated TableMigration protocol with nullable renameColumns.
| // Detect column renames: Match deleted columns with added columns | ||
| // that have compatible types and properties | ||
| var renameColumns = <String, String>{}; | ||
| var deleteColumnsCopy = List<String>.from(deleteColumns); | ||
| var addColumnsCopy = List<ColumnDefinition>.from(addColumns); | ||
|
|
||
| for (var deleteColumnName in deleteColumnsCopy) { | ||
| var srcColumn = srcTable.findColumnNamed(deleteColumnName); | ||
| if (srcColumn == null) continue; | ||
|
|
||
| // Look for a matching added column with compatible type | ||
| for (var addColumn in addColumnsCopy) { | ||
| // Check if columns can be considered a rename: | ||
| // - Same column type | ||
| // - Same or compatible nullability (can add/remove nullable via migration) | ||
| // - Same or compatible default (can change default via migration) | ||
| if (srcColumn.columnType == addColumn.columnType && | ||
| srcColumn.dartType == addColumn.dartType && | ||
| srcColumn.vectorDimension == addColumn.vectorDimension) { | ||
| // This looks like a rename | ||
| renameColumns[deleteColumnName] = addColumn.name; | ||
| deleteColumns.remove(deleteColumnName); | ||
| addColumns.remove(addColumn); | ||
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Separate this logic for better readability. Instead of inline comments, place the explanation of the logic in the doc comment of a private method or function.
There was a problem hiding this comment.
Extracted the rename detection logic into _detectColumnRenames function with comprehensive doc comments (commit 1d0f012). The function clearly explains the matching criteria and return value.
| // Check if this column was renamed | ||
| var renamedTo = renameColumns[srcColumn.name]; | ||
| var dstColumn = renamedTo != null | ||
| ? dstTable.findColumnNamed(renamedTo) | ||
| : dstTable.findColumnNamed(srcColumn.name); | ||
|
|
||
| if (dstColumn == null) { | ||
| continue; | ||
| } | ||
| if (!srcColumn.like(dstColumn)) { | ||
| if (srcColumn.canMigrateTo(dstColumn)) { | ||
|
|
||
| // For renamed columns, we need to check if properties other than name changed | ||
| // For non-renamed columns, we use the standard like() check | ||
| bool columnsAreDifferent; | ||
| if (renamedTo != null) { | ||
| // Renamed column - check everything except name | ||
| columnsAreDifferent = | ||
| srcColumn.isNullable != dstColumn.isNullable || | ||
| srcColumn.columnDefault != dstColumn.columnDefault || | ||
| srcColumn.columnType != dstColumn.columnType || | ||
| srcColumn.dartType != dstColumn.dartType || | ||
| srcColumn.vectorDimension != dstColumn.vectorDimension; | ||
| } else { | ||
| // Regular column - use standard comparison | ||
| columnsAreDifferent = !srcColumn.like(dstColumn); | ||
| } | ||
|
|
||
| if (columnsAreDifferent) { | ||
| // Check if the column can be migrated (type compatible) | ||
| bool canMigrate; | ||
| if (renamedTo != null) { | ||
| // For renamed columns, check if types are compatible (ignoring name) | ||
| canMigrate = | ||
| srcColumn.columnType == dstColumn.columnType && | ||
| (srcColumn.dartType == null || | ||
| dstColumn.dartType == null || | ||
| srcColumn.dartType == dstColumn.dartType) && | ||
| srcColumn.vectorDimension == dstColumn.vectorDimension; | ||
| } else { | ||
| canMigrate = srcColumn.canMigrateTo(dstColumn); | ||
| } |
There was a problem hiding this comment.
Same of the above comment: Separate this logic for better readability. Instead of inline comments, place the explanation of the logic in the doc comment of a private method or function.
There was a problem hiding this comment.
Extracted the column modification logic into _processColumnModifications function with comprehensive doc comments (commit 1d0f012). The function is now more readable and the doc comment explains what it populates.
- Made renameColumns nullable in TableMigration protocol for backwards compatibility - Refactored rename detection logic into _detectColumnRenames function with doc comments - Refactored column modification logic into _processColumnModifications function with doc comments - Updated extensions to handle nullable renameColumns - Fixed all test files to handle nullable renameColumns - Added table_migration_deserialization_test.dart for backwards compatibility testing - Added column_rename_e2e_test.dart for end-to-end database testing - Regenerated protocol files Co-authored-by: marcelomendoncasoares <6807059+marcelomendoncasoares@users.noreply.github.com>
Added both requested tests (commit 1d0f012):
All tests passing. |
Migration system now detects column renames and generates
RENAME COLUMNstatements instead ofDROP/ADD, preventing data loss when using the experimentalcolumnoverride feature.Changes
Protocol
renameColumns: Map<String, String>?toTableMigration(nullable for backwards compatibility)Detection Logic (
migration.dart)_detectColumnRenames()function with comprehensive doc comments_processColumnModifications()function with comprehensive doc commentsSQL Generation (
extensions.dart)ALTER TABLE RENAME COLUMNstatementsrenameColumnssafelyUsage Example
Migration Output:
Testing
Pre-launch Checklist
///), and made sure that the documentation follows the same style as other Serverpod documentation. I checked spelling and grammar.Breaking changes
None. Changes are backwards compatible—columns without type compatibility still use delete/add behavior, and old migration.json files without
renameColumnsdeserialize correctly.Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.