Skip to content

feat: Detect column renames in migrations to prevent data loss#4636

Draft
Copilot wants to merge 5 commits intomainfrom
copilot/fix-rename-column-migration
Draft

feat: Detect column renames in migrations to prevent data loss#4636
Copilot wants to merge 5 commits intomainfrom
copilot/fix-rename-column-migration

Conversation

Copy link
Contributor

Copilot AI commented Jan 22, 2026

Migration system now detects column renames and generates RENAME COLUMN statements instead of DROP/ADD, preventing data loss when using the experimental column override feature.

Changes

Protocol

  • Added renameColumns: Map<String, String>? to TableMigration (nullable for backwards compatibility)

Detection Logic (migration.dart)

  • Extracted rename detection into _detectColumnRenames() function with comprehensive doc comments
  • Extracted column modification logic into _processColumnModifications() function with comprehensive doc comments
  • Match deleted/added columns by type compatibility (columnType, dartType, vectorDimension)
  • Extract matched pairs into rename operations before generating warnings
  • Handle renamed columns with concurrent nullability/default changes

SQL Generation (extensions.dart)

  • Generate ALTER TABLE RENAME COLUMN statements
  • Execute renames before other column operations to avoid conflicts
  • Handle nullable renameColumns safely

Usage Example

# Step 1: Add column override (generates RENAME migration)
class: User
table: users
fields:
  name: String, column=user_name

# Step 2: Rename field later (no migration, column name preserved)
class: User
table: users
fields:
  userName: String

Migration Output:

ALTER TABLE "users" RENAME COLUMN "name" TO "user_name";
-- vs previous behavior:
-- ALTER TABLE "users" DROP COLUMN "name";
-- ALTER TABLE "users" ADD COLUMN "user_name" text NOT NULL;

Testing

  • 25 tests covering rename detection and SQL generation
  • 7 tests for backwards-compatible deserialization of old migration.json files
  • 2 e2e tests validating column rename with real database and data preservation
  • All 146 migration tests passing

Pre-launch Checklist

  • I read the Contribute page and followed the process outlined there for submitting PRs.
  • This update contains only one single feature or bug fix and nothing else.
  • I read and followed the Dart Style Guide and formatted the code with dart format.
  • I updated/added relevant documentation (doc comments with ///), and made sure that the documentation follows the same style as other Serverpod documentation. I checked spelling and grammar.
  • I added new tests to check the change I am making.
  • All existing and new tests are passing.
  • Any breaking changes are documented below.

Breaking changes

None. Changes are backwards compatible—columns without type compatibility still use delete/add behavior, and old migration.json files without renameColumns deserialize correctly.

Original prompt

This section details on the original issue you should resolve

<issue_title>Renaming a column using the experimental column override should produce a rename column statement</issue_title>
<issue_description>### Problem to Solve

When an existing table model receives the experimental column key to override the name of the column in the database, the migration system produces ADD/REMOVE entries instead of RENAME. This largely defeats the purpose of using the key to rename columns for already existing tables.

Proposal

If the model field still has the same type and received a column override, the migration system should generate a rename statement instead of dropping and re-creating the table.

Use Case

A developer wants to change the field of a model without loosing data on the database. Then they could do:

  1. Override the column name to the new name of the field - while keeping the field name unchanged.
  2. Generate the model.
  3. Create a migration.
  4. Change the name of the field to the new name, removing the column override.
  5. Generate the model.

Since on steps 4-5 the table definition will not change (same columns with same types), no migration will be generated and the model field will be safely renamed with no data loss.

Alternatives

Modify the SQL for the rename manually.

Additional context

Will close #4602.

How experienced are you with this library?

Expert - Experienced and comfortable with using this library in complex projects

Are you interested in working on a PR for this?

  • I want to work on this</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 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.

Copilot AI and others added 3 commits January 22, 2026 15:48
- 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>
Copilot AI changed the title [WIP] Fix migration system to generate rename column statement feat: Detect column renames in migrations to prevent data loss Jan 22, 2026
Copy link
Collaborator

@marcelomendoncasoares marcelomendoncasoares left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.json with no rename definition can be deserialized correctly.

addColumns: List<ColumnDefinition>
deleteColumns: List<String>
modifyColumns: List<ColumnMigration>
renameColumns: Map<String, String>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be nullable to avoid breaking the deserialization of older migration.json files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made renameColumns nullable in the protocol definition (commit 1d0f012). This ensures backwards compatibility with older migration.json files that don't have this field.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regenerated the chat protocol file successfully (commit 1d0f012). The file now includes the updated TableMigration protocol with nullable renameColumns.

Comment on lines +200 to +226
// 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;
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted the rename detection logic into _detectColumnRenames function with comprehensive doc comments (commit 1d0f012). The function clearly explains the matching criteria and return value.

Comment on lines +247 to +286
// 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);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Contributor Author

Copilot AI commented Jan 22, 2026

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

Added both requested tests (commit 1d0f012):

  1. Deserialization test (table_migration_deserialization_test.dart): 7 tests validating that migration.json files without renameColumns can be deserialized correctly, with renameColumns being null.

  2. E2E migration test (column_rename_e2e_test.dart): 2 tests with real database operations:

    • Creates table with original column name
    • Renames column using column override
    • Verifies no data loss by inserting test data before rename and confirming it's preserved after
    • Validates that RENAME (not DROP/ADD) was used by checking data persistence

All tests passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants