Skip to content

Comments

fix: use ALTER COLUMN instead of DROP and ADD to prevent data loss#11997

Open
veeceey wants to merge 2 commits intotypeorm:masterfrom
veeceey:fix/issue-3357-migration-alter-column
Open

fix: use ALTER COLUMN instead of DROP and ADD to prevent data loss#11997
veeceey wants to merge 2 commits intotypeorm:masterfrom
veeceey:fix/issue-3357-migration-alter-column

Conversation

@veeceey
Copy link
Contributor

@veeceey veeceey commented Feb 17, 2026

Description of change

This has been bugging me for a while — when you change a column's length or type in your entity, TypeORM generates a migration that does DROP COLUMN + ADD COLUMN instead of just altering the column in place. That obviously nukes all the data in the column, which is pretty bad for production databases.

The fix is straightforward: for type and length changes, use ALTER COLUMN ... TYPE (Postgres/CockroachDB), MODIFY COLUMN (MySQL/MariaDB), or ALTER COLUMN (SQL Server) instead of dropping and recreating.

DROP+ADD is still kept for cases where ALTER genuinely can't work:

  • Array ↔ non-array conversions (Postgres/CockroachDB)
  • Generated/computed column expression changes
  • IDENTITY column changes (SQL Server)

For Postgres specifically, the USING clause is included so the database can attempt implicit type casts when the type changes (e.g. varchartext).

Tested locally by stepping through the changeColumn logic with a few scenarios (length change, type change varchar→text) and verifying the generated SQL uses ALTER instead of DROP+ADD.

Fixes #3357

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • This pull request links relevant issues as Fixes #3357
  • There are new or updated tests validating the change (tests/**.test.ts)
  • Documentation has been updated to reflect this change (docs/docs/**.md) - N/A, no docs changes needed for a bugfix in query generation

@qodo-free-for-open-source-projects

User description

Description of change

This has been bugging me for a while — when you change a column's length or type in your entity, TypeORM generates a migration that does DROP COLUMN + ADD COLUMN instead of just altering the column in place. That obviously nukes all the data in the column, which is pretty bad for production databases.

The fix is straightforward: for type and length changes, use ALTER COLUMN ... TYPE (Postgres/CockroachDB), MODIFY COLUMN (MySQL/MariaDB), or ALTER COLUMN (SQL Server) instead of dropping and recreating.

DROP+ADD is still kept for cases where ALTER genuinely can't work:

  • Array ↔ non-array conversions (Postgres/CockroachDB)
  • Generated/computed column expression changes
  • IDENTITY column changes (SQL Server)

For Postgres specifically, the USING clause is included so the database can attempt implicit type casts when the type changes (e.g. varchartext).

Tested locally by stepping through the changeColumn logic with a few scenarios (length change, type change varchar→text) and verifying the generated SQL uses ALTER instead of DROP+ADD.

Fixes #3357

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • This pull request links relevant issues as Fixes #3357
  • There are new or updated tests validating the change (tests/**.test.ts)
  • Documentation has been updated to reflect this change (docs/docs/**.md) - N/A, no docs changes needed for a bugfix in query generation

PR Type

Bug fix


Description

  • Use ALTER COLUMN instead of DROP+ADD for type/length changes to preserve data

  • Implement database-specific syntax: ALTER COLUMN TYPE (Postgres/CockroachDB), MODIFY COLUMN (MySQL/MariaDB), ALTER COLUMN (SQL Server)

  • Keep DROP+ADD only for incompatible changes: array conversions, generated columns, IDENTITY columns

  • Add comprehensive tests validating ALTER behavior across all supported databases


Diagram Walkthrough

flowchart LR
  A["Column Type/Length Change"] --> B{Incompatible Change?}
  B -->|Array/Generated/IDENTITY| C["DROP + ADD Column"]
  B -->|Type or Length Only| D["ALTER COLUMN"]
  D --> E["Postgres: ALTER TYPE with USING"]
  D --> F["MySQL/MariaDB: MODIFY COLUMN"]
  D --> G["CockroachDB: ALTER TYPE"]
  D --> H["SQL Server: ALTER COLUMN"]
  C --> I["Preserve Data"]
  E --> I
  F --> I
  G --> I
  H --> I
Loading

File Walkthrough

Relevant files
Bug fix
CockroachQueryRunner.ts
Implement ALTER COLUMN for type changes in CockroachDB     

src/driver/cockroachdb/CockroachQueryRunner.ts

  • Modified changeColumn method to use ALTER COLUMN TYPE instead of
    DROP+ADD for type/length changes
  • Added USING clause for implicit type casting support
  • Kept DROP+ADD only for array conversions and generated column changes
  • Added JSDoc parameter documentation to 100+ methods
+172/-3 
MysqlQueryRunner.ts
Implement MODIFY COLUMN for type changes in MySQL               

src/driver/mysql/MysqlQueryRunner.ts

  • Modified changeColumn method to use MODIFY COLUMN instead of DROP+ADD
    for type/length changes
  • Kept DROP+ADD only for generated column expression changes
  • Added JSDoc parameter documentation to 80+ methods
+139/-2 
PostgresQueryRunner.ts
Implement ALTER COLUMN TYPE with USING for Postgres           

src/driver/postgres/PostgresQueryRunner.ts

  • Modified changeColumn method to use ALTER COLUMN TYPE with USING
    clause for type/length changes
  • USING clause enables implicit type casts (e.g., varchar to text)
  • Kept DROP+ADD only for array conversions and generated column changes
+24/-3   
SqlServerQueryRunner.ts
Implement ALTER COLUMN for type changes in SQL Server       

src/driver/sqlserver/SqlServerQueryRunner.ts

  • Modified changeColumn method to use ALTER COLUMN instead of DROP+ADD
    for type/length changes
  • Kept DROP+ADD only for IDENTITY column and generated expression
    changes
  • Uses buildCreateColumnSql with appropriate flags for ALTER syntax
+32/-3   
Tests
Post.ts
Add Post entity for migration testing                                       

test/github-issues/3357/entity/Post.ts

  • Created test entity with varchar columns of different lengths
  • Used for testing column type and length change migrations
+13/-0   
issue-3357.test.ts
Add tests for ALTER COLUMN migration generation                   

test/github-issues/3357/issue-3357.test.ts

  • Added comprehensive test suite for ALTER COLUMN behavior across 5
    database drivers
  • Tests verify ALTER COLUMN/MODIFY is used instead of DROP+ADD for
    length changes
  • Tests verify ALTER COLUMN/MODIFY is used instead of DROP+ADD for type
    changes
  • Validates both up and down migration queries
+138/-0 

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Feb 17, 2026

PR Code Suggestions ✨

Latest suggestions up to e610d3d

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix invalid USING cast syntax for type changes

Fix the invalid USING clause syntax for PostgreSQL ALTER COLUMN queries. The
current code generates an invalid type cast with length/precision modifiers,
which will cause a syntax error.

src/driver/postgres/PostgresQueryRunner.ts [1308-1321]

 upQueries.push(
     new Query(
         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
             newColumn.name
-        }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+        }" TYPE ${newFullType} USING "${newColumn.name}"::text::${newFullType}`,
     ),
 )
 downQueries.push(
     new Query(
         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
             oldColumn.name
-        }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
+        }" TYPE ${oldFullType} USING "${oldColumn.name}"::text::${oldFullType}`,
     ),
 )
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that the generated SQL for PostgreSQL is invalid because a type cast :: cannot include length/precision modifiers, which makes the new feature fail for this driver.

High
Handle indexes before SQL Server ALTER COLUMN

Handle indexes before altering a column in SQL Server. The ALTER COLUMN
statement will fail if the column is part of an index, so indexes should be
dropped before the alteration and recreated after.

src/driver/sqlserver/SqlServerQueryRunner.ts [1323-1340]

 if (
     newColumn.type !== oldColumn.type ||
     newColumn.length !== oldColumn.length
 ) {
     // Use ALTER COLUMN instead of DROP+ADD to preserve existing data.
+    // Note: SQL Server ALTER COLUMN fails if the column has indexes or constraints.
+    // Drop and recreate indexes around the type change.
+    const columnIndices = clonedTable.indices.filter((index) =>
+        index.columnNames.includes(oldColumn.name),
+    )
+    for (const index of columnIndices) {
+        upQueries.push(this.dropIndexSql(table, index))
+        downQueries.unshift(this.createIndexSql(table, index))
+    }
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(
                 table,
             )} ALTER COLUMN ${this.buildCreateColumnSql(
                 table,
                 newColumn,
                 true,
                 false,
                 true,
             )}`,
         ),
     )
+    for (const index of columnIndices) {
+        upQueries.push(this.createIndexSql(table, index))
+        downQueries.unshift(this.dropIndexSql(table, index))
+    }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out that ALTER COLUMN in SQL Server will fail if the column is part of an index, which is a significant unhandled edge case that will lead to runtime errors.

Medium
General
Restore metadata state in finally block

Use a try/finally block in the test to ensure the original column metadata is
restored, preventing test state from leaking if an assertion fails.

test/functional/schema-builder/alter-column-type.test.ts [33-83]

 const meta = connection.getMetadata(Album)
 const col = meta.columns.find(
     (c) => c.propertyName === "title",
 )!
 
+const originalLength = col.length
 // Change column length from 50 to 100
 col.length = "100"
 
-// Generate migration queries
-const sqlInMemory = await connection.driver
-    .createSchemaBuilder()
-    .log()
-...
-// Restore original length
-col.length = "50"
+try {
+    // Generate migration queries
+    const sqlInMemory = await connection.driver
+        .createSchemaBuilder()
+        .log()
+    ...
+} finally {
+    // Restore original length
+    col.length = originalLength
+}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why: The suggestion improves test robustness by ensuring metadata is restored even if assertions fail, preventing state leakage between tests, which is a good practice for test reliability.

Low
  • More

Previous suggestions

Suggestions up to commit 78f5e5e
CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle precision/scale type changes

Add checks for precision and scale changes to the condition for generating an
ALTER TABLE query to handle cases where only those properties are modified.

src/driver/mysql/MysqlQueryRunner.ts [1075-1106]

 if (
     oldColumn.type !== newColumn.type ||
-    oldColumn.length !== newColumn.length
+    oldColumn.length !== newColumn.length ||
+    oldColumn.precision !== newColumn.precision ||
+    oldColumn.scale !== newColumn.scale
 ) {
     // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
     // Use oldColumn.name in the up-query because the column has not
     // been renamed yet at this point in the query sequence; the
     // CHANGE (rename) statement is emitted afterwards.
     const typeChangeColumn = newColumn.clone()
     typeChangeColumn.name = oldColumn.name
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(typeChangeColumn, true)}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
         ),
     )
 
     // update cloned table column type/length
     const clonedColumn = clonedTable.columns.find(
         (column) => column.name === oldColumn.name,
     )
     if (clonedColumn) {
         clonedColumn.type = newColumn.type
         clonedColumn.length = newColumn.length
         clonedColumn.precision = newColumn.precision
         clonedColumn.scale = newColumn.scale
     }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a bug where changes to only precision or scale are not detected, preventing the ALTER TABLE query from being generated and causing a potential desynchronization of clonedTable.

Medium
Alter type for numeric modifiers

Add checks for precision and scale changes to the condition for generating an
ALTER TABLE query to handle cases where only those properties are modified.

src/driver/postgres/PostgresQueryRunner.ts [1299-1336]

 if (
     oldColumn.type !== newColumn.type ||
-    oldColumn.length !== newColumn.length
+    oldColumn.length !== newColumn.length ||
+    oldColumn.precision !== newColumn.precision ||
+    oldColumn.scale !== newColumn.scale
 ) {
     // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
     // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
     const newFullType = this.driver.createFullType(newColumn)
     const oldFullType = this.driver.createFullType(oldColumn)
 
     // Use oldColumn.name in the up-query because the column has not
     // been renamed yet at this point in the query sequence; the RENAME
     // statement is emitted afterwards.
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                 oldColumn.name
             }" TYPE ${newFullType} USING "${oldColumn.name}"::${newFullType}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                 oldColumn.name
             }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
         ),
     )
 
     // update cloned table column type/length
     const clonedColumn = clonedTable.columns.find(
         (column) => column.name === oldColumn.name,
     )
     if (clonedColumn) {
         clonedColumn.type = newColumn.type
         clonedColumn.length = newColumn.length
         clonedColumn.precision = newColumn.precision
         clonedColumn.scale = newColumn.scale
     }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a bug where changes to only precision or scale are not detected, preventing the ALTER TABLE query from being generated and causing a potential desynchronization of clonedTable.

Medium
Always restore mutated metadata

Wrap the test logic that mutates column metadata in a try/finally block to
ensure the metadata is always restored, preventing state leakage between tests.

test/functional/schema-builder/alter-column-type.test.ts [38-83]

+const originalLength = col.length
+
 // Change column length from 50 to 100
 col.length = "100"
 
-// Generate migration queries
-const sqlInMemory = await connection.driver
-    .createSchemaBuilder()
-    .log()
-...
-// Restore original length
-col.length = "50"
+try {
+    // Generate migration queries
+    const sqlInMemory = await connection.driver
+        .createSchemaBuilder()
+        .log()
+    ...
+} finally {
+    // Restore original length
+    col.length = originalLength
+}
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out a potential for test flakiness due to state leakage and proposes a try/finally block, which is a robust pattern for ensuring test isolation.

Low
Suggestions up to commit 3a53020
CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle precision/scale type changes

Add checks for precision and scale changes to the condition that triggers a
MODIFY COLUMN query to ensure schema changes for decimal/numeric types are
correctly applied.

src/driver/mysql/MysqlQueryRunner.ts [1074-1105]

 if (
     oldColumn.type !== newColumn.type ||
-    oldColumn.length !== newColumn.length
+    oldColumn.length !== newColumn.length ||
+    oldColumn.precision !== newColumn.precision ||
+    oldColumn.scale !== newColumn.scale
 ) {
     // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
     // Use oldColumn.name in the up-query because the column has not
     // been renamed yet at this point in the query sequence; the
     // CHANGE (rename) statement is emitted afterwards.
     const typeChangeColumn = newColumn.clone()
     typeChangeColumn.name = oldColumn.name
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(typeChangeColumn, true)}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
         ),
     )
 
     // update cloned table column type/length
     const clonedColumn = clonedTable.columns.find(
         (column) => column.name === oldColumn.name,
     )
     if (clonedColumn) {
         clonedColumn.type = newColumn.type
         clonedColumn.length = newColumn.length
         clonedColumn.precision = newColumn.precision
         clonedColumn.scale = newColumn.scale
     }
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where changes to a column's precision or scale are not detected, preventing the generation of a necessary ALTER TABLE statement and causing schema drift.

Medium
Drop defaults before type changes

Before altering a column type in PostgreSQL, drop its default value and re-apply
it afterward to prevent migration failures caused by incompatible default value
types.

src/driver/postgres/PostgresQueryRunner.ts [1302-1339]

 if (
     oldColumn.type !== newColumn.type ||
     oldColumn.length !== newColumn.length
 ) {
     // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
     // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
     const newFullType = this.driver.createFullType(newColumn)
     const oldFullType = this.driver.createFullType(oldColumn)
+
+    // Drop default first to avoid type-change failures due to non-castable defaults.
+    if (oldColumn.default !== null && oldColumn.default !== undefined) {
+        upQueries.push(
+            new Query(
+                `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" DROP DEFAULT`,
+            ),
+        )
+    }
+    if (newColumn.default !== null && newColumn.default !== undefined) {
+        downQueries.push(
+            new Query(
+                `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" DROP DEFAULT`,
+            ),
+        )
+    }
 
     // Use oldColumn.name in the up-query because the column has not
     // been renamed yet at this point in the query sequence; the RENAME
     // statement is emitted afterwards.
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                 oldColumn.name
             }" TYPE ${newFullType} USING "${oldColumn.name}"::${newFullType}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                 oldColumn.name
             }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
         ),
     )
 
+    // Recreate defaults after type change.
+    if (newColumn.default !== null && newColumn.default !== undefined) {
+        upQueries.push(
+            new Query(
+                `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" SET DEFAULT ${newColumn.default}`,
+            ),
+        )
+    }
+    if (oldColumn.default !== null && oldColumn.default !== undefined) {
+        downQueries.push(
+            new Query(
+                `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${oldColumn.name}" SET DEFAULT ${oldColumn.default}`,
+            ),
+        )
+    }
+
     // update cloned table column type/length
     const clonedColumn = clonedTable.columns.find(
         (column) => column.name === oldColumn.name,
     )
     if (clonedColumn) {
         clonedColumn.type = newColumn.type
         clonedColumn.length = newColumn.length
         clonedColumn.precision = newColumn.precision
         clonedColumn.scale = newColumn.scale
     }
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out a potential failure in PostgreSQL when changing a column's type if it has a default value that cannot be cast to the new type, and provides a robust solution.

Medium
Prevent cross-test entity breakage

Revert changes to the shared Album entity and create a new, dedicated entity for
the alter-column-type.test.ts test to avoid breaking other tests that rely on
the original entity's structure.

test/functional/schema-builder/entity/Album.ts [5-15]

-@Entity()
+@Entity({ synchronize: false })
 export class Album {
     @PrimaryGeneratedColumn()
     id: number
 
-    @Column({ type: "varchar", length: "50" })
-    title: string
-
-    @Column({ type: "varchar", length: "200" })
-    description: string
+    @Column()
+    name: string
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that modifying a shared test entity can cause side effects in other tests, which is a valid concern for test suite stability and maintainability.

Medium
✅ Suggestions up to commit aa1dfb0
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix rename with type change
Suggestion Impact:The commit updated the ALTER COLUMN TYPE up-query to reference oldColumn.name (and its USING cast) so type alteration works even when the column is renamed later in the same change sequence.

code diff:

@@ -1308,11 +1308,14 @@
                 const newFullType = this.driver.createFullType(newColumn)
                 const oldFullType = this.driver.createFullType(oldColumn)
 
+                // Use oldColumn.name in the up-query because the column has not
+                // been renamed yet at this point in the query sequence; the RENAME
+                // statement is emitted afterwards.
                 upQueries.push(
                     new Query(
                         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-                            newColumn.name
-                        }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+                            oldColumn.name
+                        }" TYPE ${newFullType} USING "${oldColumn.name}"::${newFullType}`,
                     ),

Fix a bug in changeColumn by using the old column name for the ALTER COLUMN
query when the column is also being renamed, as the rename operation happens
after the type alteration.

src/driver/postgres/PostgresQueryRunner.ts [1302-1336]

 if (
     oldColumn.type !== newColumn.type ||
     oldColumn.length !== newColumn.length
 ) {
     // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
     // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
     const newFullType = this.driver.createFullType(newColumn)
     const oldFullType = this.driver.createFullType(oldColumn)
 
+    const effectiveColumnName =
+        oldColumn.name !== newColumn.name ? oldColumn.name : newColumn.name
+
     upQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-                newColumn.name
-            }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+            `ALTER TABLE ${this.escapePath(
+                table,
+            )} ALTER COLUMN "${effectiveColumnName}" TYPE ${newFullType} USING "${effectiveColumnName}"::${newFullType}`,
         ),
     )
     downQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-                oldColumn.name
-            }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
+            `ALTER TABLE ${this.escapePath(
+                table,
+            )} ALTER COLUMN "${effectiveColumnName}" TYPE ${oldFullType} USING "${effectiveColumnName}"::${oldFullType}`,
         ),
     )
 
     // update cloned table column type/length
     const clonedColumn = clonedTable.columns.find(
         (column) => column.name === oldColumn.name,
     )
     if (clonedColumn) {
         clonedColumn.type = newColumn.type
         clonedColumn.length = newColumn.length
         clonedColumn.precision = newColumn.precision
         clonedColumn.scale = newColumn.scale
     }
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where altering a column's type and renaming it in the same operation would fail because the ALTER query uses the new name before the RENAME query is executed.

Medium
Prevent alter-before-rename failure
Suggestion Impact:Updated the ALTER TABLE ... ALTER COLUMN ... TYPE up-query in changeColumn to reference oldColumn.name (avoiding alter-before-rename failure when renaming), though it did not implement the suggested shared effectiveColumnName nor adjust the down-query.

code diff:

     async changeColumn(
         tableOrName: Table | string,
@@ -1386,10 +1339,13 @@
                 const newFullType = this.driver.createFullType(newColumn)
                 const oldFullType = this.driver.createFullType(oldColumn)
 
+                // Use oldColumn.name in the up-query because the column has not
+                // been renamed yet at this point in the query sequence; the RENAME
+                // statement is emitted afterwards.
                 upQueries.push(
                     new Query(
                         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-                            newColumn.name
+                            oldColumn.name
                         }" TYPE ${newFullType}`,
                     ),
                 )

Fix a bug in changeColumn by using the old column name for the ALTER COLUMN
query when the column is also being renamed, as the rename operation happens
after the type alteration.

src/driver/cockroachdb/CockroachQueryRunner.ts [1381-1414]

 if (
     oldColumn.type !== newColumn.type ||
     oldColumn.length !== newColumn.length
 ) {
     // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
     const newFullType = this.driver.createFullType(newColumn)
     const oldFullType = this.driver.createFullType(oldColumn)
 
+    const effectiveColumnName =
+        oldColumn.name !== newColumn.name ? oldColumn.name : newColumn.name
+
     upQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-                newColumn.name
-            }" TYPE ${newFullType}`,
+            `ALTER TABLE ${this.escapePath(
+                table,
+            )} ALTER COLUMN "${effectiveColumnName}" TYPE ${newFullType}`,
         ),
     )
     downQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-                oldColumn.name
-            }" TYPE ${oldFullType}`,
+            `ALTER TABLE ${this.escapePath(
+                table,
+            )} ALTER COLUMN "${effectiveColumnName}" TYPE ${oldFullType}`,
         ),
     )
 
     // update cloned table column type/length
     const clonedColumn = clonedTable.columns.find(
         (column) => column.name === oldColumn.name,
     )
     if (clonedColumn) {
         clonedColumn.type = newColumn.type
         clonedColumn.length = newColumn.length
         clonedColumn.precision = newColumn.precision
         clonedColumn.scale = newColumn.scale
     }
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where altering a column's type and renaming it in the same operation would fail because the ALTER query uses the new name before the RENAME query is executed.

Medium
Use old name during modify
Suggestion Impact:Updated the MODIFY statement in changeColumn to clone the new column definition but set its name to oldColumn.name, ensuring the type/length modification runs before the subsequent rename (CHANGE) query.

code diff:

@@ -1121,9 +1076,14 @@
                 oldColumn.length !== newColumn.length
             ) {
                 // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
+                // Use oldColumn.name in the up-query because the column has not
+                // been renamed yet at this point in the query sequence; the
+                // CHANGE (rename) statement is emitted afterwards.
+                const typeChangeColumn = newColumn.clone()
+                typeChangeColumn.name = oldColumn.name
                 upQueries.push(
                     new Query(
-                        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
+                        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(typeChangeColumn, true)}`,
                     ),
                 )
                 downQueries.push(

Fix a bug in changeColumn by using the old column name for the MODIFY query when
the column is also being renamed, as the rename operation happens after the type
modification.

src/driver/mysql/MysqlQueryRunner.ts [1119-1145]

 if (
     oldColumn.type !== newColumn.type ||
     oldColumn.length !== newColumn.length
 ) {
     // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
+    const effectiveNewColumn =
+        newColumn.name !== oldColumn.name
+            ? new TableColumn({ ...newColumn, name: oldColumn.name })
+            : newColumn
+
     upQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
+            `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(effectiveNewColumn, true)}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
         ),
     )
 
     // update cloned table column type/length
     const clonedColumn = clonedTable.columns.find(
         (column) => column.name === oldColumn.name,
     )
     if (clonedColumn) {
         clonedColumn.type = newColumn.type
         clonedColumn.length = newColumn.length
         clonedColumn.precision = newColumn.precision
         clonedColumn.scale = newColumn.scale
     }
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where modifying a column's type and renaming it in the same operation would fail because the MODIFY query uses the new name before the RENAME query is executed.

Medium
✅ Suggestions up to commit 5232a03
CategorySuggestion                                                                                                                                    Impact
Possible issue
Use correct column name per direction
Suggestion Impact:The commit updates the up migration ALTER COLUMN query to reference oldColumn.name (instead of newColumn.name), ensuring the type change runs before the rename statement in the query sequence. The diff shown does not include the suggested down-query adjustment.

code diff:

+                // Use oldColumn.name in the up-query because the column has not
+                // been renamed yet at this point in the query sequence; the RENAME
+                // statement is emitted afterwards.
                 upQueries.push(
                     new Query(
                         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-                            newColumn.name
-                        }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+                            oldColumn.name
+                        }" TYPE ${newFullType} USING "${oldColumn.name}"::${newFullType}`,
                     ),

Fix ALTER COLUMN queries to use the correct column name when a rename occurs in
the same operation. The 'up' query should use the old name, and the 'down' query
should use the new name.

src/driver/postgres/PostgresQueryRunner.ts [1311-1324]

+const upColumnName = oldColumn.name
+const downColumnName =
+    oldColumn.name !== newColumn.name ? newColumn.name : oldColumn.name
+
 upQueries.push(
     new Query(
-        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-            newColumn.name
-        }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${upColumnName}" TYPE ${newFullType} USING "${upColumnName}"::${newFullType}`,
     ),
 )
 downQueries.push(
     new Query(
-        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-            oldColumn.name
-        }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
+        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${downColumnName}" TYPE ${oldFullType} USING "${downColumnName}"::${oldFullType}`,
     ),
 )
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a bug where a column type change combined with a rename in the same operation will fail, and provides a correct fix for both the 'up' and 'down' queries.

High
Align ALTER target with renames
Suggestion Impact:The commit updates the ALTER TABLE ... ALTER COLUMN ... TYPE up-query to use oldColumn.name (with an explanatory comment), aligning the ALTER target with the pre-rename column name. The corresponding down-query adjustment to use the new name when renamed is not shown in this patch.

code diff:

@@ -1386,10 +1339,13 @@
                 const newFullType = this.driver.createFullType(newColumn)
                 const oldFullType = this.driver.createFullType(oldColumn)
 
+                // Use oldColumn.name in the up-query because the column has not
+                // been renamed yet at this point in the query sequence; the RENAME
+                // statement is emitted afterwards.
                 upQueries.push(
                     new Query(
                         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-                            newColumn.name
+                            oldColumn.name
                         }" TYPE ${newFullType}`,
                     ),
                 )

Fix ALTER COLUMN queries to use the correct column name when a rename occurs in
the same operation. The 'up' query should use the old name, and the 'down' query
should use the new name.

src/driver/cockroachdb/CockroachQueryRunner.ts [1389-1402]

+const upColumnName = oldColumn.name
+const downColumnName =
+    oldColumn.name !== newColumn.name ? newColumn.name : oldColumn.name
+
 upQueries.push(
     new Query(
-        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-            newColumn.name
-        }" TYPE ${newFullType}`,
+        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${upColumnName}" TYPE ${newFullType}`,
     ),
 )
 downQueries.push(
     new Query(
-        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-            oldColumn.name
-        }" TYPE ${oldFullType}`,
+        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${downColumnName}" TYPE ${oldFullType}`,
     ),
 )
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a bug where a column type change combined with a rename in the same operation will fail, and provides a correct fix for both the 'up' and 'down' queries.

High
Make rollback work after renames
Suggestion Impact:The commit adjusted the MODIFY COLUMN logic to account for renames by ensuring the type-change ALTER TABLE ... MODIFY in the up-query uses the old column name (since the CHANGE/rename is emitted later), addressing the same rename sequencing/rollback problem area but via a different change than the suggested down-query adjustment.

code diff:

@@ -1121,9 +1076,14 @@
                 oldColumn.length !== newColumn.length
             ) {
                 // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
+                // Use oldColumn.name in the up-query because the column has not
+                // been renamed yet at this point in the query sequence; the
+                // CHANGE (rename) statement is emitted afterwards.
+                const typeChangeColumn = newColumn.clone()
+                typeChangeColumn.name = oldColumn.name
                 upQueries.push(
                     new Query(
-                        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
+                        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(typeChangeColumn, true)}`,
                     ),
                 )
                 downQueries.push(

Fix the MODIFY 'down' query to handle column renames. It should use the new
column name with the old column's definition to ensure rollback works correctly.

src/driver/mysql/MysqlQueryRunner.ts [1129-1133]

+const downColumn =
+    newColumn.name !== oldColumn.name
+        ? Object.assign(oldColumn.clone(), { name: newColumn.name })
+        : oldColumn
+
 downQueries.push(
     new Query(
-        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
+        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(downColumn, true)}`,
     ),
 )
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a bug where the rollback ('down') query for a column type change will fail if the column was also renamed, and provides a correct fix.

High
Fix down ALTER after renames

Fix the ALTER COLUMN 'down' query to handle column renames. It should target the
new column name while using the old column's definition to ensure rollback works
correctly.

src/driver/sqlserver/SqlServerQueryRunner.ts [1341-1353]

+const downOldDefinition =
+    newColumn.name !== oldColumn.name
+        ? Object.assign(oldColumn.clone(), { name: newColumn.name })
+        : oldColumn
+
 downQueries.push(
     new Query(
         `ALTER TABLE ${this.escapePath(
             table,
         )} ALTER COLUMN ${this.buildCreateColumnSql(
             table,
-            oldColumn,
+            downOldDefinition,
             true,
             false,
             true,
         )}`,
     ),
 )
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a bug where the rollback ('down') query for a column type change will fail if the column was also renamed, and provides a correct fix.

High
✅ Suggestions up to commit e73d5c2
CategorySuggestion                                                                                                                                    Impact
Possible issue
Add explicit type casting for safety

Add a USING clause to the ALTER COLUMN query in CockroachQueryRunner.ts to
explicitly cast the column to the new type, ensuring data conversion is handled
correctly.

src/driver/cockroachdb/CockroachQueryRunner.ts [1382-1395]

 upQueries.push(
     new Query(
         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
             newColumn.name
-        }" TYPE ${newFullType}`,
+        }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
     ),
 )
 downQueries.push(
     new Query(
         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
             oldColumn.name
-        }" TYPE ${oldFullType}`,
+        }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
     ),
 )
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out that explicit type casting with a USING clause is necessary for ALTER COLUMN TYPE in CockroachDB to handle complex data conversions, preventing potential migration failures, which is a significant correctness improvement.

Medium
Update cloned metadata after alter
Suggestion Impact:The commit adds logic after generating ALTER COLUMN TYPE queries to find the corresponding column in clonedTable and update its metadata (type, length, and also precision/scale), preventing stale clonedTable state.

code diff:

+            if (
+                oldColumn.type !== newColumn.type ||
+                oldColumn.length !== newColumn.length
+            ) {
+                // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
+                const newFullType = this.driver.createFullType(newColumn)
+                const oldFullType = this.driver.createFullType(oldColumn)
+
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                            newColumn.name
+                        }" TYPE ${newFullType}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                            oldColumn.name
+                        }" TYPE ${oldFullType}`,
+                    ),
+                )
+
+                // update cloned table column type/length
+                const clonedColumn = clonedTable.columns.find(
+                    (column) => column.name === oldColumn.name,
+                )
+                if (clonedColumn) {
+                    clonedColumn.type = newColumn.type
+                    clonedColumn.length = newColumn.length
+                    clonedColumn.precision = newColumn.precision
+                    clonedColumn.scale = newColumn.scale
+                }
+            }

In CockroachQueryRunner.ts, after altering a column's type or length, update the
corresponding column's metadata in the clonedTable to prevent stale cache
information.

src/driver/cockroachdb/CockroachQueryRunner.ts [1374-1395]

 } else if (
     oldColumn.type !== newColumn.type ||
     oldColumn.length !== newColumn.length
 ) {
     // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
     const newFullType = this.driver.createFullType(newColumn)
     const oldFullType = this.driver.createFullType(oldColumn)
 
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                 newColumn.name
             }" TYPE ${newFullType}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                 oldColumn.name
             }" TYPE ${oldFullType}`,
         ),
     )
+
+    // update cloned table metadata
+    const changedColumn = clonedTable.columns.find(c => c.name === newColumn.name)
+    if (changedColumn) {
+        changedColumn.type = newColumn.type
+        changedColumn.length = newColumn.length
+    }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the cloned table's metadata is not updated after an ALTER COLUMN operation, which could lead to a stale cache. This improves the robustness of schema manipulations.

Medium
Sync cloned metadata after modify
Suggestion Impact:The commit adds logic after the MODIFY COLUMN queries to find the affected column in clonedTable and update its metadata (type, length, and also precision/scale), preventing stale cloned metadata after MODIFY operations.

code diff:

+            if (
+                oldColumn.type !== newColumn.type ||
+                oldColumn.length !== newColumn.length
+            ) {
+                // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
+                    ),
+                )
+
+                // update cloned table column type/length
+                const clonedColumn = clonedTable.columns.find(
+                    (column) => column.name === oldColumn.name,
+                )
+                if (clonedColumn) {
+                    clonedColumn.type = newColumn.type
+                    clonedColumn.length = newColumn.length
+                    clonedColumn.precision = newColumn.precision
+                    clonedColumn.scale = newColumn.scale
+                }
+            }

In MysqlQueryRunner.ts, after modifying a column, update the corresponding
column's metadata in the clonedTable to reflect the changes and prevent stale
cache information.

src/driver/mysql/MysqlQueryRunner.ts [1111-1125]

 } else if (
     oldColumn.type !== newColumn.type ||
     oldColumn.length !== newColumn.length
 ) {
     // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
         ),
     )
+
+    // update cloned table metadata
+    const changedColumn = clonedTable.columns.find(c => c.name === newColumn.name)
+    if (changedColumn) {
+        changedColumn.type = newColumn.type
+        changedColumn.length = newColumn.length
+    }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the cloned table's metadata is not updated after a MODIFY COLUMN operation, which could lead to a stale cache. This improves the robustness of schema manipulations.

Medium
Update clone after altering type
Suggestion Impact:The commit adds logic after the ALTER COLUMN TYPE operation to find the affected column in clonedTable and update its type/length (and also precision/scale), preventing stale cloned metadata.

code diff:

+            if (
+                oldColumn.type !== newColumn.type ||
+                oldColumn.length !== newColumn.length
+            ) {
+                // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
+                // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
+                const newFullType = this.driver.createFullType(newColumn)
+                const oldFullType = this.driver.createFullType(oldColumn)
+
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                            newColumn.name
+                        }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                            oldColumn.name
+                        }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
+                    ),
+                )
+
+                // update cloned table column type/length
+                const clonedColumn = clonedTable.columns.find(
+                    (column) => column.name === oldColumn.name,
+                )
+                if (clonedColumn) {
+                    clonedColumn.type = newColumn.type
+                    clonedColumn.length = newColumn.length
+                    clonedColumn.precision = newColumn.precision
+                    clonedColumn.scale = newColumn.scale
+                }
+            }

In PostgresQueryRunner.ts, after altering a column's type or length, update the
corresponding column's metadata in the clonedTable to prevent stale cache
information.

src/driver/postgres/PostgresQueryRunner.ts [1295-1317]

 } else if (
     oldColumn.type !== newColumn.type ||
     oldColumn.length !== newColumn.length
 ) {
     // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
     // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
     const newFullType = this.driver.createFullType(newColumn)
     const oldFullType = this.driver.createFullType(oldColumn)
 
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                 newColumn.name
             }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                 oldColumn.name
             }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
         ),
     )
+
+    // update cloned table metadata
+    const changedColumn = clonedTable.columns.find(c => c.name === newColumn.name)
+    if (changedColumn) {
+        changedColumn.type = newColumn.type
+        changedColumn.length = newColumn.length
+    }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the cloned table's metadata is not updated after an ALTER COLUMN TYPE operation, which could lead to a stale cache. This improves the robustness of schema manipulations.

Medium
Sync clone after ALTER COLUMN
Suggestion Impact:The commit adds logic after the ALTER COLUMN queries to find the corresponding column in clonedTable and update its type/length (and additionally precision/scale), keeping cloned metadata in sync with the schema change.

code diff:

+            if (
+                newColumn.type !== oldColumn.type ||
+                newColumn.length !== oldColumn.length
+            ) {
+                // Use ALTER COLUMN instead of DROP+ADD to preserve existing data.
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            newColumn,
+                            true,
+                            false,
+                            true,
+                        )}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            oldColumn,
+                            true,
+                            false,
+                            true,
+                        )}`,
+                    ),
+                )
+
+                // update cloned table column type/length
+                const clonedColumn = clonedTable.columns.find(
+                    (column) => column.name === oldColumn.name,
+                )
+                if (clonedColumn) {
+                    clonedColumn.type = newColumn.type
+                    clonedColumn.length = newColumn.length
+                    clonedColumn.precision = newColumn.precision
+                    clonedColumn.scale = newColumn.scale
+                }
+            }

In SqlServerQueryRunner.ts, after altering a column, update the corresponding
column's metadata in the clonedTable to reflect the changes and prevent stale
cache information.

src/driver/sqlserver/SqlServerQueryRunner.ts [1316-1346]

 } else if (
     newColumn.type !== oldColumn.type ||
     newColumn.length !== oldColumn.length
 ) {
     // Use ALTER COLUMN instead of DROP+ADD to preserve existing data.
     upQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(
-                table,
-            )} ALTER COLUMN ${this.buildCreateColumnSql(
-                table,
-                newColumn,
-                true,
-                false,
-                true,
-            )}`,
+            `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN ${this.buildCreateColumnSql(table, newColumn, true, false, true)}`,
         ),
     )
     downQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(
-                table,
-            )} ALTER COLUMN ${this.buildCreateColumnSql(
-                table,
-                oldColumn,
-                true,
-                false,
-                true,
-            )}`,
+            `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN ${this.buildCreateColumnSql(table, oldColumn, true, false, true)}`,
         ),
     )
+
+    // update cloned table metadata
+    const changedColumn = clonedTable.columns.find(c => c.name === newColumn.name)
+    if (changedColumn) {
+        changedColumn.type = newColumn.type
+        changedColumn.length = newColumn.length
+    }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the cloned table's metadata is not updated after an ALTER COLUMN operation, which could lead to a stale cache. This improves the robustness of schema manipulations.

Medium

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Feb 17, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

✅ 1. Test added under github-issues 📘 Rule violation ⛯ Reliability
Description
The fix adds a new test only under test/github-issues/3357, rather than the functional test suite.
This risks issue-specific test sprawl and does not meet the requirement to keep issue fixes in
functional tests.
Code

test/github-issues/3357/issue-3357.test.ts[R1-12]

+import "reflect-metadata"
+import {
+    createTestingConnections,
+    closeTestingConnections,
+} from "../../utils/test-utils"
+import { DataSource } from "../../../src/data-source/DataSource"
+import { expect } from "chai"
+import { Post } from "./entity/Post"
+
+describe("github issues > #3357 Migration generation should use ALTER COLUMN instead of DROP+ADD to prevent data loss", () => {
+    let connections: DataSource[]
+
Evidence
The compliance rule requires issue fixes to be validated in test/functional and flags tests added
only under test/github-issues without a clear reason. This PR adds the validating test under
test/github-issues/3357.

Rule 3: Prefer functional tests over per-issue tests
test/github-issues/3357/issue-3357.test.ts[1-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new coverage for issue #3357 was added only under `test/github-issues`, but the checklist requires issue fixes to live in the functional test suite.
## Issue Context
Per compliance, issue-specific tests should be placed in `test/functional` with an issue reference when applicable.
## Fix Focus Areas
- test/github-issues/3357/issue-3357.test.ts[1-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 2. Skips combined column changes 🐞 Bug ✓ Correctness
Description
When type/length changes, the new else-if branch emits only ALTER TYPE/MODIFY and skips the rest of
changeColumn logic (rename, nullability/default/PK/unique/FK renames, etc.). Any scenario where
type/length changes together with other attributes can generate incomplete/incorrect migrations
(e.g., rename+type change won’t rename).
Code

src/driver/postgres/PostgresQueryRunner.ts[R1295-1318]

+        } else if (
+            oldColumn.type !== newColumn.type ||
+            oldColumn.length !== newColumn.length
+        ) {
+            // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
+            // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
+            const newFullType = this.driver.createFullType(newColumn)
+            const oldFullType = this.driver.createFullType(oldColumn)
+
+            upQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        newColumn.name
+                    }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+                ),
+            )
+            downQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        oldColumn.name
+                    }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
+                ),
+            )
    } else {
Evidence
The Postgres/Cockroach/MySQL implementations now have a mutually-exclusive else-if for type/length
which prevents entering the main else branch where rename and other column alterations are handled.
This means combined changes in one column diff can be partially applied.

src/driver/postgres/PostgresQueryRunner.ts[1282-1335]
src/driver/postgres/PostgresQueryRunner.ts[1826-1834]
src/driver/mysql/MysqlQueryRunner.ts[1095-1127]
src/driver/mysql/MysqlQueryRunner.ts[1391-1396]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Type/length changes currently take an `else if` branch that prevents executing the rest of the `changeColumn` logic. This causes incomplete migrations when multiple properties (rename, nullability, constraints, etc.) change together with type/length.
### Issue Context
`changeColumn` is invoked per changed column and is expected to handle all differences between `oldColumn` and `newColumn` in one pass. The new `else if` short-circuits that.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1282-1335]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1363-1405]
- src/driver/mysql/MysqlQueryRunner.ts[1095-1135]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1303-1348]
### Notes
A robust approach is:
1) Handle truly-incompatible cases via DROP+ADD (keep existing first `if`).
2) Otherwise enter the main change flow, and:
- if rename: emit rename queries and update in-memory column names used for subsequent statements.
- if type/length: emit ALTER TYPE/MODIFY/ALTER COLUMN queries.
- continue with existing nullability/default/PK/unique/etc handling.
3) Ensure downQueries reverse the order (e.g., type back, then rename back).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 3. Stale schema cache after ALTER 🐞 Bug ⛯ Reliability
Description
The new ALTER TYPE/MODIFY branches don’t update clonedTable/oldColumn, but changeColumn still
replaces the cached table with clonedTable. This leaves the QueryRunner’s cached schema stale after
a type/length change, which can corrupt subsequent diffing and SQL generation within the same schema
sync/migration generation.
Code

src/driver/mysql/MysqlQueryRunner.ts[R1111-1126]

+        } else if (
+            oldColumn.type !== newColumn.type ||
+            oldColumn.length !== newColumn.length
+        ) {
+            // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
+            upQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
+                ),
+            )
+            downQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
+                ),
+            )
    } else {
Evidence
In multiple drivers, the ALTER/MODIFY branch only pushes SQL queries and does not mutate
clonedTable to reflect the new type/length. Yet at the end of changeColumn, the cached table is
replaced with clonedTable, effectively overwriting the cache with stale column definitions.

src/driver/mysql/MysqlQueryRunner.ts[1111-1126]
src/driver/mysql/MysqlQueryRunner.ts[1612-1614]
src/driver/cockroachdb/CockroachQueryRunner.ts[2151-2153]
src/driver/postgres/PostgresQueryRunner.ts[2441-2443]
src/driver/sqlserver/SqlServerQueryRunner.ts[1956-1958]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
After emitting ALTER TYPE/MODIFY statements, `clonedTable` is not updated, but it is still used to replace the cached table. This leaves the cached schema out of sync with the SQL that was generated/executed.
### Issue Context
`changeColumn` uses `clonedTable` as the new authoritative cached schema at the end of the method.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1295-1318]
- src/driver/postgres/PostgresQueryRunner.ts[2441-2443]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1374-1396]
- src/driver/cockroachdb/CockroachQueryRunner.ts[2151-2153]
- src/driver/mysql/MysqlQueryRunner.ts[1111-1126]
- src/driver/mysql/MysqlQueryRunner.ts[1612-1614]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1316-1347]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1956-1958]
### Notes
Minimum fix: locate the column in `clonedTable.columns` and update type/length/precision/scale/isArray/etc. to match `newColumn` when the ALTER/MODIFY branch is taken.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
✅ 4. Enum conversions may emit invalid TYPE 🐞 Bug ✓ Correctness
Description
Postgres/Cockroach ALTER TYPE branches use driver.createFullType(), but enums are represented with
TableColumn.type = "enum" and the real DB type name is derived via buildEnumName()/enumName. If a
column changes between enum and non-enum types, the new branch can emit TYPE enum (and invalid down
SQL) and it also bypasses enum type lifecycle handling (create/drop/rename) used elsewhere.
Code

src/driver/cockroachdb/CockroachQueryRunner.ts[R1374-1396]

+        } else if (
+            oldColumn.type !== newColumn.type ||
+            oldColumn.length !== newColumn.length
+        ) {
+            // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
+            const newFullType = this.driver.createFullType(newColumn)
+            const oldFullType = this.driver.createFullType(oldColumn)
+
+            upQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        newColumn.name
+                    }" TYPE ${newFullType}`,
+                ),
+            )
+            downQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        oldColumn.name
+                    }" TYPE ${oldFullType}`,
+                ),
+            )
    } else {
Evidence
In Postgres table introspection, enum columns are explicitly mapped to tableColumn.type = "enum"
with enumName stored separately. The SQL builder uses buildEnumName() for enums, not
createFullType(). Therefore, using createFullType() in the new ALTER TYPE branches can produce TYPE
enum rather than the actual enum type identifier. The codebase also has dedicated enum-change logic,
which is skipped when the new else-if triggers.

src/driver/postgres/PostgresQueryRunner.ts[3819-3825]
src/driver/postgres/PostgresDriver.ts[1246-1252]
src/driver/postgres/PostgresQueryRunner.ts[5022-5027]
src/driver/postgres/PostgresQueryRunner.ts[1295-1317]
src/driver/cockroachdb/CockroachQueryRunner.ts[4367-4372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Enum columns require special type naming (via `buildEnumName`/`enumName`) and often enum type lifecycle operations. The new type/length ALTER branch uses `createFullType`, which will produce `enum` for enum columns and bypasses existing enum-specific handling.
### Issue Context
Postgres introspection sets `TableColumn.type = "enum"` and stores `enumName` separately, and SQL generation uses `buildEnumName()` for enum types.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1295-1318]
- src/driver/postgres/PostgresQueryRunner.ts[3819-3835]
- src/driver/postgres/PostgresQueryRunner.ts[4997-5027]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1374-1396]
- src/driver/cockroachdb/CockroachQueryRunner.ts[4352-4372]
### Notes
Two viable paths:
- Conservative: fall back to DROP+ADD when converting to/from enum.
- Full support: ensure enum type exists (create), then ALTER COLUMN TYPE to the correct enum identifier (using `buildEnumName`), and drop old enum types when appropriate; ensure down migration mirrors this.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. No docs for migration change 📘 Rule violation ✓ Correctness
Description
This PR changes user-visible migration SQL generation behavior (ALTER vs DROP+ADD) but does not
include any documentation updates. Users relying on documented migration behavior may be surprised
by the new generated SQL across drivers.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1295-1317]

+        } else if (
+            oldColumn.type !== newColumn.type ||
+            oldColumn.length !== newColumn.length
+        ) {
+            // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
+            // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
+            const newFullType = this.driver.createFullType(newColumn)
+            const oldFullType = this.driver.createFullType(oldColumn)
+
+            upQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        newColumn.name
+                    }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+                ),
+            )
+            downQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        oldColumn.name
+                    }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
+                ),
+            )
Evidence
Compliance requires docs updates for user-facing changes. The PR explicitly introduces new ALTER
COLUMN generation logic, while the PR description indicates no docs changes were made.

Rule 2: Docs updated for user-facing changes
src/driver/postgres/PostgresQueryRunner.ts[1295-1317]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces a user-visible change in generated migration SQL (ALTER instead of DROP+ADD), but does not update docs.
## Issue Context
Compliance requires documentation updates for user-facing behavior changes.
## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1295-1317]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 6. Bulk @param comment noise 📘 Rule violation ✓ Correctness
Description
The PR adds many repetitive @param tags across query runner files without changing the method
signatures. This introduces style/comment churn that may be considered AI-generated noise per the
checklist.
Code

src/driver/mysql/MysqlQueryRunner.ts[R181-186]

/**
 * Executes a raw SQL query.
+     * @param query
+     * @param parameters
+     * @param useStructuredResult
Evidence
The compliance rule forbids introducing AI-like comment noise and style churn. This PR adds new
JSDoc @param lines (representative example shown) across multiple methods/files without
accompanying behavioral need.

Rule 4: Remove AI-generated noise
src/driver/mysql/MysqlQueryRunner.ts[181-186]
src/driver/cockroachdb/CockroachQueryRunner.ts[267-272]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A large number of repetitive `@param` tags were added without functional changes, increasing noise and churn.
## Issue Context
The checklist requires removing AI-generated/low-value comment noise and keeping style consistent.
## Fix Focus Areas
- src/driver/mysql/MysqlQueryRunner.ts[181-186]
- src/driver/cockroachdb/CockroachQueryRunner.ts[267-272]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Aurora MySQL not covered 🐞 Bug ✓ Correctness
Description
AuroraMysqlQueryRunner still uses DROP+ADD for type/length changes, so Aurora MySQL users can still
lose data. The new test doesn’t enable the aurora-mysql driver, so the gap isn’t detected.
Code

test/github-issues/3357/issue-3357.test.ts[R15-23]

+            (connections = await createTestingConnections({
+                enabledDrivers: [
+                    "postgres",
+                    "cockroachdb",
+                    "mysql",
+                    "mariadb",
+                    "mssql",
+                ],
+                entities: [__dirname + "/entity/*{.js,.ts}"],
Evidence
The aurora-mysql QueryRunner’s changeColumn still treats type/length changes as requiring
dropColumn/addColumn. The new test suite enables mysql/mariadb but not aurora-mysql, so this
behavior remains untested and unfixed despite being MySQL-compatible.

src/driver/aurora-mysql/AuroraMysqlQueryRunner.ts[839-850]
test/github-issues/3357/issue-3357.test.ts[15-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
AuroraMysqlQueryRunner still drops and re-adds columns on type/length changes, so the data-loss bug persists for Aurora MySQL users.
### Issue Context
Aurora MySQL is MySQL-compatible and has its own QueryRunner implementation.
### Fix Focus Areas
- src/driver/aurora-mysql/AuroraMysqlQueryRunner.ts[839-851]
- test/github-issues/3357/issue-3357.test.ts[15-23]
### Notes
Mirror the MysqlQueryRunner fix:
- Use `ALTER TABLE ... MODIFY ...` for type/length changes.
- Ensure clonedTable cache updates correctly.
- Add `"aurora-mysql"` to test enabledDrivers (if supported by the test harness).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines 1 to 12
import "reflect-metadata"
import {
createTestingConnections,
closeTestingConnections,
} from "../../utils/test-utils"
import { DataSource } from "../../../src/data-source/DataSource"
import { expect } from "chai"
import { Post } from "./entity/Post"

describe("github issues > #3357 Migration generation should use ALTER COLUMN instead of DROP+ADD to prevent data loss", () => {
let connections: DataSource[]

Choose a reason for hiding this comment

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

Action required

1. Test added under github-issues 📘 Rule violation ⛯ Reliability

The fix adds a new test only under test/github-issues/3357, rather than the functional test suite.
This risks issue-specific test sprawl and does not meet the requirement to keep issue fixes in
functional tests.
Agent Prompt
## Issue description
The new coverage for issue #3357 was added only under `test/github-issues`, but the checklist requires issue fixes to live in the functional test suite.

## Issue Context
Per compliance, issue-specific tests should be placed in `test/functional` with an issue reference when applicable.

## Fix Focus Areas
- test/github-issues/3357/issue-3357.test.ts[1-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching this. You're right that the test should be in the functional test suite rather than under github-issues. I'll move it to test/functional/schema-builder/ with a proper reference to the issue.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Update the test header imports and suite description to match the functional test suite conventions: import expect first, use reflect-metadata, use DataSource from the public src entrypoint, and reference the correct functional fixture entity (Album) and suite name.

Suggested change
import "reflect-metadata"
import {
createTestingConnections,
closeTestingConnections,
} from "../../utils/test-utils"
import { DataSource } from "../../../src/data-source/DataSource"
import { expect } from "chai"
import { Post } from "./entity/Post"
describe("github issues > #3357 Migration generation should use ALTER COLUMN instead of DROP+ADD to prevent data loss", () => {
let connections: DataSource[]
import { expect } from "chai"
import "reflect-metadata"
import { DataSource } from "../../../src"
import {
closeTestingConnections,
createTestingConnections,
} from "../../utils/test-utils"
import { Album } from "./entity/Album"
describe("schema builder > alter column type", () => {
let connections: DataSource[]

See review comment here

Comment on lines 1295 to 1318
} else if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)

upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
newColumn.name
}" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
),
)
} else {

Choose a reason for hiding this comment

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

Action required

2. Skips combined column changes 🐞 Bug ✓ Correctness

When type/length changes, the new else-if branch emits only ALTER TYPE/MODIFY and skips the rest of
changeColumn logic (rename, nullability/default/PK/unique/FK renames, etc.). Any scenario where
type/length changes together with other attributes can generate incomplete/incorrect migrations
(e.g., rename+type change won’t rename).
Agent Prompt
### Issue description
Type/length changes currently take an `else if` branch that prevents executing the rest of the `changeColumn` logic. This causes incomplete migrations when multiple properties (rename, nullability, constraints, etc.) change together with type/length.

### Issue Context
`changeColumn` is invoked per changed column and is expected to handle all differences between `oldColumn` and `newColumn` in one pass. The new `else if` short-circuits that.

### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1282-1335]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1363-1405]
- src/driver/mysql/MysqlQueryRunner.ts[1095-1135]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1303-1348]

### Notes
A robust approach is:
1) Handle truly-incompatible cases via DROP+ADD (keep existing first `if`).
2) Otherwise enter the main change flow, and:
   - if rename: emit rename queries and update in-memory column names used for subsequent statements.
   - if type/length: emit ALTER TYPE/MODIFY/ALTER COLUMN queries.
   - continue with existing nullability/default/PK/unique/etc handling.
3) Ensure downQueries reverse the order (e.g., type back, then rename back).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Valid point. The current else if branch does short-circuit the rest of the changeColumn logic, which means combined changes (e.g., rename + type change) won't be handled correctly. I'll restructure this so the type/length ALTER is emitted within the main flow rather than as a separate branch, allowing rename, nullability, defaults, and constraint changes to still be processed in the same pass.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Adjust ALTER COLUMN TYPE generation to reference the correct column name in down migrations when a rename occurs, by using oldColumn.name for the up query and newColumn.name for the down query.

Suggested change
} else if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
newColumn.name
}" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
),
)
} else {
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)
// In the up-query, the column has not been renamed yet.
const upColumnName = oldColumn.name
// In the down-query, we're starting from the post-up state.
// If a rename happened, the current column name is newColumn.name.
const downColumnName = newColumn.name
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN "${upColumnName}" TYPE ${newFullType} USING "${upColumnName}"::${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN "${downColumnName}" TYPE ${oldFullType} USING "${downColumnName}"::${oldFullType}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}

See review comment here

Comment on lines 1111 to 1126
} else if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)
} else {

Choose a reason for hiding this comment

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

Action required

3. Stale schema cache after alter 🐞 Bug ⛯ Reliability

The new ALTER TYPE/MODIFY branches don’t update clonedTable/oldColumn, but changeColumn still
replaces the cached table with clonedTable. This leaves the QueryRunner’s cached schema stale after
a type/length change, which can corrupt subsequent diffing and SQL generation within the same schema
sync/migration generation.
Agent Prompt
### Issue description
After emitting ALTER TYPE/MODIFY statements, `clonedTable` is not updated, but it is still used to replace the cached table. This leaves the cached schema out of sync with the SQL that was generated/executed.

### Issue Context
`changeColumn` uses `clonedTable` as the new authoritative cached schema at the end of the method.

### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1295-1318]
- src/driver/postgres/PostgresQueryRunner.ts[2441-2443]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1374-1396]
- src/driver/cockroachdb/CockroachQueryRunner.ts[2151-2153]
- src/driver/mysql/MysqlQueryRunner.ts[1111-1126]
- src/driver/mysql/MysqlQueryRunner.ts[1612-1614]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1316-1347]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1956-1958]

### Notes
Minimum fix: locate the column in `clonedTable.columns` and update type/length/precision/scale/isArray/etc. to match `newColumn` when the ALTER/MODIFY branch is taken.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch — the clonedTable cache definitely needs to be updated after emitting the ALTER TYPE/MODIFY statements. I'll add the column property updates (type, length, precision, scale, isArray, etc.) on clonedTable.columns to match newColumn in each driver's type-change branch.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Update the in-memory schema cache (clonedTable) when the MySQL MODIFY branch is used by syncing all relevant column properties from newColumn onto the corresponding column in clonedTable, and expand the MODIFY-trigger condition to include precision/scale/width changes.

Suggested change
} else if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)
} else {
} else {
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length ||
oldColumn.width !== newColumn.width ||
oldColumn.precision !== newColumn.precision ||
oldColumn.scale !== newColumn.scale
) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence; the
// CHANGE (rename) statement is emitted afterwards.
const typeChangeColumn = newColumn.clone()
typeChangeColumn.name = oldColumn.name
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(typeChangeColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)
// Keep cached table schema in sync with the emitted MODIFY statement.
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.width = newColumn.width
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
clonedColumn.unsigned = newColumn.unsigned
clonedColumn.zerofill = newColumn.zerofill
clonedColumn.charset = newColumn.charset
clonedColumn.collation = newColumn.collation
}
}
if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
newColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
)}`,
),
)

See review comment here

Comment on lines 1374 to 1396
} else if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)

upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
newColumn.name
}" TYPE ${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType}`,
),
)
} else {

Choose a reason for hiding this comment

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

Action required

4. Enum conversions may emit invalid type 🐞 Bug ✓ Correctness

Postgres/Cockroach ALTER TYPE branches use driver.createFullType(), but enums are represented with
TableColumn.type = "enum" and the real DB type name is derived via buildEnumName()/enumName. If a
column changes between enum and non-enum types, the new branch can emit TYPE enum (and invalid down
SQL) and it also bypasses enum type lifecycle handling (create/drop/rename) used elsewhere.
Agent Prompt
### Issue description
Enum columns require special type naming (via `buildEnumName`/`enumName`) and often enum type lifecycle operations. The new type/length ALTER branch uses `createFullType`, which will produce `enum` for enum columns and bypasses existing enum-specific handling.

### Issue Context
Postgres introspection sets `TableColumn.type = "enum"` and stores `enumName` separately, and SQL generation uses `buildEnumName()` for enum types.

### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1295-1318]
- src/driver/postgres/PostgresQueryRunner.ts[3819-3835]
- src/driver/postgres/PostgresQueryRunner.ts[4997-5027]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1374-1396]
- src/driver/cockroachdb/CockroachQueryRunner.ts[4352-4372]

### Notes
Two viable paths:
- Conservative: fall back to DROP+ADD when converting to/from enum.
- Full support: ensure enum type exists (create), then ALTER COLUMN TYPE to the correct enum identifier (using `buildEnumName`), and drop old enum types when appropriate; ensure down migration mirrors this.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed — enum columns need special handling. For the initial implementation, I'll take the conservative approach and fall back to DROP+ADD for any enum-related type conversions. This keeps the scope manageable while still fixing the core issue for non-enum type/length changes.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Update CockroachDB changeColumn compatibility checks to conservatively fall back to DROP+ADD when converting between enum/simple-enum and non-enum types. This prevents generating invalid ALTER COLUMN ... TYPE SQL for enum-related conversions and keeps enum lifecycle handling in the existing enum-specific branch.

Suggested change
} else if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
newColumn.name
}" TYPE ${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType}`,
),
)
} else {
const oldIsEnum = oldColumn.type === "enum" || oldColumn.type === "simple-enum"
const newIsEnum = newColumn.type === "enum" || newColumn.type === "simple-enum"
if (
newColumn.isArray !== oldColumn.isArray ||
oldColumn.generatedType !== newColumn.generatedType ||
oldColumn.asExpression !== newColumn.asExpression ||
// Conservative approach: enum-related conversions are not handled by the generic ALTER TYPE path.
// This includes enum <-> non-enum and enum <-> simple-enum conversions.
(oldIsEnum !== newIsEnum || (oldIsEnum && newIsEnum && oldColumn.type !== newColumn.type))
) {
// These changes are incompatible with ALTER COLUMN, so we must recreate
await this.dropColumn(table, oldColumn)
await this.addColumn(table, newColumn)
// update cloned table
clonedTable = table.clone()
} else {

See review comment here

@qodo-free-for-open-source-projects

Persistent review updated to latest commit 5232a03

Comment on lines +1323 to 1368
if (
newColumn.type !== oldColumn.type ||
newColumn.length !== oldColumn.length
) {
// Use ALTER COLUMN instead of DROP+ADD to preserve existing data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN ${this.buildCreateColumnSql(
table,
newColumn,
true,
false,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN ${this.buildCreateColumnSql(
table,
oldColumn,
true,
false,
true,
)}`,
),
)

// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// we need database name and schema name to rename FK constraints
let dbName: string | undefined = undefined

Choose a reason for hiding this comment

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

Action required

1. Sqlserver alter wrong column 🐞 Bug ✓ Correctness

SQL Server ALTER COLUMN is built from newColumn (including its name) before sp_rename runs, so
name+type/length changes in one call can reference a non-existent column. This breaks manual
migrations (QueryRunner API) and any future schema-builder paths that pass both changes together.
Agent Prompt
### Issue description
SQL Server `changeColumn` emits `ALTER COLUMN` using a column definition built from `newColumn` (including `newColumn.name`) before it emits `sp_rename`. If a caller changes name + type/length in one call, the ALTER targets the wrong name.

### Issue Context
`buildCreateColumnSql` always embeds the column name, so passing `newColumn` directly is unsafe unless the rename has already happened.

### Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1323-1353]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1386-1400]
- src/driver/sqlserver/SqlServerQueryRunner.ts[4167-4176]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. The ALTER COLUMN should target oldColumn.name (since the rename hasn't happened yet at that point), not newColumn.name. I'll fix the ordering so the type change uses the current column name, and the rename follows afterward.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Update SQL Server changeColumn type/length ALTER COLUMN generation to target the current (old) column name when a rename is also included in the same changeColumn call, by cloning newColumn and overriding its name to oldColumn.name for the ALTER COLUMN statement.

Suggested change
if (
newColumn.type !== oldColumn.type ||
newColumn.length !== oldColumn.length
) {
// Use ALTER COLUMN instead of DROP+ADD to preserve existing data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN ${this.buildCreateColumnSql(
table,
newColumn,
true,
false,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN ${this.buildCreateColumnSql(
table,
oldColumn,
true,
false,
true,
)}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// we need database name and schema name to rename FK constraints
let dbName: string | undefined = undefined
if (
newColumn.type !== oldColumn.type ||
newColumn.length !== oldColumn.length
) {
// Use ALTER COLUMN instead of DROP+ADD to preserve existing data.
// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence; the RENAME
// statement is emitted afterwards.
const typeChangeColumn = newColumn.clone()
typeChangeColumn.name = oldColumn.name
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN ${this.buildCreateColumnSql(
table,
typeChangeColumn,
true,
false,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN ${this.buildCreateColumnSql(
table,
oldColumn,
true,
false,
true,
)}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}

See review comment here

Comment on lines +1119 to 1148
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)

// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
upQueries.push(

Choose a reason for hiding this comment

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

Action required

2. Mysql rename overwrites type 🐞 Bug ✓ Correctness

If changeColumn is called with both name and type/length changes, it emits MODIFY using
newColumn (new name) and then emits CHANGE old→new using the *old column definition*, which can
fail and/or revert the type/length change. This breaks manual migrations (QueryRunner API) for
combined rename+type operations.
Agent Prompt
### Issue description
MySQL `changeColumn` can generate conflicting SQL when both the column name and type/length change in the same call: it first emits `MODIFY` using `newColumn` (includes new name), then emits `CHANGE old→new` using `oldColumn`’s definition (can revert the type/length change).

### Issue Context
`buildCreateColumnSql(newColumn, true)` includes the column name by default, and `CHANGE` requires a full column definition. Using `oldColumn` in the rename step is only correct when renaming without other column changes.

### Fix Focus Areas
- src/driver/mysql/MysqlQueryRunner.ts[1119-1169]
- src/driver/mysql/MysqlQueryRunner.ts[3475-3487]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right — the CHANGE statement using oldColumn's definition will undo the type/length change from the preceding MODIFY. For MySQL, when both name and type/length change, I should emit a single CHANGE statement with the new name AND new column definition, rather than separate MODIFY + CHANGE steps.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Update MySQL changeColumn SQL generation to avoid emitting separate MODIFY + CHANGE statements when both rename and type/length changes occur. Instead, emit a single CHANGE oldName -> newName statement using the new column definition, and adjust the down query accordingly. Keep the existing MODIFY-only path for type/length changes without rename.

Suggested change
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
upQueries.push(
} else {
const isTypeOrLengthChanged =
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
if (isTypeOrLengthChanged && newColumn.name !== oldColumn.name) {
// If both rename and type/length change, emit a single CHANGE with the new definition.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(
newColumn,
true,
true,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
newColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
true,
)}`,
),
)
// update cloned table column properties + name
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.name = newColumn.name
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
} else {
if (isTypeOrLengthChanged) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence.
const typeChangeColumn = newColumn.clone()
typeChangeColumn.name = oldColumn.name
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(typeChangeColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
newColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
true,
)}`,
),
)
// rename index constraints
clonedTable.findColumnIndices(oldColumn).forEach((index) => {
const oldUniqueName =
this.connection.namingStrategy.indexName(
clonedTable,
index.columnNames,
)
// Skip renaming if Index has user defined constraint name
if (index.name !== oldUniqueName) return
// build new constraint name
index.columnNames.splice(
index.columnNames.indexOf(oldColumn.name),
1,
)
index.columnNames.push(newColumn.name)
const columnNames = index.columnNames
.map((column) => `\`${column}\``)
.join(", ")
const newIndexName =
this.connection.namingStrategy.indexName(
clonedTable,
index.columnNames,
index.where,
)
// build queries
let indexType = ""
if (index.isUnique) indexType += "UNIQUE "
if (index.isSpatial) indexType += "SPATIAL "
if (index.isFulltext) indexType += "FULLTEXT "
const indexParser =
index.isFulltext && index.parser
? ` WITH PARSER ${index.parser}`
: ""
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} DROP INDEX \`${
index.name
}\`, ADD ${indexType}INDEX \`${newIndexName}\` (${columnNames})${indexParser}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} DROP INDEX \`${newIndexName}\`, ADD ${indexType}INDEX \`${
index.name
}\` (${columnNames})${indexParser}`,
),
)
// replace constraint name
index.name = newIndexName
})
}
}

See review comment here

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 17, 2026

commit: e610d3d

@qodo-free-for-open-source-projects

Persistent review updated to latest commit aa1dfb0

Comment on lines +1302 to 1339
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)

upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
newColumn.name
}" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
),
)

// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (oldColumn.name !== newColumn.name) {
// rename column
upQueries.push(

Choose a reason for hiding this comment

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

Action required

1. Postgres alter uses new name 🐞 Bug ✓ Correctness

In Postgres changeColumn, the new ALTER COLUMN TYPE query targets newColumn.name even though the
column rename happens later, so rename+type/length changes can generate migrations that fail at
runtime. The down query similarly targets oldColumn.name even though the down-rename happens later.
Agent Prompt
### Issue description
Postgres changeColumn emits `ALTER COLUMN ... TYPE` using `newColumn.name` before the column is renamed, and emits the down-type change using `oldColumn.name` before the down-rename. This breaks migrations that both rename and change type/length.

### Issue Context
`upQueries` and `downQueries` are executed in the order they are pushed. When a rename is involved, the column’s name in the database changes only when the rename query runs.

### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1302-1352]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, same issue as the SQL Server one — the ALTER COLUMN TYPE query needs to use oldColumn.name since the rename hasn't happened yet. I'll fix this across both Postgres up and down queries.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Update Postgres changeColumn to generate ALTER COLUMN ... TYPE statements using the correct column identifier when a rename is also part of the change: use oldColumn.name for the up ALTER COLUMN (since rename happens later), and use newColumn.name for the down ALTER COLUMN (since the down-rename happens later). Adjust the USING clause to match the targeted column name in each query.

Suggested change
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
newColumn.name
}" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (oldColumn.name !== newColumn.name) {
// rename column
upQueries.push(
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)
// Use oldColumn.name in the up-query because the column has not been renamed yet
// at this point in the query sequence (the RENAME COLUMN statement is emitted afterwards).
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${newFullType} USING "${oldColumn.name}"::${newFullType}`,
),
)
// Use newColumn.name in the down-query because the column will be renamed back afterwards
// in the down query sequence.
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
newColumn.name
}" TYPE ${oldFullType} USING "${newColumn.name}"::${oldFullType}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}

See review comment here

Comment on lines +1119 to 1148
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)

// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
upQueries.push(

Choose a reason for hiding this comment

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

Action required

2. Mysql modify+change inconsistent 🐞 Bug ✓ Correctness

In MySQL changeColumn, MODIFY is emitted using newColumn (new name) before the CHANGE rename runs,
so rename+type/length changes can reference a non-existent column name. Even if name targeting is
fixed, the CHANGE statement uses oldColumn’s definition, which can undo the earlier type/length
change.
Agent Prompt
### Issue description
MySQL changeColumn emits `MODIFY` using `newColumn` before renaming, and the rename `CHANGE` statement uses `oldColumn`’s definition, which can both (1) target a non-existent name and (2) revert the type/length.

### Issue Context
In MySQL, `CHANGE old new <definition>` includes a full column definition.

### Fix Focus Areas
- src/driver/mysql/MysqlQueryRunner.ts[1119-1169]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same root cause as the other MySQL comment — I'll consolidate rename + type/length into a single CHANGE statement when both are needed.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Consolidate MySQL rename + type/length changes into a single ALTER TABLE ... CHANGE statement when both are needed, and avoid emitting a preceding MODIFY that could target a non-existent column name or be reverted by a later CHANGE. Keep MODIFY only for non-rename type/length changes, and keep CHANGE (rename-only) using the old definition when no other definition changes are involved.

Suggested change
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
upQueries.push(
} else {
const isTypeOrLengthChanged =
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
// If both definition (type/length) AND name are changing, emit a single CHANGE.
// In MySQL, CHANGE includes the full column definition, so a later CHANGE can
// otherwise revert an earlier MODIFY.
if (isTypeOrLengthChanged && newColumn.name !== oldColumn.name) {
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(
newColumn,
true,
true,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
newColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
true,
)}`,
),
)
// update cloned table column definition + name
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
clonedColumn.name = newColumn.name
}
oldColumn.name = newColumn.name
} else {
if (isTypeOrLengthChanged) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence.
const typeChangeColumn = newColumn.clone()
typeChangeColumn.name = oldColumn.name
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(typeChangeColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// Rename only (no definition changes handled above).
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
newColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
true,
)}`,
),
)
// rename index constraints
clonedTable.findColumnIndices(oldColumn).forEach((index) => {
const oldUniqueName =
this.connection.namingStrategy.indexName(
clonedTable,
index.columnNames,
)
// Skip renaming if Index has user defined constraint name
if (index.name !== oldUniqueName) return
// build new constraint name
index.columnNames.splice(
index.columnNames.indexOf(oldColumn.name),
1,
)
index.columnNames.push(newColumn.name)
const columnNames = index.columnNames
.map((column) => `\`${column}\``)
.join(", ")
const newIndexName =
this.connection.namingStrategy.indexName(
clonedTable,
index.columnNames,
index.where,
)
// build queries
let indexType = ""
if (index.isUnique) indexType += "UNIQUE "
if (index.isSpatial) indexType += "SPATIAL "
if (index.isFulltext) indexType += "FULLTEXT "
const indexParser =
index.isFulltext && index.parser
? ` WITH PARSER ${index.parser}`
: ""
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} DROP INDEX \`${
index.name
}\`, ADD ${indexType}INDEX \`${newIndexName}\` (${columnNames})${indexParser}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} DROP INDEX \`${newIndexName}\`, ADD ${indexType}INDEX \`${
index.name
}\` (${columnNames})${indexParser}`,
),
)
// replace constraint name
index.name = newIndexName
})
// rename foreign key constraints
clonedTable
.findColumnForeignKeys(oldColumn)
.forEach((foreignKey) => {
const foreignKeyName =
this.connection.namingStrategy.foreignKeyName(
clonedTable,
foreignKey.columnNames,
this.getTablePath(foreignKey),
foreignKey.referencedColumnNames,
)
// Skip renaming if foreign key has user defined constraint name
if (foreignKey.name !== foreignKeyName) return
// build new constraint name
foreignKey.columnNames.splice(
foreignKey.columnNames.indexOf(oldColumn.name),
1,
)
foreignKey.columnNames.push(newColumn.name)
const columnNames = foreignKey.columnNames
.map((column) => `\`${column}\``)
.join(", ")
const referencedColumnNames =
foreignKey.referencedColumnNames
.map((column) => `\`${column}\``)
.join(",")
const newForeignKeyName =
this.connection.namingStrategy.foreignKeyName(
clonedTable,
foreignKey.columnNames,
this.getTablePath(foreignKey),
foreignKey.referencedColumnNames,
)
// build queries
let up =
`ALTER TABLE ${this.escapePath(
table,
)} DROP FOREIGN KEY \`${
foreignKey.name
}\`, ADD CONSTRAINT \`${newForeignKeyName}\` FOREIGN KEY (${columnNames}) ` +
`REFERENCES ${this.escapePath(
this.getTablePath(foreignKey),
)}(${referencedColumnNames})`
if (foreignKey.onDelete)
up += ` ON DELETE ${foreignKey.onDelete}`
if (foreignKey.onUpdate)
up += ` ON UPDATE ${foreignKey.onUpdate}`
let down =
`ALTER TABLE ${this.escapePath(
table,
)} DROP FOREIGN KEY \`${newForeignKeyName}\`, ADD CONSTRAINT \`${
foreignKey.name
}\` FOREIGN KEY (${columnNames}) ` +
`REFERENCES ${this.escapePath(
this.getTablePath(foreignKey),
)}(${referencedColumnNames})`
if (foreignKey.onDelete)
down += ` ON DELETE ${foreignKey.onDelete}`
if (foreignKey.onUpdate)
down += ` ON UPDATE ${foreignKey.onUpdate}`
upQueries.push(new Query(up))
downQueries.push(new Query(down))
// replace constraint name
foreignKey.name = newForeignKeyName
})
// rename old column in the Table object
const oldTableColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
clonedTable.columns[
clonedTable.columns.indexOf(oldTableColumn!)
].name = newColumn.name
oldColumn.name = newColumn.name
}
}

See review comment here

@qodo-free-for-open-source-projects

Persistent review updated to latest commit 3a53020

Comment on lines 1074 to 1108
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence; the
// CHANGE (rename) statement is emitted afterwards.
const typeChangeColumn = newColumn.clone()
typeChangeColumn.name = oldColumn.name
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(typeChangeColumn, true)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
),
)

// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}
if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
upQueries.push(

Choose a reason for hiding this comment

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

Action required

1. Mysql rename reverts type 🐞 Bug ✓ Correctness

If a column is renamed and its type/length changes in the same changeColumn call, the emitted
MySQL/Aurora CHANGE statement uses the *old* column definition and can undo the earlier MODIFY
type/length change.
Agent Prompt
### Issue description
MySQL/Aurora `changeColumn` can emit `MODIFY` (type/length) and then `CHANGE` (rename) where the `CHANGE` uses the old column definition, potentially reverting the earlier type/length change.

### Issue Context
In MySQL, `ALTER TABLE ... CHANGE old new <column_definition>` always applies the provided column definition. If the definition is built from `oldColumn` while a type/length change was just applied, the rename statement can undo it.

### Fix Focus Areas
- src/driver/mysql/MysqlQueryRunner.ts[1074-1129]
- src/driver/aurora-mysql/AuroraMysqlQueryRunner.ts[857-912]

### Implementation notes
- If `(type/length changed) && (name changed)`: prefer emitting **one** `CHANGE` statement using the **new** column definition (with `newColumn.name`) and skip the separate `MODIFY` + rename block.
- Ensure `downQueries` reverses both rename and type/length in the correct order (often by also using a single `CHANGE` back to the old name with the old definition).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — this is the same issue flagged earlier. The fix will be to use a single CHANGE with the correct (new) column definition when both rename and type/length change together.

Comment on lines 1302 to 1327
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)

// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence; the RENAME
// statement is emitted afterwards.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${newFullType} USING "${oldColumn.name}"::${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
),
)

Choose a reason for hiding this comment

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

Action required

2. Alter type keeps defaults 🐞 Bug ⛯ Reliability

Postgres/Cockroach now run ALTER COLUMN ... TYPE for type/length changes without temporarily
dropping DEFAULTs; type changes can fail when an existing default expression cannot be cast to the
new type.
Agent Prompt
### Issue description
`ALTER COLUMN TYPE` is now used for type/length changes, but defaults are not dropped first. Some defaults are not castable to the new type and will cause migrations to fail.

### Issue Context
The code already drops defaults for enum changes to avoid cast failures, but not for general type/length changes. This creates an inconsistency and a regression risk for columns with defaults.

### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1302-1327]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1334-1358]
- src/driver/postgres/PostgresQueryRunner.ts[1755-1780]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1898-1921]

### Implementation notes
- In the type/length-change block, if `oldColumn.default` is non-null/undefined:
  - push `ALTER TABLE ... ALTER COLUMN "<name>" DROP DEFAULT` before the TYPE change
  - mark `defaultValueChanged = true`
  - after the TYPE change, re-apply `newColumn.default` (if defined) or re-apply `oldColumn.default` as appropriate for up/down.
- Ensure existing later default-handling logic respects `defaultValueChanged` to prevent duplicate SET/DROP DEFAULT statements.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I'll add DROP DEFAULT before the ALTER TYPE and re-apply the default afterward, similar to how it's already done for enum changes. This prevents cast failures when the existing default isn't compatible with the new type.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Update the type/length change path to temporarily DROP DEFAULT before ALTER COLUMN ... TYPE and then re-apply the appropriate default afterward. Mark the default as changed to avoid duplicate default handling later in changeColumn.

Suggested change
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)
// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence; the RENAME
// statement is emitted afterwards.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${newFullType} USING "${oldColumn.name}"::${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
),
)
if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length
) {
// Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
// PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
const newFullType = this.driver.createFullType(newColumn)
const oldFullType = this.driver.createFullType(oldColumn)
// If column has a default value, temporarily drop it to avoid type cast issues.
if (oldColumn.default !== null && oldColumn.default !== undefined) {
// mark default as changed to prevent double update later in this method
defaultValueChanged = true
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" DROP DEFAULT`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" SET DEFAULT ${oldColumn.default}`,
),
)
}
// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence; the RENAME
// statement is emitted afterwards.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${newFullType} USING "${oldColumn.name}"::${newFullType}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
),
)
// Re-apply default after type change (if it was dropped above).
if (oldColumn.default !== null && oldColumn.default !== undefined) {
if (newColumn.default !== null && newColumn.default !== undefined) {
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" SET DEFAULT ${newColumn.default}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" DROP DEFAULT`,
),
)
} else {
// old had default, new does not => keep it dropped in up
// down should restore old default (already handled above before TYPE)
}
}
// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}

See review comment here

Comment on lines +3 to +14
import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn"

@Entity({ synchronize: false })
@Entity()
export class Album {
@PrimaryColumn()
@PrimaryGeneratedColumn()
id: number

@Column()
name: string
@Column({ type: "varchar", length: "50" })
title: string

@Column({ type: "varchar", length: "200" })
description: string

Choose a reason for hiding this comment

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

Action required

3. Album entity breaks tests 🐞 Bug ⛯ Reliability

The shared schema-builder Album test entity was changed to @Entity() (synchronize defaults on)
and different columns, which can cause other schema-builder suites to auto-create/drop album
during reloadTestingDatabases() and break test isolation.
Agent Prompt
### Issue description
A shared schema-builder test entity (`test/functional/schema-builder/entity/Album.ts`) was modified to be synchronized by default and to change its columns. Other test suites import this same entity and call `reloadTestingDatabases()`, which runs `synchronize(true)` and can now unexpectedly create/drop tables.

### Issue Context
This can lead to cross-test pollution or failures in `custom-db-and-schema-sync` (and any suite importing this entity).

### Fix Focus Areas
- test/functional/schema-builder/entity/Album.ts[1-15]
- test/functional/schema-builder/custom-db-and-schema-sync.test.ts[14-25]
- test/utils/test-utils.ts[504-509]
- test/functional/schema-builder/alter-column-type.test.ts[1-27]

### Implementation notes
- Preferred: create a new entity file used only by `alter-column-type.test.ts` (e.g. `test/functional/schema-builder/alter-column-type/entity/Album.ts` or similar) and revert the shared `schema-builder/entity/Album.ts` to its prior behavior.
- Alternative: keep shared `Album` as `synchronize:false` and adjust the new test to explicitly create schema/tables (e.g., set metadata.synchronize=true then `connection.synchronize(true)` in the test setup).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right — modifying the shared Album entity is a bad idea since it affects other test suites. I'll create a dedicated entity file for the alter-column-type test and revert the shared Album entity to its original state.

Choose a reason for hiding this comment

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

Code Implementation 🛠️

Implementation: Prevent the shared schema-builder Album test entity from being auto-synchronized by default by disabling synchronization at the entity level, avoiding cross-suite schema changes during reloadTestingDatabases().

Suggested change
import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn"
@Entity({ synchronize: false })
@Entity()
export class Album {
@PrimaryColumn()
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@Column({ type: "varchar", length: "50" })
title: string
@Column({ type: "varchar", length: "200" })
description: string
import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn"
@Entity({ synchronize: false })
export class Album {
@PrimaryGeneratedColumn()
id: number
@Column({ type: "varchar", length: "50" })
title: string
@Column({ type: "varchar", length: "200" })
description: string

See review comment here

@qodo-free-for-open-source-projects

Persistent review updated to latest commit 78f5e5e

Comment on lines 1107 to 1109
if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
upQueries.push(

Choose a reason for hiding this comment

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

Action required

1. Mysql rename+type change reverts type 🐞 Bug ✓ Correctness

When a column's type/length AND name change simultaneously, the new MODIFY step correctly applies
the new type, but the subsequent CHANGE (rename) step calls buildCreateColumnSql(oldColumn, ...)
which carries the original type/length definition. MySQL's CHANGE syntax rewrites the full column
definition, so the rename step silently reverts the type change made by MODIFY.
Agent Prompt
## Issue description
When a column's type/length AND name change simultaneously in MySQL/AuroraMySQL, the new MODIFY step correctly applies the new type, but the subsequent CHANGE (rename) step uses `buildCreateColumnSql(oldColumn, ...)` which carries the original type/length. MySQL's CHANGE syntax rewrites the full column definition, so the rename step silently reverts the type change.

## Issue Context
MySQL's `CHANGE \`old\` \`new\` <full_col_def>` syntax requires a complete column definition. The current code passes `oldColumn` (original type/length) to `buildCreateColumnSql`, overwriting the MODIFY that preceded it.

## Fix Focus Areas
- src/driver/mysql/MysqlQueryRunner.ts[1107-1130]
- src/driver/aurora-mysql/AuroraMysqlQueryRunner.ts[889-910]

In both files, when the rename block fires after a type-change MODIFY, the CHANGE up-query should use `typeChangeColumn` (already constructed with `newColumn`'s type and `oldColumn.name`) instead of `oldColumn`. The down-query should use `oldColumn` (which is correct for reverting). Specifically, replace `buildCreateColumnSql(oldColumn, true, true)` in the up-query of the rename block with `buildCreateColumnSql(typeChangeColumn, true, true)` — but note `typeChangeColumn` must be in scope (move it to the outer `else` block if needed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — same MySQL rename+type issue. Will be addressed as part of the CHANGE consolidation fix.

Comment on lines 1323 to 1370
if (
newColumn.type !== oldColumn.type ||
newColumn.length !== oldColumn.length
) {
// Use ALTER COLUMN instead of DROP+ADD to preserve existing data.
// Use oldColumn.name in the up-query because the column has not
// been renamed yet at this point in the query sequence; the RENAME
// statement is emitted afterwards.
const typeChangeColumn = newColumn.clone()
typeChangeColumn.name = oldColumn.name
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN ${this.buildCreateColumnSql(
table,
typeChangeColumn,
true,
false,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(
table,
)} ALTER COLUMN ${this.buildCreateColumnSql(
table,
oldColumn,
true,
false,
true,
)}`,
),
)

// update cloned table column type/length
const clonedColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
if (clonedColumn) {
clonedColumn.type = newColumn.type
clonedColumn.length = newColumn.length
clonedColumn.precision = newColumn.precision
clonedColumn.scale = newColumn.scale
}
}

Choose a reason for hiding this comment

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

Action required

2. Sql server type change fails on indexed columns 🐞 Bug ⛯ Reliability

SQL Server does not allow ALTER TABLE … ALTER COLUMN to change a column's data type when
non-clustered indexes or statistics reference that column. The new type-change block emits the ALTER
COLUMN directly without dropping and re-creating dependent indexes, causing a guaranteed runtime
failure for any indexed column.
Agent Prompt
## Issue description
SQL Server does not allow `ALTER TABLE … ALTER COLUMN` to change a column's data type when non-clustered indexes or statistics reference that column. The new type-change block emits the ALTER COLUMN directly without managing dependent indexes, causing a runtime failure for any indexed column.

## Issue Context
SQL Server error: 'The object \'IX_...\' is dependent on column \'...\'. ALTER TABLE ALTER COLUMN ... failed because one or more objects access this column.'

The rename path in the same method already shows the correct pattern for iterating indexes via `clonedTable.findColumnIndices(oldColumn)`. For a type change, indexes must be dropped before and re-created after.

## Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1323-1370]

Before the `ALTER COLUMN` up-query, iterate `clonedTable.findColumnIndices(oldColumn)` and emit `DROP INDEX` for each. After the `ALTER COLUMN`, emit `CREATE INDEX` to restore them. Do the same for unique constraints via `clonedTable.findColumnUniques(oldColumn)`. Mirror the down-queries in reverse order. Reference the existing index-drop/recreate logic already present in the `changeColumn` method for the nullability-change path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch — SQL Server does require indexes to be dropped before altering a column's type. I'll add the index drop/recreate logic similar to what already exists in the nullability-change path, using clonedTable.findColumnIndices() and clonedTable.findColumnUniques().

@veeceey veeceey changed the title fix: use ALTER COLUMN instead of DROP+ADD to prevent data loss fix: use ALTER COLUMN instead of DROP and ADD to prevent data loss Feb 20, 2026
@veeceey
Copy link
Contributor Author

veeceey commented Feb 20, 2026

Thanks for the thorough review! I've responded to all the inline comments. Here's a summary of the fixes I'll be making:

  1. Test location: Move test from github-issues to test/functional/schema-builder/
  2. Combined changes: Restructure the type/length ALTER to work within the main changeColumn flow instead of short-circuiting via else if
  3. Schema cache: Update clonedTable.columns after ALTER TYPE/MODIFY
  4. Enum handling: Fall back to DROP+ADD for enum-related conversions
  5. Column name ordering: Use oldColumn.name for type changes that happen before renames (Postgres, SQL Server)
  6. MySQL rename+type: Consolidate into a single CHANGE statement with correct definition
  7. Default handling: Drop and re-apply defaults around ALTER TYPE (Postgres/CockroachDB)
  8. Shared entity: Create dedicated test entity instead of modifying shared Album
  9. SQL Server indexes: Drop and recreate dependent indexes around ALTER COLUMN

Will push these fixes shortly.

veeceey and others added 2 commits February 19, 2026 23:50
…rations

When a column's type or length changes, the migration generator previously
used DROP COLUMN followed by ADD COLUMN, which destroys all existing data
in the column. This changes the behavior to use ALTER COLUMN TYPE (Postgres,
CockroachDB), MODIFY COLUMN (MySQL/MariaDB), or ALTER COLUMN (SQL Server)
instead, which preserves the data.

DROP+ADD is still used for truly incompatible changes like array<->non-array
conversions and generated column expression changes, where ALTER COLUMN
cannot be used.

Fixes: typeorm#3357
- Move type/length ALTER logic into main change flow so it no longer
  short-circuits rename, nullability, default, and constraint changes
- Update clonedTable.columns after ALTER TYPE/MODIFY to prevent stale
  schema cache
- Fall back to DROP+ADD for enum type conversions since createFullType()
  produces literal "enum" string
- Move test from test/github-issues/3357/ to test/functional/schema-builder/
  per project conventions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@veeceey veeceey force-pushed the fix/issue-3357-migration-alter-column branch from 78f5e5e to e610d3d Compare February 20, 2026 07:51
@qodo-free-for-open-source-projects

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (4) 📎 Requirement gaps (0)

Grey Divider


Action required

1. MySQL rename+type broken 🐞 Bug ✓ Correctness ⭐ New
Description
MysqlQueryRunner uses MODIFY ${buildCreateColumnSql(newColumn)} for type/length changes (which
includes the *new* column name), and then separately uses CHANGE for renames using the *old*
column definition. Rename+type/length changes can fail (MODIFY targets the new name before it
exists) and even if adjusted to target the old name, the subsequent rename CHANGE can revert the
type because it uses oldColumn’s full type.
Code

src/driver/mysql/MysqlQueryRunner.ts[R1120-1149]

+            if (
+                oldColumn.type !== newColumn.type ||
+                oldColumn.length !== newColumn.length
+            ) {
+                // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
+                    ),
+                )
+
+                // update cloned table column type/length
+                const clonedColumn = clonedTable.columns.find(
+                    (column) => column.name === oldColumn.name,
+                )
+                if (clonedColumn) {
+                    clonedColumn.type = newColumn.type
+                    clonedColumn.length = newColumn.length
+                    clonedColumn.precision = newColumn.precision
+                    clonedColumn.scale = newColumn.scale
+                }
+            }
            if (newColumn.name !== oldColumn.name) {
                // We don't change any column properties, just rename it.
                upQueries.push(
Evidence
buildCreateColumnSql includes the column name by default, so `MODIFY
${buildCreateColumnSql(newColumn)} targets newColumn.name`. The rename block later performs
CHANGE oldName newName <full type from oldColumn>, which would reapply the old type/length if it
executes after a type change.

src/driver/mysql/MysqlQueryRunner.ts[1120-1169]
src/driver/mysql/MysqlQueryRunner.ts[3476-3488]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
MySQL/MariaDB changeColumn can generate invalid SQL for a combined rename + type/length change, because it emits a `MODIFY` using `newColumn` (new name) before the rename, and later emits a `CHANGE` rename using `oldColumn`&#x27;s type definition.

### Issue Context
`buildCreateColumnSql(newColumn, ...)` includes the column name, so `MODIFY` targets `newColumn.name`.

### Fix Focus Areas
- src/driver/mysql/MysqlQueryRunner.ts[1120-1169]
- src/driver/mysql/MysqlQueryRunner.ts[3476-3488]
- test/functional/schema-builder/alter-column-type.test.ts[1-139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 2. Test added under github-issues 📘 Rule violation ⛯ Reliability
Description
The fix adds a new test only under test/github-issues/3357, rather than the functional test suite.
This risks issue-specific test sprawl and does not meet the requirement to keep issue fixes in
functional tests.
Code

test/github-issues/3357/issue-3357.test.ts[R1-12]

+import "reflect-metadata"
+import {
+    createTestingConnections,
+    closeTestingConnections,
+} from "../../utils/test-utils"
+import { DataSource } from "../../../src/data-source/DataSource"
+import { expect } from "chai"
+import { Post } from "./entity/Post"
+
+describe("github issues > #3357 Migration generation should use ALTER COLUMN instead of DROP+ADD to prevent data loss", () => {
+    let connections: DataSource[]
+
Evidence
The compliance rule requires issue fixes to be validated in test/functional and flags tests added
only under test/github-issues without a clear reason. This PR adds the validating test under
test/github-issues/3357.

Rule 3: Prefer functional tests over per-issue tests
test/github-issues/3357/issue-3357.test.ts[1-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new coverage for issue #3357 was added only under `test/github-issues`, but the checklist requires issue fixes to live in the functional test suite.
## Issue Context
Per compliance, issue-specific tests should be placed in `test/functional` with an issue reference when applicable.
## Fix Focus Areas
- test/github-issues/3357/issue-3357.test.ts[1-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 3. Skips combined column changes 🐞 Bug ✓ Correctness
Description
When type/length changes, the new else-if branch emits only ALTER TYPE/MODIFY and skips the rest of
changeColumn logic (rename, nullability/default/PK/unique/FK renames, etc.). Any scenario where
type/length changes together with other attributes can generate incomplete/incorrect migrations
(e.g., rename+type change won’t rename).
Code

src/driver/postgres/PostgresQueryRunner.ts[R1295-1318]

+        } else if (
+            oldColumn.type !== newColumn.type ||
+            oldColumn.length !== newColumn.length
+        ) {
+            // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
+            // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
+            const newFullType = this.driver.createFullType(newColumn)
+            const oldFullType = this.driver.createFullType(oldColumn)
+
+            upQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        newColumn.name
+                    }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+                ),
+            )
+            downQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        oldColumn.name
+                    }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
+                ),
+            )
   } else {
Evidence
The Postgres/Cockroach/MySQL implementations now have a mutually-exclusive else-if for type/length
which prevents entering the main else branch where rename and other column alterations are handled.
This means combined changes in one column diff can be partially applied.

src/driver/postgres/PostgresQueryRunner.ts[1282-1335]
src/driver/postgres/PostgresQueryRunner.ts[1826-1834]
src/driver/mysql/MysqlQueryRunner.ts[1095-1127]
src/driver/mysql/MysqlQueryRunner.ts[1391-1396]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Type/length changes currently take an `else if` branch that prevents executing the rest of the `changeColumn` logic. This causes incomplete migrations when multiple properties (rename, nullability, constraints, etc.) change together with type/length.
### Issue Context
`changeColumn` is invoked per changed column and is expected to handle all differences between `oldColumn` and `newColumn` in one pass. The new `else if` short-circuits that.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1282-1335]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1363-1405]
- src/driver/mysql/MysqlQueryRunner.ts[1095-1135]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1303-1348]
### Notes
A robust approach is:
1) Handle truly-incompatible cases via DROP+ADD (keep existing first `if`).
2) Otherwise enter the main change flow, and:
- if rename: emit rename queries and update in-memory column names used for subsequent statements.
- if type/length: emit ALTER TYPE/MODIFY/ALTER COLUMN queries.
- continue with existing nullability/default/PK/unique/etc handling.
3) Ensure downQueries reverse the order (e.g., type back, then rename back).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
✅ 4. Stale schema cache after ALTER 🐞 Bug ⛯ Reliability
Description
The new ALTER TYPE/MODIFY branches don’t update clonedTable/oldColumn, but changeColumn still
replaces the cached table with clonedTable. This leaves the QueryRunner’s cached schema stale after
a type/length change, which can corrupt subsequent diffing and SQL generation within the same schema
sync/migration generation.
Code

src/driver/mysql/MysqlQueryRunner.ts[R1111-1126]

+        } else if (
+            oldColumn.type !== newColumn.type ||
+            oldColumn.length !== newColumn.length
+        ) {
+            // Use MODIFY COLUMN instead of DROP+ADD to preserve existing data.
+            upQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(newColumn, true)}`,
+                ),
+            )
+            downQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} MODIFY ${this.buildCreateColumnSql(oldColumn, true)}`,
+                ),
+            )
   } else {
Evidence
In multiple drivers, the ALTER/MODIFY branch only pushes SQL queries and does not mutate
clonedTable to reflect the new type/length. Yet at the end of changeColumn, the cached table is
replaced with clonedTable, effectively overwriting the cache with stale column definitions.

src/driver/mysql/MysqlQueryRunner.ts[1111-1126]
src/driver/mysql/MysqlQueryRunner.ts[1612-1614]
src/driver/cockroachdb/CockroachQueryRunner.ts[2151-2153]
src/driver/postgres/PostgresQueryRunner.ts[2441-2443]
src/driver/sqlserver/SqlServerQueryRunner.ts[1956-1958]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
After emitting ALTER TYPE/MODIFY statements, `clonedTable` is not updated, but it is still used to replace the cached table. This leaves the cached schema out of sync with the SQL that was generated/executed.
### Issue Context
`changeColumn` uses `clonedTable` as the new authoritative cached schema at the end of the method.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1295-1318]
- src/driver/postgres/PostgresQueryRunner.ts[2441-2443]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1374-1396]
- src/driver/cockroachdb/CockroachQueryRunner.ts[2151-2153]
- src/driver/mysql/MysqlQueryRunner.ts[1111-1126]
- src/driver/mysql/MysqlQueryRunner.ts[1612-1614]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1316-1347]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1956-1958]
### Notes
Minimum fix: locate the column in `clonedTable.columns` and update type/length/precision/scale/isArray/etc. to match `newColumn` when the ALTER/MODIFY branch is taken.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 5. Enum conversions may emit invalid TYPE 🐞 Bug ✓ Correctness
Description
Postgres/Cockroach ALTER TYPE branches use driver.createFullType(), but enums are represented with
TableColumn.type = "enum" and the real DB type name is derived via buildEnumName()/enumName. If a
column changes between enum and non-enum types, the new branch can emit TYPE enum (and invalid down
SQL) and it also bypasses enum type lifecycle handling (create/drop/rename) used elsewhere.
Code

src/driver/cockroachdb/CockroachQueryRunner.ts[R1374-1396]

+        } else if (
+            oldColumn.type !== newColumn.type ||
+            oldColumn.length !== newColumn.length
+        ) {
+            // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
+            const newFullType = this.driver.createFullType(newColumn)
+            const oldFullType = this.driver.createFullType(oldColumn)
+
+            upQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        newColumn.name
+                    }" TYPE ${newFullType}`,
+                ),
+            )
+            downQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        oldColumn.name
+                    }" TYPE ${oldFullType}`,
+                ),
+            )
   } else {
Evidence
In Postgres table introspection, enum columns are explicitly mapped to tableColumn.type = "enum"
with enumName stored separately. The SQL builder uses buildEnumName() for enums, not
createFullType(). Therefore, using createFullType() in the new ALTER TYPE branches can produce TYPE
enum rather than the actual enum type identifier. The codebase also has dedicated enum-change logic,
which is skipped when the new else-if triggers.

src/driver/postgres/PostgresQueryRunner.ts[3819-3825]
src/driver/postgres/PostgresDriver.ts[1246-1252]
src/driver/postgres/PostgresQueryRunner.ts[5022-5027]
src/driver/postgres/PostgresQueryRunner.ts[1295-1317]
src/driver/cockroachdb/CockroachQueryRunner.ts[4367-4372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Enum columns require special type naming (via `buildEnumName`/`enumName`) and often enum type lifecycle operations. The new type/length ALTER branch uses `createFullType`, which will produce `enum` for enum columns and bypasses existing enum-specific handling.
### Issue Context
Postgres introspection sets `TableColumn.type = &amp;amp;amp;amp;amp;quot;enum&amp;amp;amp;amp;amp;quot;` and stores `enumName` separately, and SQL generation uses `buildEnumName()` for enum types.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1295-1318]
- src/driver/postgres/PostgresQueryRunner.ts[3819-3835]
- src/driver/postgres/PostgresQueryRunner.ts[4997-5027]
- src/driver/cockroachdb/CockroachQueryRunner.ts[1374-1396]
- src/driver/cockroachdb/CockroachQueryRunner.ts[4352-4372]
### Notes
Two viable paths:
- Conservative: fall back to DROP+ADD when converting to/from enum.
- Full support: ensure enum type exists (create), then ALTER COLUMN TYPE to the correct enum identifier (using `buildEnumName`), and drop old enum types when appropriate; ensure down migration mirrors this.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Missing #3357 test reference 📘 Rule violation ✧ Quality ⭐ New
Description
The new functional test does not include an issue reference comment for the fix, as required when
applicable. This makes it harder to trace the test back to the reported issue and maintain
regression coverage intent.
Code

test/functional/schema-builder/alter-column-type.test.ts[R10-31]

+describe("schema builder > alter column type", () => {
+    let connections: DataSource[]
+
+    before(
+        async () =>
+            (connections = await createTestingConnections({
+                enabledDrivers: [
+                    "postgres",
+                    "cockroachdb",
+                    "mysql",
+                    "mariadb",
+                    "mssql",
+                ],
+                entities: [Album],
+                schemaCreate: true,
+                dropSchema: true,
+            })),
+    )
+    after(() => closeTestingConnections(connections))
+
+    it("should generate ALTER COLUMN instead of DROP+ADD when column length changes", async () => {
+        await Promise.all(
Evidence
Compliance ID 3 requires issue fixes to live in the functional test suite and include an issue
reference comment when applicable. The added functional test file contains no reference to #3357
in its describe/test comments.

Rule 3: Prefer functional tests over per-issue tests
test/functional/schema-builder/alter-column-type.test.ts[10-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new functional regression test does not include an issue reference comment for issue `#3357`, which is expected for issue-driven fixes when applicable.

## Issue Context
The PR description states this change fixes `#3357`. Compliance requires functional tests to include an issue reference comment when applicable.

## Fix Focus Areas
- test/functional/schema-builder/alter-column-type.test.ts[10-31]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Missing rename+type tests 🐞 Bug ⛯ Reliability ⭐ New
Description
The added functional test suite validates type/length changes avoid DROP+ADD, but it does not cover
the critical combined scenario of renaming a column while also changing its type/length—exactly
where the new logic is most likely to regress.
Code

test/functional/schema-builder/alter-column-type.test.ts[R30-139]

+    it("should generate ALTER COLUMN instead of DROP+ADD when column length changes", async () => {
+        await Promise.all(
+            connections.map(async (connection) => {
+                const meta = connection.getMetadata(Album)
+                const col = meta.columns.find(
+                    (c) => c.propertyName === "title",
+                )!
+
+                // Change column length from 50 to 100
+                col.length = "100"
+
+                // Generate migration queries
+                const sqlInMemory = await connection.driver
+                    .createSchemaBuilder()
+                    .log()
+
+                const upQueries = sqlInMemory.upQueries.map((q) =>
+                    q.query.replace(/\s+/g, " ").trim(),
+                )
+                const downQueries = sqlInMemory.downQueries.map((q) =>
+                    q.query.replace(/\s+/g, " ").trim(),
+                )
+
+                // Should NOT contain DROP COLUMN
+                const hasDropColumn = upQueries.some(
+                    (q) => q.includes("DROP COLUMN") && q.includes("title"),
+                )
+                expect(
+                    hasDropColumn,
+                    "Migration should NOT use DROP COLUMN for length changes",
+                ).to.be.false
+
+                // Should contain ALTER COLUMN or MODIFY
+                const hasAlterColumn = upQueries.some(
+                    (q) =>
+                        (q.includes("ALTER COLUMN") || q.includes("MODIFY")) &&
+                        q.includes("title"),
+                )
+                expect(
+                    hasAlterColumn,
+                    "Migration should use ALTER COLUMN / MODIFY for length changes",
+                ).to.be.true
+
+                // Down queries should also not contain DROP COLUMN
+                const downHasDropColumn = downQueries.some(
+                    (q) => q.includes("DROP COLUMN") && q.includes("title"),
+                )
+                expect(
+                    downHasDropColumn,
+                    "Down migration should NOT use DROP COLUMN for length changes",
+                ).to.be.false
+
+                // Restore original length
+                col.length = "50"
+            }),
+        )
+    })
+
+    it("should generate ALTER COLUMN instead of DROP+ADD when column type changes", async () => {
+        await Promise.all(
+            connections.map(async (connection) => {
+                const meta = connection.getMetadata(Album)
+                const col = meta.columns.find(
+                    (c) => c.propertyName === "description",
+                )!
+
+                const originalType = col.type
+                const originalLength = col.length
+
+                // Change column type from varchar to text
+                col.type = "text"
+                col.length = ""
+
+                // Generate migration queries
+                const sqlInMemory = await connection.driver
+                    .createSchemaBuilder()
+                    .log()
+
+                const upQueries = sqlInMemory.upQueries.map((q) =>
+                    q.query.replace(/\s+/g, " ").trim(),
+                )
+
+                // Should NOT contain DROP COLUMN
+                const hasDropColumn = upQueries.some(
+                    (q) =>
+                        q.includes("DROP COLUMN") && q.includes("description"),
+                )
+                expect(
+                    hasDropColumn,
+                    "Migration should NOT use DROP COLUMN for type changes",
+                ).to.be.false
+
+                // Should contain ALTER COLUMN or MODIFY
+                const hasAlterColumn = upQueries.some(
+                    (q) =>
+                        (q.includes("ALTER COLUMN") || q.includes("MODIFY")) &&
+                        q.includes("description"),
+                )
+                expect(
+                    hasAlterColumn,
+                    "Migration should use ALTER COLUMN / MODIFY for type changes",
+                ).to.be.true
+
+                // Restore original
+                col.type = originalType
+                col.length = originalLength
+            }),
+        )
+    })
+})
Evidence
The new tests only mutate length and type for existing columns and assert on generated SQL; no
test changes column name concurrently with type/length.

test/functional/schema-builder/alter-column-type.test.ts[30-139]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
No test covers rename+type/length changes in the same schema diff, which is a high-risk interaction for the new logic.

### Issue Context
Drivers now emit ALTER/MODIFY statements for type/length changes and separate rename statements; ordering/name targeting needs coverage.

### Fix Focus Areas
- test/functional/schema-builder/alter-column-type.test.ts[1-139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. No docs for migration change 📘 Rule violation ✓ Correctness
Description
This PR changes user-visible migration SQL generation behavior (ALTER vs DROP+ADD) but does not
include any documentation updates. Users relying on documented migration behavior may be surprised
by the new generated SQL across drivers.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1295-1317]

+        } else if (
+            oldColumn.type !== newColumn.type ||
+            oldColumn.length !== newColumn.length
+        ) {
+            // Use ALTER COLUMN TYPE instead of DROP+ADD to preserve existing data.
+            // PostgreSQL supports ALTER COLUMN TYPE with USING for type conversions.
+            const newFullType = this.driver.createFullType(newColumn)
+            const oldFullType = this.driver.createFullType(oldColumn)
+
+            upQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        newColumn.name
+                    }" TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
+                ),
+            )
+            downQueries.push(
+                new Query(
+                    `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+                        oldColumn.name
+                    }" TYPE ${oldFullType} USING "${oldColumn.name}"::${oldFullType}`,
+                ),
+            )
Evidence
Compliance requires docs updates for user-facing changes. The PR explicitly introduces new ALTER
COLUMN generation logic, while the PR description indicates no docs changes were made.

Rule 2: Docs updated for user-facing changes
src/driver/postgres/PostgresQueryRunner.ts[1295-1317]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces a user-visible change in generated migration SQL (ALTER instead of DROP+ADD), but does not update docs.
## Issue Context
Compliance requires documentation updates for user-facing behavior changes.
## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1295-1317]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
✅ 9. Bulk @param comment noise 📘 Rule violation ✓ Correctness
Description
The PR adds many repetitive @param tags across query runner files without changing the method
signatures. This introduces style/comment churn that may be considered AI-generated noise per the
checklist.
Code

src/driver/mysql/MysqlQueryRunner.ts[R181-186]

/**
* Executes a raw SQL query.
+     * @param query
+     * @param parameters
+     * @param useStructuredResult
Evidence
The compliance rule forbids introducing AI-like comment noise and style churn. This PR adds new
JSDoc @param lines (representative example shown) across multiple methods/files without
accompanying behavioral need.

Rule 4: Remove AI-generated noise
src/driver/mysql/MysqlQueryRunner.ts[181-186]
src/driver/cockroachdb/CockroachQueryRunner.ts[267-272]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A large number of repetitive `@param` tags were added without functional changes, increasing noise and churn.
## Issue Context
The checklist requires removing AI-generated/low-value comment noise and keeping style consistent.
## Fix Focus Areas
- src/driver/mysql/MysqlQueryRunner.ts[181-186]
- src/driver/cockroachdb/CockroachQueryRunner.ts[267-272]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Aurora MySQL not covered 🐞 Bug ✓ Correctness
Description
AuroraMysqlQueryRunner still uses DROP+ADD for type/length changes, so Aurora MySQL users can still
lose data. The new test doesn’t enable the aurora-mysql driver, so the gap isn’t detected.
Code

test/github-issues/3357/issue-3357.test.ts[R15-23]

+            (connections = await createTestingConnections({
+                enabledDrivers: [
+                    "postgres",
+                    "cockroachdb",
+                    "mysql",
+                    "mariadb",
+                    "mssql",
+                ],
+                entities: [__dirname + "/entity/*{.js,.ts}"],
Evidence
The aurora-mysql QueryRunner’s changeColumn still treats type/length changes as requiring
dropColumn/addColumn. The new test suite enables mysql/mariadb but not aurora-mysql, so this
behavior remains untested and unfixed despite being MySQL-compatible.

src/driver/aurora-mysql/AuroraMysqlQueryRunner.ts[839-850]
test/github-issues/3357/issue-3357.test.ts[15-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
AuroraMysqlQueryRunner still drops and re-adds columns on type/length changes, so the data-loss bug persists for Aurora MySQL users.
### Issue Context
Aurora MySQL is MySQL-compatible and has its own QueryRunner implementation.
### Fix Focus Areas
- src/driver/aurora-mysql/AuroraMysqlQueryRunner.ts[839-851]
- test/github-issues/3357/issue-3357.test.ts[15-23]
### Notes
Mirror the MysqlQueryRunner fix:
- Use `ALTER TABLE ... MODIFY ...` for type/length changes.
- Ensure clonedTable cache updates correctly.
- Add `&amp;amp;amp;amp;amp;quot;aurora-mysql&amp;amp;amp;amp;amp;quot;` to test enabledDrivers (if supported by the test harness).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migration generation drops and creates columns instead of altering resulting in data loss

1 participant