Skip to content

fix: use ALTER COLUMN instead of drop/add for type/length changes to prevent data loss#12032

Open
hruss-software wants to merge 4 commits intotypeorm:masterfrom
hruss-software:fix/alter-column-preserve-data
Open

fix: use ALTER COLUMN instead of drop/add for type/length changes to prevent data loss#12032
hruss-software wants to merge 4 commits intotypeorm:masterfrom
hruss-software:fix/alter-column-preserve-data

Conversation

@hruss-software
Copy link

Problem

When changing a column's type or length (e.g., varchar(50)varchar(100)), TypeORM generates
destructive DROP COLUMN + ADD COLUMN statements instead of ALTER COLUMN. This causes
complete data loss for the affected column.

Reported in #3357 (97 👍, open since 2019).

Solution

Move type/length changes from the destructive DROP+ADD code path to each database's native ALTER
COLUMN syntax, which preserves existing data:

Driver ALTER Syntax Used
PostgreSQL ALTER COLUMN "col" TYPE <type> USING "col"::<type>
MySQL/MariaDB ALTER TABLE CHANGE (already existed in codebase)
SQL Server ALTER TABLE ALTER COLUMN (already existed in codebase)
Oracle ALTER TABLE MODIFY (already existed in codebase)
CockroachDB ALTER COLUMN "col" TYPE <type>

DROP+ADD is preserved only for genuinely destructive changes:

  • Array type conversion (isArray changes)
  • Generated/identity column changes
  • Computed column expression changes

Why this approach

  • Minimal diff (102 lines) - no new abstractions, no helper functions
  • Leverages existing code - MySQL, SQL Server, and Oracle already had ALTER paths for property
    changes; this routes type/length through them
  • All 5 database drivers - not just PostgreSQL
  • PostgreSQL USING clause - enables safe cross-type casts (e.g., inttext)
  • Incompatible conversions fail explicitly - the database will throw a clear error rather than
    silently dropping data

Test

Added test case verifying:

  1. Insert data into a column
  2. Change column length via changeColumn
  3. Verify data is preserved after the change
  4. Revert via executeMemoryDownSql
  5. Verify data is still intact after revert

Closes #3357

Description of change

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • This pull request links relevant issues as Fixes #00000
  • There are new or updated tests validating the change (tests/**.test.ts)
  • Documentation has been updated to reflect this change (docs/docs/**.md)

…prevent data loss

Previously, changing a column's type or length caused TypeORM to generate
DROP COLUMN followed by ADD COLUMN statements, resulting in data loss.
This change uses each database's native ALTER COLUMN syntax instead:

- PostgreSQL: ALTER COLUMN TYPE ... USING
- MySQL/MariaDB: ALTER TABLE CHANGE
- SQL Server: ALTER TABLE ALTER COLUMN
- Oracle: ALTER TABLE MODIFY
- CockroachDB: ALTER COLUMN TYPE

DROP+ADD is preserved only for genuinely destructive changes (array
conversion, generated/identity column changes).

Closes typeorm#3357
@qodo-free-for-open-source-projects

User description

Problem

When changing a column's type or length (e.g., varchar(50)varchar(100)), TypeORM generates
destructive DROP COLUMN + ADD COLUMN statements instead of ALTER COLUMN. This causes
complete data loss for the affected column.

Reported in #3357 (97 👍, open since 2019).

Solution

Move type/length changes from the destructive DROP+ADD code path to each database's native ALTER
COLUMN syntax, which preserves existing data:

Driver ALTER Syntax Used
PostgreSQL ALTER COLUMN "col" TYPE <type> USING "col"::<type>
MySQL/MariaDB ALTER TABLE CHANGE (already existed in codebase)
SQL Server ALTER TABLE ALTER COLUMN (already existed in codebase)
Oracle ALTER TABLE MODIFY (already existed in codebase)
CockroachDB ALTER COLUMN "col" TYPE <type>

DROP+ADD is preserved only for genuinely destructive changes:

  • Array type conversion (isArray changes)
  • Generated/identity column changes
  • Computed column expression changes

Why this approach

  • Minimal diff (102 lines) - no new abstractions, no helper functions
  • Leverages existing code - MySQL, SQL Server, and Oracle already had ALTER paths for property
    changes; this routes type/length through them
  • All 5 database drivers - not just PostgreSQL
  • PostgreSQL USING clause - enables safe cross-type casts (e.g., inttext)
  • Incompatible conversions fail explicitly - the database will throw a clear error rather than
    silently dropping data

Test

Added test case verifying:

  1. Insert data into a column
  2. Change column length via changeColumn
  3. Verify data is preserved after the change
  4. Revert via executeMemoryDownSql
  5. Verify data is still intact after revert

Closes #3357

Description of change

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • This pull request links relevant issues as Fixes #00000
  • There are new or updated tests validating the change (tests/**.test.ts)
  • Documentation has been updated to reflect this change (docs/docs/**.md)

PR Type

Bug fix


Description

  • Use ALTER COLUMN instead of DROP+ADD for type/length changes across all database drivers

  • Preserves existing data when modifying column types or lengths

  • Routes type/length changes through native ALTER syntax for PostgreSQL, MySQL, SQL Server, Oracle, CockroachDB

  • Reserves DROP+ADD only for genuinely destructive changes (array conversion, generated/identity/computed columns)

  • Adds comprehensive test verifying data preservation during column modifications


Diagram Walkthrough

flowchart LR
  A["Column Type/Length Change"] --> B{Change Type}
  B -->|Type or Length| C["Use ALTER COLUMN"]
  B -->|Array/Generated/Computed| D["Use DROP+ADD"]
  C --> E["PostgreSQL: ALTER COLUMN TYPE USING"]
  C --> F["MySQL: ALTER TABLE CHANGE"]
  C --> G["SQL Server: ALTER TABLE ALTER COLUMN"]
  C --> H["Oracle: ALTER TABLE MODIFY"]
  C --> I["CockroachDB: ALTER COLUMN TYPE"]
  E --> J["Data Preserved"]
  F --> J
  G --> J
  H --> J
  I --> J
  D --> K["Column Recreated"]
Loading

File Walkthrough

Relevant files
Bug fix
PostgresQueryRunner.ts
Add USING clause to PostgreSQL ALTER COLUMN TYPE                 

src/driver/postgres/PostgresQueryRunner.ts

  • Removed type/length checks from destructive DROP+ADD condition
  • Added type/length checks to ALTER COLUMN TYPE condition
  • Enhanced ALTER COLUMN TYPE with USING clause for safe type casting
  • Added variables to store full type definitions for both up and down
    migrations
+14/-5   
MysqlQueryRunner.ts
Route MySQL type/length changes to ALTER TABLE CHANGE       

src/driver/mysql/MysqlQueryRunner.ts

  • Removed type/length checks from destructive DROP+ADD condition
  • Added type/length checks to ALTER TABLE CHANGE condition
  • Preserved existing ALTER TABLE CHANGE logic for type/length
    modifications
+6/-3     
SqlServerQueryRunner.ts
Route SQL Server type/length changes to ALTER COLUMN         

src/driver/sqlserver/SqlServerQueryRunner.ts

  • Removed type/length checks from destructive DROP+ADD condition
  • Added type/length checks to ALTER TABLE ALTER COLUMN condition
  • Updated comment to clarify DROP+ADD is only for generated/computed
    columns
+3/-3     
OracleQueryRunner.ts
Route Oracle type/length changes to ALTER TABLE MODIFY     

src/driver/oracle/OracleQueryRunner.ts

  • Removed type/length checks from destructive DROP+ADD condition
  • Added type/length checks to ALTER TABLE MODIFY condition
  • Updated comment to clarify DROP+ADD is only for generated/computed
    columns
+6/-4     
CockroachQueryRunner.ts
Route CockroachDB type/length changes to ALTER COLUMN TYPE

src/driver/cockroachdb/CockroachQueryRunner.ts

  • Removed type/length checks from destructive DROP+ADD condition
  • Added type/length checks to ALTER COLUMN TYPE condition
  • Updated comment to clarify DROP+ADD is only for genuinely destructive
    changes
+5/-3     
Tests
change-column.test.ts
Add test for column change data preservation                         

test/functional/query-runner/change-column.test.ts

  • Added comprehensive test case for data preservation during column
    type/length changes
  • Test inserts data, modifies column length, verifies data is preserved
  • Test reverts changes and confirms data remains intact after revert
  • Skips SQLite, CockroachDB, and Spanner due to their specific
    constraints
+68/-0   

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

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

PR Code Suggestions ✨

Latest suggestions up to c46a968

CategorySuggestion                                                                                                                                    Impact
Possible issue
Always release the query runner

Wrap the test logic in a try/finally block to ensure the queryRunner is always
released, preventing resource leaks and potential cascading test failures.

test/functional/query-runner/change-column.test.ts [288-353]

 const queryRunner = connection.createQueryRunner()
 const postRepository = connection.getRepository(Post)
 
-// Insert test data before modifying the column
-await postRepository.insert({
-    id: 1,
-    version: 1,
-    name: "Test Post",
-    text: "Some text",
-    tag: "tag1",
-})
+try {
+    // Insert test data before modifying the column
+    await postRepository.insert({
+        id: 1,
+        version: 1,
+        name: "Test Post",
+        text: "Some text",
+        tag: "tag1",
+    })
 
-// --- Test length change ---
-let table = await queryRunner.getTable("post")
-const nameColumn = table!.findColumnByName("name")!
+    // --- Test length change ---
+    let table = await queryRunner.getTable("post")
+    const nameColumn = table!.findColumnByName("name")!
 
-// Change column length (e.g., varchar(255) -> varchar(500))
-const changedNameColumn = nameColumn.clone()
-changedNameColumn.length = "500"
-await queryRunner.changeColumn(
-    table!,
-    nameColumn,
-    changedNameColumn,
-)
+    const changedNameColumn = nameColumn.clone()
+    changedNameColumn.length = "500"
+    await queryRunner.changeColumn(table!, nameColumn, changedNameColumn)
 
-// Verify data is preserved after length change
-let post = await postRepository.findOneBy({ id: 1 })
-expect(post).to.not.be.null
-expect(post!.name).to.equal("Test Post")
+    let post = await postRepository.findOneBy({ id: 1 })
+    expect(post).to.not.be.null
+    expect(post!.name).to.equal("Test Post")
 
-// Verify column length was actually changed
-table = await queryRunner.getTable("post")
-table!.findColumnByName("name")!.length!.should.be.equal("500")
+    table = await queryRunner.getTable("post")
+    table!.findColumnByName("name")!.length!.should.be.equal("500")
 
-// Revert length change and verify data is still intact
-await queryRunner.executeMemoryDownSql()
-queryRunner.clearSqlMemory()
+    await queryRunner.executeMemoryDownSql()
+    queryRunner.clearSqlMemory()
 
-post = await postRepository.findOneBy({ id: 1 })
-expect(post).to.not.be.null
-expect(post!.name).to.equal("Test Post")
+    post = await postRepository.findOneBy({ id: 1 })
+    expect(post).to.not.be.null
+    expect(post!.name).to.equal("Test Post")
 
-// --- Test type change (e.g., varchar -> text) ---
-table = await queryRunner.getTable("post")
-const nameCol = table!.findColumnByName("name")!
+    // --- Test type change (e.g., varchar -> text) ---
+    table = await queryRunner.getTable("post")
+    const nameCol = table!.findColumnByName("name")!
 
-const typeChangedCol = nameCol.clone()
-typeChangedCol.type = "text"
-typeChangedCol.length = ""
-await queryRunner.changeColumn(table!, nameCol, typeChangedCol)
+    const typeChangedCol = nameCol.clone()
+    typeChangedCol.type = "text"
+    typeChangedCol.length = ""
+    await queryRunner.changeColumn(table!, nameCol, typeChangedCol)
 
-// Verify data is preserved after type change
-post = await postRepository.findOneBy({ id: 1 })
-expect(post).to.not.be.null
-expect(post!.name).to.equal("Test Post")
+    post = await postRepository.findOneBy({ id: 1 })
+    expect(post).to.not.be.null
+    expect(post!.name).to.equal("Test Post")
 
-// Revert type change and verify data is still intact
-await queryRunner.executeMemoryDownSql()
+    await queryRunner.executeMemoryDownSql()
+    queryRunner.clearSqlMemory()
 
-post = await postRepository.findOneBy({ id: 1 })
-expect(post).to.not.be.null
-expect(post!.name).to.equal("Test Post")
+    post = await postRepository.findOneBy({ id: 1 })
+    expect(post).to.not.be.null
+    expect(post!.name).to.equal("Test Post")
 
-// Clean up test data
-await postRepository.delete({ id: 1 })
-await queryRunner.release()
+    await postRepository.delete({ id: 1 })
+} finally {
+    await queryRunner.release()
+}
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a resource leak risk in the test and proposes using a try/finally block to ensure the queryRunner is always released, which is crucial for test suite stability.

Medium
Use consistent column name references

To improve robustness, use a single variable for the column name in all ALTER
COLUMN statements instead of mixing oldColumn.name and newColumn.name.

src/driver/postgres/PostgresQueryRunner.ts [1578-1646]

 const newFullType = this.driver.createFullType(newColumn)
 const oldFullType = this.driver.createFullType(oldColumn)
+const columnName = newColumn.name
 
 // if column has a default, drop it before ALTER TYPE to avoid
 // casting failures (same pattern used for enum type changes)
-if (
-    oldColumn.default !== null &&
-    oldColumn.default !== undefined
-) {
+if (oldColumn.default !== null && oldColumn.default !== undefined) {
     defaultValueChanged = true
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(
                 table,
-            )} ALTER COLUMN "${oldColumn.name}" DROP DEFAULT`,
+            )} ALTER COLUMN "${columnName}" DROP DEFAULT`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(
                 table,
-            )} ALTER COLUMN "${
-                oldColumn.name
-            }" SET DEFAULT ${oldColumn.default}`,
+            )} ALTER COLUMN "${columnName}" SET DEFAULT ${oldColumn.default}`,
         ),
     )
 }
 
 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 "${columnName}" TYPE ${newFullType} USING "${columnName}"::${newFullType}`,
     ),
 )
 downQueries.push(
     new Query(
-        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-            newColumn.name
-        }" TYPE ${oldFullType} USING "${
-            newColumn.name
-        }"::${oldFullType}`,
+        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${columnName}" TYPE ${oldFullType} USING "${columnName}"::${oldFullType}`,
     ),
 )
 
 // restore default after type change
-if (
-    newColumn.default !== null &&
-    newColumn.default !== undefined
-) {
+if (newColumn.default !== null && newColumn.default !== undefined) {
     upQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(
                 table,
-            )} ALTER COLUMN "${
-                newColumn.name
-            }" SET DEFAULT ${newColumn.default}`,
+            )} ALTER COLUMN "${columnName}" SET DEFAULT ${newColumn.default}`,
         ),
     )
     downQueries.push(
         new Query(
             `ALTER TABLE ${this.escapePath(
                 table,
-            )} ALTER COLUMN "${newColumn.name}" DROP DEFAULT`,
+            )} ALTER COLUMN "${columnName}" DROP DEFAULT`,
         ),
     )
 }
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies inconsistent use of oldColumn.name and newColumn.name, and proposing to use a single variable improves code readability and robustness against future refactoring.

Low
  • More

Previous suggestions

✅ Suggestions up to commit 5417de9
CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle defaults during type changes
Suggestion Impact:The commit adds queries to DROP DEFAULT before ALTER COLUMN TYPE and to SET DEFAULT after the type change, addressing the suggested migration failure scenario (implemented more broadly than only when the default is unchanged).

code diff:

+
+                // if column has a default, drop it before ALTER TYPE to avoid
+                // casting failures (same pattern used for enum type changes)
+                if (
+                    oldColumn.default !== null &&
+                    oldColumn.default !== undefined
+                ) {
+                    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}`,
+                        ),
+                    )
+                }
+
                 upQueries.push(
                     new Query(
                         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
@@ -1595,6 +1621,29 @@
                         }"::${oldFullType}`,
                     ),
                 )
+
+                // restore default after type change
+                if (
+                    newColumn.default !== null &&
+                    newColumn.default !== undefined
+                ) {
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ALTER COLUMN "${
+                                newColumn.name
+                            }" SET DEFAULT ${newColumn.default}`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ALTER COLUMN "${newColumn.name}" DROP DEFAULT`,
+                        ),
+                    )
+                }

In PostgresQueryRunner.ts, when changing a column's type, drop its default value
before the ALTER statement and re-add it after to prevent errors if the default
value is unchanged.

src/driver/postgres/PostgresQueryRunner.ts [1575-1597]

 // Use ALTER COLUMN TYPE to change type/length/precision/scale
 // without dropping and recreating the column, which preserves data.
 // The USING clause enables PostgreSQL to cast existing data to the new type.
 const newFullType = this.driver.createFullType(newColumn)
 const oldFullType = this.driver.createFullType(oldColumn)
+
+const isDefaultUnchanged =
+    oldColumn.default !== undefined &&
+    newColumn.default !== undefined &&
+    oldColumn.default === newColumn.default
+
+if (isDefaultUnchanged) {
+    upQueries.push(
+        new Query(
+            `ALTER TABLE ${this.escapePath(
+                table,
+            )} ALTER COLUMN "${newColumn.name}" DROP DEFAULT`,
+        ),
+    )
+    downQueries.push(
+        new Query(
+            `ALTER TABLE ${this.escapePath(
+                table,
+            )} ALTER COLUMN "${newColumn.name}" DROP DEFAULT`,
+        ),
+    )
+}
+
 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 "${
             newColumn.name
         }" TYPE ${oldFullType} USING "${
             newColumn.name
         }"::${oldFullType}`,
     ),
 )
 
+if (isDefaultUnchanged) {
+    upQueries.push(
+        new Query(
+            `ALTER TABLE ${this.escapePath(
+                table,
+            )} ALTER COLUMN "${newColumn.name}" SET DEFAULT ${
+                newColumn.default
+            }`,
+        ),
+    )
+    downQueries.push(
+        new Query(
+            `ALTER TABLE ${this.escapePath(
+                table,
+            )} ALTER COLUMN "${newColumn.name}" SET DEFAULT ${
+                oldColumn.default
+            }`,
+        ),
+    )
+}
+
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a potential runtime error in PostgreSQL when altering a column type that has a default value, which could cause migrations to fail.

Medium
General
Make cleanup conditional on revert

In the change-column.test.ts test, use the reverted flag to conditionally
execute executeMemoryDownSql() in the finally block, preventing it from running
twice.

test/functional/query-runner/change-column.test.ts [332-341]

 } finally {
     // Best-effort cleanup regardless of test outcome
-    await queryRunner
-        .executeMemoryDownSql()
-        .catch(() => undefined)
-    await postRepository
-        .delete({ id: 1 })
-        .catch(() => undefined)
+    if (!reverted) {
+        await queryRunner.executeMemoryDownSql().catch(() => undefined)
+    }
+    await postRepository.delete({ id: 1 }).catch(() => undefined)
     await queryRunner.release()
 }
Suggestion importance[1-10]: 6

__

Why: This suggestion correctly prevents executeMemoryDownSql() from being called twice, which improves test stability and avoids potential side effects from multiple reverts.

Low
Suggestions up to commit ffab028
CategorySuggestion                                                                                                                                    Impact
Possible issue
Add explicit casts for type changes

In CockroachQueryRunner.ts, add a conditional USING clause with an explicit cast
to the ALTER COLUMN query to ensure data is correctly converted when a column's
type changes.

src/driver/cockroachdb/CockroachQueryRunner.ts [1622-1627]

 // Use ALTER COLUMN TYPE to change type/length/precision/scale
 // without dropping and recreating the column, which preserves data.
+const newFullType = this.driver.createFullType(newColumn)
+const oldFullType = this.driver.createFullType(oldColumn)
+const usingNew =
+    oldColumn.type !== newColumn.type
+        ? ` USING "${newColumn.name}"::${newFullType}`
+        : ""
+const usingOld =
+    oldColumn.type !== newColumn.type
+        ? ` USING "${newColumn.name}"::${oldFullType}`
+        : ""
+
 upQueries.push(
     new Query(
         `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
             newColumn.name
+        }" TYPE ${newFullType}${usingNew}`,
+    ),
+)
+downQueries.push(
+    new Query(
+        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
+            newColumn.name
+        }" TYPE ${oldFullType}${usingOld}`,
+    ),
+)
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that CockroachDB, being PostgreSQL-compatible, likely requires an explicit USING clause for ALTER COLUMN TYPE on certain type changes, which the PR's current implementation lacks, thus preventing potential migration failures.

Medium
Ensure key driver is tested

Remove cockroachdb from the skip condition in the new change-column.test.ts test
to ensure the PR's changes for that driver are properly validated.

test/functional/query-runner/change-column.test.ts [279-284]

-// CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
-if (
-    connection.driver.options.type === "cockroachdb" ||
-    connection.driver.options.type === "spanner"
-)
-    return
+// Spanner may have restrictions on ALTER COLUMN TYPE
+if (connection.driver.options.type === "spanner") return
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out a significant testing gap, as the new test case for data preservation explicitly skips cockroachdb, a driver that was modified by this PR, leaving the new logic for it untested.

Medium
General
Make casts robust across types

In PostgresQueryRunner.ts, wrap the type in the USING clause's cast with
parentheses (e.g., ::(${newFullType})) to prevent syntax errors with complex
data types.

src/driver/postgres/PostgresQueryRunner.ts [1578-1588]

 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}`,
+        }"::(${newFullType})`,
     ),
 )
Suggestion importance[1-10]: 7

__

Why: The suggestion improves the robustness of the new PostgreSQL casting logic by wrapping the type in parentheses, which prevents syntax errors with complex or multi-word types, making migrations more reliable.

Medium
Suggestions up to commit beb14e0
CategorySuggestion                                                                                                                                    Impact
Possible issue
Add USING clause for safe casting

Add a USING clause to the ALTER COLUMN statement for CockroachDB to ensure safe
data casting when changing a column's type. This prevents potential migration
failures.

src/driver/cockroachdb/CockroachQueryRunner.ts [1624-1637]

+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
-        }" SET DATA TYPE ${this.driver.createFullType(
-            newColumn,
-        )}`,
+        `ALTER TABLE ${this.escapePath(
+            table,
+        )} ALTER COLUMN "${newColumn.name}" SET DATA TYPE ${newFullType} USING "${newColumn.name}"::${newFullType}`,
     ),
 )
 downQueries.push(
     new Query(
-        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
-            newColumn.name
-        }" SET DATA TYPE ${this.driver.createFullType(
-            oldColumn,
-        )}`,
+        `ALTER TABLE ${this.escapePath(
+            table,
+        )} ALTER COLUMN "${newColumn.name}" SET DATA TYPE ${oldFullType} USING "${newColumn.name}"::${oldFullType}`,
     ),
 )
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out that changing a column type in CockroachDB without a USING clause can lead to data conversion errors, and this change is not present in the PR.

Medium
General
Use MODIFY instead of CHANGE

In the MySQL query runner, replace the ALTER TABLE ... CHANGE statement with
ALTER TABLE ... MODIFY. The MODIFY statement is more idiomatic for changing a
column's definition without altering its name.

src/driver/mysql/MysqlQueryRunner.ts [1280-1297]

 if (
     oldColumn.type !== newColumn.type ||
     oldColumn.length !== newColumn.length ||
     this.isColumnChanged(oldColumn, newColumn, true, true)
 ) {
     upQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(table)} CHANGE \`${
-                oldColumn.name
+            `ALTER TABLE ${this.escapePath(table)} MODIFY \`${
+                newColumn.name
             }\` ${this.createColumnDefinition(newColumn)}`,
         ),
     )
     downQueries.push(
         new Query(
-            `ALTER TABLE ${this.escapePath(table)} CHANGE \`${
-                newColumn.name
+            `ALTER TABLE ${this.escapePath(table)} MODIFY \`${
+                oldColumn.name
             }\` ${this.createColumnDefinition(oldColumn)}`,
         ),
     )
 }
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that MODIFY is more idiomatic than CHANGE for altering a column's type without renaming it, which improves code clarity and intent.

Low

@hruss-software hruss-software changed the title fix: use ALTER COLUMN instead of DROP+ADD for type/length changes to prevent data loss fix: use ALTER COLUMN instead of drop/add for type/length changes to prevent data loss Feb 23, 2026
@hruss-software
Copy link
Author

/claim #3357

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

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

Code Review by Qodo

🐞 Bugs (21) 📘 Rule violations (8) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Non-portable raw SQL quotes🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;quot;post&amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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


2. PG type change default risk🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now alters type/length via ALTER COLUMN ... TYPE ... USING, but it does not
drop/recreate an existing DEFAULT, which can make ALTER TYPE fail when the default can’t be cast.
TypeORM already drops DEFAULT for enum type changes to avoid casting issues, so the new generic path
is inconsistent and may introduce migration failures for columns with defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1596]

+                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 ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                  ),
              )
              downQueries.push(
                  new Query(
                      `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                          newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                  ),
Evidence
The new generic type/length/precision/scale change path adds a USING cast but does not touch column
defaults, so any existing DEFAULT remains in place during ALTER TYPE. In the same method, enum type
changes explicitly drop DEFAULT to avoid type-casting issues, and the generic default-update logic
only runs when the default expression changes—meaning type-only changes won’t adjust defaults at
all.

src/driver/postgres/PostgresQueryRunner.ts[1569-1597]
src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
src/driver/postgres/PostgresQueryRunner.ts[2218-2223]

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

## Issue description
`PostgresQueryRunner.changeColumn` now uses `ALTER COLUMN ... TYPE ... USING` for type/length changes, but it leaves existing `DEFAULT` expressions in place. PostgreSQL can fail `ALTER COLUMN TYPE` when a column has a default that cannot be cast to the new type. TypeORM already mitigates this in the enum-type-change flow by dropping the default before the type change.
### Issue Context
- The new generic type/length/precision/scale change block adds a USING cast but does not drop/recreate defaults.
- Default updates later in the method only run when `newColumn.default !== oldColumn.default`, so type-only changes won’t re-apply defaults if we drop them.
- `executeMemoryDownSql()` runs `downQueries.reverse()`, so order of `downQueries.push(...)` matters.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1598]
- src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
- src/driver/postgres/PostgresQueryRunner.ts[2218-2270]
### Implementation notes
- In the generic type-change block, if either old/new column has a default:
- set `defaultValueChanged = true`
- add an `upQueries` step to `DROP DEFAULT` **before** the `ALTER COLUMN ... TYPE ... USING` query
- add an `upQueries` step to `SET DEFAULT &amp;amp;lt;newColumn.default&amp;amp;gt;` (or keep dropped if new default is null/undefined) **after** the type change
- add corresponding `downQueries` that restore the old default (or drop it if old had none)
- Ensure you don’t double-apply defaults via the later default-update section (the `defaultValueChanged` flag should prevent that).

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


3. Non-portable raw SQL quotes🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;amp;quot;post&amp;amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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


View more (8)
4. PG type change default risk🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now alters type/length via ALTER COLUMN ... TYPE ... USING, but it does not
drop/recreate an existing DEFAULT, which can make ALTER TYPE fail when the default can’t be cast.
TypeORM already drops DEFAULT for enum type changes to avoid casting issues, so the new generic path
is inconsistent and may introduce migration failures for columns with defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1596]

+                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 ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                   ),
               )
               downQueries.push(
                   new Query(
                       `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                           newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                   ),
Evidence
The new generic type/length/precision/scale change path adds a USING cast but does not touch column
defaults, so any existing DEFAULT remains in place during ALTER TYPE. In the same method, enum type
changes explicitly drop DEFAULT to avoid type-casting issues, and the generic default-update logic
only runs when the default expression changes—meaning type-only changes won’t adjust defaults at
all.

src/driver/postgres/PostgresQueryRunner.ts[1569-1597]
src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
src/driver/postgres/PostgresQueryRunner.ts[2218-2223]

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

## Issue description
`PostgresQueryRunner.changeColumn` now uses `ALTER COLUMN ... TYPE ... USING` for type/length changes, but it leaves existing `DEFAULT` expressions in place. PostgreSQL can fail `ALTER COLUMN TYPE` when a column has a default that cannot be cast to the new type. TypeORM already mitigates this in the enum-type-change flow by dropping the default before the type change.
### Issue Context
- The new generic type/length/precision/scale change block adds a USING cast but does not drop/recreate defaults.
- Default updates later in the method only run when `newColumn.default !== oldColumn.default`, so type-only changes won’t re-apply defaults if we drop them.
- `executeMemoryDownSql()` runs `downQueries.reverse()`, so order of `downQueries.push(...)` matters.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1598]
- src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
- src/driver/postgres/PostgresQueryRunner.ts[2218-2270]
### Implementation notes
- In the generic type-change block, if either old/new column has a default:
- set `defaultValueChanged = true`
- add an `upQueries` step to `DROP DEFAULT` **before** the `ALTER COLUMN ... TYPE ... USING` query
- add an `upQueries` step to `SET DEFAULT &amp;lt;newColumn.default&amp;gt;` (or keep dropped if new default is null/undefined) **after** the type change
- add corresponding `downQueries` that restore the old default (or drop it if old had none)
- Ensure you don’t double-apply defaults via the later default-update section (the `defaultValueChanged` flag should prevent that).

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


5. Non-portable raw SQL quotes🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;quot;post&amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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


6. Postgres enum ALTER TYPE wrong 🐞 Bug ✓ Correctness
Description
PostgresQueryRunner.changeColumn now builds ALTER COLUMN TYPE statements from
PostgresDriver.createFullType(). For enum/simple-enum columns, createFullType() does not translate
to the underlying user-defined enum type name (it just uses column.type), so changing a column
to/from enum can generate invalid SQL like TYPE enum / ::enum, and non-enum→enum conversions also
won’t create the enum type.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1623]

+                const newFullType = this.driver.createFullType(newColumn)
+                const oldFullType = this.driver.createFullType(oldColumn)
+
+                // if column has a default, drop it before ALTER TYPE to avoid
+                // casting failures (same pattern used for enum type changes)
+                if (
+                    oldColumn.default !== null &&
+                    oldColumn.default !== undefined
+                ) {
+                    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}`,
+                        ),
+                    )
+                }
+
               upQueries.push(
                   new Query(
                       `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                           newColumn.name
-                        }" TYPE ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                   ),
               )
               downQueries.push(
                   new Query(
                       `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                           newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                   ),
               )
Evidence
The new type-change path in changeColumn uses PostgresDriver.createFullType() for both the TYPE
clause and the USING cast. PostgresDriver.createFullType() simply returns the TableColumn.type with
length/precision adjustments and has no enum/simple-enum special-casing, while
PostgresQueryRunner.buildCreateColumnSql explicitly handles enum/simple-enum by using
buildEnumName(table, column). Additionally, enum type creation exists in addColumn, but
changeColumn’s enum-handling block only runs when both old and new are already enum/simple-enum
(enum value/name changes), not for non-enum→enum conversions.

src/driver/postgres/PostgresQueryRunner.ts[1569-1647]
src/driver/postgres/PostgresDriver.ts[1269-1330]
src/driver/postgres/PostgresQueryRunner.ts[5031-5061]
src/driver/postgres/PostgresQueryRunner.ts[1031-1037]
src/driver/postgres/PostgresQueryRunner.ts[1649-1656]

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

## Issue description
`PostgresQueryRunner.changeColumn` now uses `this.driver.createFullType()` for `ALTER COLUMN ... TYPE` and its `USING` cast. For `enum`/`simple-enum`, `PostgresDriver.createFullType()` does **not** return the underlying user-defined enum type name (it uses `column.type`), which can produce invalid SQL (`TYPE enum`, `::enum`). Additionally, non-enum → enum conversions don’t create the enum type.
### Issue Context
Postgres enum columns are emitted via `buildEnumName(...)` in `buildCreateColumnSql`, and enum types are created in `addColumn` via `hasEnumType/createEnumTypeSql`. The new ALTER TYPE path should match that behavior.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1647]
- src/driver/postgres/PostgresQueryRunner.ts[5031-5061]
- src/driver/postgres/PostgresQueryRunner.ts[1020-1037]
- src/driver/postgres/PostgresDriver.ts[1269-1330]
### Expected change (high level)
- Introduce a small local helper inside `changeColumn` to compute `newTypeSql` / `oldTypeSql`:
- if enum/simple-enum: use `this.buildEnumName(table, col)` (+ `[]` if array)
- else: use `this.driver.createFullType(col)
- If converting non-enum → enum/simple-enum:
- ensure enum type exists (`hasEnumType` / `createEnumTypeSql`) before the ALTER TYPE
- consider a safer `USING` expression (possibly `&amp;quot;col&amp;quot;::text::enum_type`) similar to existing enum-change logic.
- Use these computed type SQL strings in both up/down ALTER TYPE queries.

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


7. Non-portable raw SQL quotes🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;amp;quot;post&amp;amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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


8. PG type change default risk 🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now alters type/length via ALTER COLUMN ... TYPE ... USING, but it does not
drop/recreate an existing DEFAULT, which can make ALTER TYPE fail when the default can’t be cast.
TypeORM already drops DEFAULT for enum type changes to avoid casting issues, so the new generic path
is inconsistent and may introduce migration failures for columns with defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1596]

+                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 ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                 ),
             )
             downQueries.push(
                 new Query(
                     `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                         newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                 ),
Evidence
The new generic type/length/precision/scale change path adds a USING cast but does not touch column
defaults, so any existing DEFAULT remains in place during ALTER TYPE. In the same method, enum type
changes explicitly drop DEFAULT to avoid type-casting issues, and the generic default-update logic
only runs when the default expression changes—meaning type-only changes won’t adjust defaults at
all.

src/driver/postgres/PostgresQueryRunner.ts[1569-1597]
src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
src/driver/postgres/PostgresQueryRunner.ts[2218-2223]

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

## Issue description
`PostgresQueryRunner.changeColumn` now uses `ALTER COLUMN ... TYPE ... USING` for type/length changes, but it leaves existing `DEFAULT` expressions in place. PostgreSQL can fail `ALTER COLUMN TYPE` when a column has a default that cannot be cast to the new type. TypeORM already mitigates this in the enum-type-change flow by dropping the default before the type change.
### Issue Context
- The new generic type/length/precision/scale change block adds a USING cast but does not drop/recreate defaults.
- Default updates later in the method only run when `newColumn.default !== oldColumn.default`, so type-only changes won’t re-apply defaults if we drop them.
- `executeMemoryDownSql()` runs `downQueries.reverse()`, so order of `downQueries.push(...)` matters.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1598]
- src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
- src/driver/postgres/PostgresQueryRunner.ts[2218-2270]
### Implementation notes
- In the generic type-change block, if either old/new column has a default:
- set `defaultValueChanged = true`
- add an `upQueries` step to `DROP DEFAULT` **before** the `ALTER COLUMN ... TYPE ... USING` query
- add an `upQueries` step to `SET DEFAULT &amp;amp;amp;lt;newColumn.default&amp;amp;amp;gt;` (or keep dropped if new default is null/undefined) **after** the type change
- add corresponding `downQueries` that restore the old default (or drop it if old had none)
- Ensure you don’t double-apply defaults via the later default-update section (the `defaultValueChanged` flag should prevent that).

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


9. Non-portable raw SQL quotes🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;amp;amp;quot;post&amp;amp;amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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


10. PG type change default risk 🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now alters type/length via ALTER COLUMN ... TYPE ... USING, but it does not
drop/recreate an existing DEFAULT, which can make ALTER TYPE fail when the default can’t be cast.
TypeORM already drops DEFAULT for enum type changes to avoid casting issues, so the new generic path
is inconsistent and may introduce migration failures for columns with defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1596]

+                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 ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                  ),
              )
              downQueries.push(
                  new Query(
                      `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                          newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                  ),
Evidence
The new generic type/length/precision/scale change path adds a USING cast but does not touch column
defaults, so any existing DEFAULT remains in place during ALTER TYPE. In the same method, enum type
changes explicitly drop DEFAULT to avoid type-casting issues, and the generic default-update logic
only runs when the default expression changes—meaning type-only changes won’t adjust defaults at
all.

src/driver/postgres/PostgresQueryRunner.ts[1569-1597]
src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
src/driver/postgres/PostgresQueryRunner.ts[2218-2223]

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

## Issue description
`PostgresQueryRunner.changeColumn` now uses `ALTER COLUMN ... TYPE ... USING` for type/length changes, but it leaves existing `DEFAULT` expressions in place. PostgreSQL can fail `ALTER COLUMN TYPE` when a column has a default that cannot be cast to the new type. TypeORM already mitigates this in the enum-type-change flow by dropping the default before the type change.
### Issue Context
- The new generic type/length/precision/scale change block adds a USING cast but does not drop/recreate defaults.
- Default updates later in the method only run when `newColumn.default !== oldColumn.default`, so type-only changes won’t re-apply defaults if we drop them.
- `executeMemoryDownSql()` runs `downQueries.reverse()`, so order of `downQueries.push(...)` matters.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1598]
- src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
- src/driver/postgres/PostgresQueryRunner.ts[2218-2270]
### Implementation notes
- In the generic type-change block, if either old/new column has a default:
- set `defaultValueChanged = true`
- add an `upQueries` step to `DROP DEFAULT` **before** the `ALTER COLUMN ... TYPE ... USING` query
- add an `upQueries` step to `SET DEFAULT &amp;amp;lt;newColumn.default&amp;amp;gt;` (or keep dropped if new default is null/undefined) **after** the type change
- add corresponding `downQueries` that restore the old default (or drop it if old had none)
- Ensure you don’t double-apply defaults via the later default-update section (the `defaultValueChanged` flag should prevent that).

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


11. Non-portable raw SQL quotes 🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;amp;quot;post&amp;amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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



Remediation recommended

12. Test cleanup not guaranteed🐞 Bug ⛯ Reliability
Description
Row cleanup and schema revert are inside the try block; if the test throws before reaching them, the
inserted row and/or schema change may remain and can contaminate subsequent tests within the same
run. Defensive cleanup should be moved to the finally block (best effort).
Code

test/functional/query-runner/change-column.test.ts[R287-336]

+                try {
+                    // Insert test data before modifying the column
+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
+                    expect(rowsAfterLengthChange).to.have.length(1)
+                    expect(rowsAfterLengthChange[0]["name"]).to.equal(
+                        "Test Post",
+                    )
+
+                    // Verify column length was actually changed
+                    table = await queryRunner.getTable("post")
+                    table!
+                        .findColumnByName("name")!
+                        .length!.should.be.equal("500")
+
+                    // Revert changes and verify data is still intact
+                    await queryRunner.executeMemoryDownSql()
+
+                    const rowsAfterRevert = await connection.manager.query(
+                        `SELECT "name" FROM "post" WHERE "id" = 1`,
+                    )
+                    expect(rowsAfterRevert).to.have.length(1)
+                    expect(rowsAfterRevert[0]["name"]).to.equal("Test Post")
+
+                    // Clean up test data
+                    await connection.manager.query(
+                        `DELETE FROM "post" WHERE "id" = 1`,
+                    )
+                } finally {
+                    await queryRunner.release()
+                }
Evidence
The test guarantees only queryRunner.release() in finally. The row deletion and
executeMemoryDownSql() are in the try body, so they can be skipped on early failure/throw.

test/functional/query-runner/change-column.test.ts[287-336]
src/query-runner/BaseQueryRunner.ts[275-282]

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

## Issue description
Cleanup (row delete) and schema revert (`executeMemoryDownSql`) are executed inside the `try` body; an early throw (e.g., from `changeColumn` or an assertion) skips them.
### Issue Context
Within this `describe`, the schema is created once and reused across tests, so leftover rows/schema changes can contaminate subsequent tests in the same run.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[287-336]
### Suggested approach
- Track whether the row was inserted / column was changed.
- In `finally`, run best-effort cleanup:
- `await queryRunner.executeMemoryDownSql().catch(() =&amp;amp;gt; undefined)`
- delete the row (prefer repository/manager API; if raw SQL remains, ensure driver-portable quoting/parameters)
- then `await queryRunner.release()`

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


13. Cleanup swallows errors📘 Rule violation ⛯ Reliability
Description
The new functional test silently ignores failures via .catch(() => undefined) during cleanup,
which can hide real regressions and makes failures harder to diagnose. This introduces abnormal
defensive error-handling inconsistent with the rest of the suite.
Code

test/functional/query-runner/change-column.test.ts[R333-340]

+                    // Best-effort cleanup regardless of test outcome
+                    await queryRunner
+                        .executeMemoryDownSql()
+                        .catch(() => undefined)
+                    await postRepository
+                        .delete({ id: 1 })
+                        .catch(() => undefined)
+                    await queryRunner.release()
Evidence
Compliance ID 4 disallows abnormal defensive try/catch patterns and noise; the added cleanup
explicitly suppresses errors by catching and discarding them.

Rule 4: Remove AI-generated noise
test/functional/query-runner/change-column.test.ts[333-340]

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 test cleanup uses `.catch(() =&amp;amp;gt; undefined)` which silently ignores cleanup failures and adds abnormal defensive error handling.
## Issue Context
Other tests in this file typically call `executeMemoryDownSql()` and `release()` without swallowing errors; suppressing errors can hide regressions and makes debugging harder.
## Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[333-340]

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


14. No #3357 reference 📘 Rule violation ✓ Correctness
Description
This PR fixes issue #3357, but the added functional regression test does not include an issue
reference comment near the test as requested. This reduces traceability from the test back to the
reported bug.
Code

test/functional/query-runner/change-column.test.ts[R273-285]

+    it("should preserve data when changing column type or length", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                // SQLite does not impose length restrictions and handles types differently
+                if (DriverUtils.isSQLiteFamily(connection.driver)) return
+
+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
+
Evidence
Compliance ID 3 requests issue-reference comments in functional tests when applicable; the newly
added it("should preserve data...") block has no nearby #3357 reference.

Rule 3: Prefer functional tests over per-issue tests
test/functional/query-runner/change-column.test.ts[273-285]

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 for this fix does not include an issue reference comment (e.g., `#3357`) near the test.
## Issue Context
Compliance requires issue fixes to live in `test/functional` and to include an issue reference comment when applicable for traceability.
## Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[273-275]

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


View more (15)
15. Type conversion untested🐞 Bug ⛯ Reliability
Description
The added regression test exercises the new PostgreSQL USING-cast path via a length change, but it
doesn’t cover actual type conversions (nor default-casting scenarios). This leaves the highest-risk
edge case (type change + existing DEFAULT) unverified by tests.
Code

test/functional/query-runner/change-column.test.ts[R273-309]

+    it("should preserve data when changing column type or length", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                // SQLite does not impose length restrictions and handles types differently
+                if (DriverUtils.isSQLiteFamily(connection.driver)) return
+
+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
+
+                const queryRunner = connection.createQueryRunner()
+                const postRepository = connection.getRepository(Post)
+
+                try {
+                    // Insert test data before modifying the column
+                    await postRepository.insert({
+                        id: 1,
+                        version: 1,
+                        name: "Test Post",
+                        text: "Some text",
+                        tag: "tag1",
+                    })
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
Evi...

henryruss2 and others added 2 commits February 23, 2026 10:49
- Replace raw SQL with repository API for driver-portable queries
- Move cleanup (executeMemoryDownSql, row deletion) to finally block
  so test artifacts are cleaned up even on early failure
@qodo-free-for-open-source-projects

Code Review by Qodo

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

Grey Divider


Action required

1. PG type change default risk 🐞 Bug ⛯ Reliability ⭐ New
Description
PostgresQueryRunner now alters type/length via ALTER COLUMN ... TYPE ... USING, but it does not
drop/recreate an existing DEFAULT, which can make ALTER TYPE fail when the default can’t be cast.
TypeORM already drops DEFAULT for enum type changes to avoid casting issues, so the new generic path
is inconsistent and may introduce migration failures for columns with defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1596]

+                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 ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                    ),
                )
                downQueries.push(
                    new Query(
                        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                            newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                    ),
Evidence
The new generic type/length/precision/scale change path adds a USING cast but does not touch column
defaults, so any existing DEFAULT remains in place during ALTER TYPE. In the same method, enum type
changes explicitly drop DEFAULT to avoid type-casting issues, and the generic default-update logic
only runs when the default expression changes—meaning type-only changes won’t adjust defaults at
all.

src/driver/postgres/PostgresQueryRunner.ts[1569-1597]
src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
src/driver/postgres/PostgresQueryRunner.ts[2218-2223]

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

### Issue description
`PostgresQueryRunner.changeColumn` now uses `ALTER COLUMN ... TYPE ... USING` for type/length changes, but it leaves existing `DEFAULT` expressions in place. PostgreSQL can fail `ALTER COLUMN TYPE` when a column has a default that cannot be cast to the new type. TypeORM already mitigates this in the enum-type-change flow by dropping the default before the type change.

### Issue Context
- The new generic type/length/precision/scale change block adds a USING cast but does not drop/recreate defaults.
- Default updates later in the method only run when `newColumn.default !== oldColumn.default`, so type-only changes won’t re-apply defaults if we drop them.
- `executeMemoryDownSql()` runs `downQueries.reverse()`, so order of `downQueries.push(...)` matters.

### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1598]
- src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
- src/driver/postgres/PostgresQueryRunner.ts[2218-2270]

### Implementation notes
- In the generic type-change block, if either old/new column has a default:
 - set `defaultValueChanged = true`
 - add an `upQueries` step to `DROP DEFAULT` **before** the `ALTER COLUMN ... TYPE ... USING` query
 - add an `upQueries` step to `SET DEFAULT &lt;newColumn.default&gt;` (or keep dropped if new default is null/undefined) **after** the type change
 - add corresponding `downQueries` that restore the old default (or drop it if old had none)
- Ensure you don’t double-apply defaults via the later default-update section (the `defaultValueChanged` flag should prevent that).

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


2. Non-portable raw SQL quotes 🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;quot;post&amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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



Remediation recommended

3. Cleanup swallows errors 📘 Rule violation ⛯ Reliability ⭐ New
Description
The new functional test silently ignores failures via .catch(() => undefined) during cleanup,
which can hide real regressions and makes failures harder to diagnose. This introduces abnormal
defensive error-handling inconsistent with the rest of the suite.
Code

test/functional/query-runner/change-column.test.ts[R333-340]

+                    // Best-effort cleanup regardless of test outcome
+                    await queryRunner
+                        .executeMemoryDownSql()
+                        .catch(() => undefined)
+                    await postRepository
+                        .delete({ id: 1 })
+                        .catch(() => undefined)
+                    await queryRunner.release()
Evidence
Compliance ID 4 disallows abnormal defensive try/catch patterns and noise; the added cleanup
explicitly suppresses errors by catching and discarding them.

Rule 4: Remove AI-generated noise
test/functional/query-runner/change-column.test.ts[333-340]

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 test cleanup uses `.catch(() =&gt; undefined)` which silently ignores cleanup failures and adds abnormal defensive error handling.

## Issue Context
Other tests in this file typically call `executeMemoryDownSql()` and `release()` without swallowing errors; suppressing errors can hide regressions and makes debugging harder.

## Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[333-340]

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


4. No #3357 reference 📘 Rule violation ✧ Quality ⭐ New
Description
This PR fixes issue #3357, but the added functional regression test does not include an issue
reference comment near the test as requested. This reduces traceability from the test back to the
reported bug.
Code

test/functional/query-runner/change-column.test.ts[R273-285]

+    it("should preserve data when changing column type or length", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                // SQLite does not impose length restrictions and handles types differently
+                if (DriverUtils.isSQLiteFamily(connection.driver)) return
+
+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
+
Evidence
Compliance ID 3 requests issue-reference comments in functional tests when applicable; the newly
added it("should preserve data...") block has no nearby #3357 reference.

Rule 3: Prefer functional tests over per-issue tests
test/functional/query-runner/change-column.test.ts[273-285]

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 for this fix does not include an issue reference comment (e.g., `#3357`) near the test.

## Issue Context
Compliance requires issue fixes to live in `test/functional` and to include an issue reference comment when applicable for traceability.

## Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[273-275]

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


5. Type conversion untested 🐞 Bug ⛯ Reliability ⭐ New
Description
The added regression test exercises the new PostgreSQL USING-cast path via a length change, but it
doesn’t cover actual type conversions (nor default-casting scenarios). This leaves the highest-risk
edge case (type change + existing DEFAULT) unverified by tests.
Code

test/functional/query-runner/change-column.test.ts[R273-309]

+    it("should preserve data when changing column type or length", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                // SQLite does not impose length restrictions and handles types differently
+                if (DriverUtils.isSQLiteFamily(connection.driver)) return
+
+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
+
+                const queryRunner = connection.createQueryRunner()
+                const postRepository = connection.getRepository(Post)
+
+                try {
+                    // Insert test data before modifying the column
+                    await postRepository.insert({
+                        id: 1,
+                        version: 1,
+                        name: "Test Post",
+                        text: "Some text",
+                        tag: "tag1",
+                    })
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
Evidence
The new test case only changes the column length and never changes TableColumn.type, so it won’t
catch failures specific to type conversions (e.g., default casting) introduced by the new ALTER TYPE
... USING path.

test/functional/query-runner/change-column.test.ts[273-309]

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 regression test validates length changes but not true type conversions (e.g., `int` → `text`, `varchar` → `int`). The new Postgres `ALTER COLUMN ... TYPE ... USING` path is most likely to break on type conversions, especially when defaults exist.

### Issue Context
- The test name claims &quot;type or length&quot;, but current coverage is length-only.
- A type-conversion test would also surface default-casting problems.

### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[273-343]

### Suggested test additions
- Add a second sub-case in the same test (or a new test) that:
 - adds/ensures a default on a column (or uses an existing default column)
 - changes `TableColumn.type` (e.g., `varchar` → `text`, or `int` → `bigint`) using `changeColumn`
 - verifies data is preserved after the type change and after `executeMemoryDownSql()`
- Keep driver skips where type conversion is known unsupported, but avoid over-skipping drivers that should support the scenario.

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


View more (1)
6. Test cleanup not guaranteed 🐞 Bug ⛯ Reliability
Description
Row cleanup and schema revert are inside the try block; if the test throws before reaching them, the
inserted row and/or schema change may remain and can contaminate subsequent tests within the same
run. Defensive cleanup should be moved to the finally block (best effort).
Code

test/functional/query-runner/change-column.test.ts[R287-336]

+                try {
+                    // Insert test data before modifying the column
+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
+                    expect(rowsAfterLengthChange).to.have.length(1)
+                    expect(rowsAfterLengthChange[0]["name"]).to.equal(
+                        "Test Post",
+                    )
+
+                    // Verify column length was actually changed
+                    table = await queryRunner.getTable("post")
+                    table!
+                        .findColumnByName("name")!
+                        .length!.should.be.equal("500")
+
+                    // Revert changes and verify data is still intact
+                    await queryRunner.executeMemoryDownSql()
+
+                    const rowsAfterRevert = await connection.manager.query(
+                        `SELECT "name" FROM "post" WHERE "id" = 1`,
+                    )
+                    expect(rowsAfterRevert).to.have.length(1)
+                    expect(rowsAfterRevert[0]["name"]).to.equal("Test Post")
+
+                    // Clean up test data
+                    await connection.manager.query(
+                        `DELETE FROM "post" WHERE "id" = 1`,
+                    )
+                } finally {
+                    await queryRunner.release()
+                }
Evidence
The test guarantees only queryRunner.release() in finally. The row deletion and
executeMemoryDownSql() are in the try body, so they can be skipped on early failure/throw.

test/functional/query-runner/change-column.test.ts[287-336]
src/query-runner/BaseQueryRunner.ts[275-282]

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

## Issue description
Cleanup (row delete) and schema revert (`executeMemoryDownSql`) are executed inside the `try` body; an early throw (e.g., from `changeColumn` or an assertion) skips them.
### Issue Context
Within this `describe`, the schema is created once and reused across tests, so leftover rows/schema changes can contaminate subsequent tests in the same run.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[287-336]
### Suggested approach
- Track whether the row was inserted / column was changed.
- In `finally`, run best-effort cleanup:
- `await queryRunner.executeMemoryDownSql().catch(() =&amp;gt; undefined)`
- delete the row (prefer repository/manager API; if raw SQL remains, ensure driver-portable quoting/parameters)
- then `await queryRunner.release()`

ⓘ 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

…ures

Address code review feedback:
- Drop and recreate DEFAULT around ALTER COLUMN TYPE in PostgreSQL to
  prevent casting failures (mirrors existing enum type change pattern)
- Add type conversion test case (varchar -> text) alongside length change
- Add typeorm#3357 issue reference for traceability
- Use repository API instead of raw SQL for cross-driver portability
- Match existing test cleanup patterns
@qodo-free-for-open-source-projects

Code Review by Qodo

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

Grey Divider


Action required

1. Postgres enum ALTER TYPE wrong 🐞 Bug ✓ Correctness ⭐ New
Description
PostgresQueryRunner.changeColumn now builds ALTER COLUMN TYPE statements from
PostgresDriver.createFullType(). For enum/simple-enum columns, createFullType() does not translate
to the underlying user-defined enum type name (it just uses column.type), so changing a column
to/from enum can generate invalid SQL like TYPE enum / ::enum, and non-enum→enum conversions also
won’t create the enum type.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1623]

+                const newFullType = this.driver.createFullType(newColumn)
+                const oldFullType = this.driver.createFullType(oldColumn)
+
+                // if column has a default, drop it before ALTER TYPE to avoid
+                // casting failures (same pattern used for enum type changes)
+                if (
+                    oldColumn.default !== null &&
+                    oldColumn.default !== undefined
+                ) {
+                    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}`,
+                        ),
+                    )
+                }
+
                upQueries.push(
                    new Query(
                        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                            newColumn.name
-                        }" TYPE ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                    ),
                )
                downQueries.push(
                    new Query(
                        `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                            newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                    ),
                )
Evidence
The new type-change path in changeColumn uses PostgresDriver.createFullType() for both the TYPE
clause and the USING cast. PostgresDriver.createFullType() simply returns the TableColumn.type with
length/precision adjustments and has no enum/simple-enum special-casing, while
PostgresQueryRunner.buildCreateColumnSql explicitly handles enum/simple-enum by using
buildEnumName(table, column). Additionally, enum type creation exists in addColumn, but
changeColumn’s enum-handling block only runs when both old and new are already enum/simple-enum
(enum value/name changes), not for non-enum→enum conversions.

src/driver/postgres/PostgresQueryRunner.ts[1569-1647]
src/driver/postgres/PostgresDriver.ts[1269-1330]
src/driver/postgres/PostgresQueryRunner.ts[5031-5061]
src/driver/postgres/PostgresQueryRunner.ts[1031-1037]
src/driver/postgres/PostgresQueryRunner.ts[1649-1656]

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

### Issue description
`PostgresQueryRunner.changeColumn` now uses `this.driver.createFullType()` for `ALTER COLUMN ... TYPE` and its `USING` cast. For `enum`/`simple-enum`, `PostgresDriver.createFullType()` does **not** return the underlying user-defined enum type name (it uses `column.type`), which can produce invalid SQL (`TYPE enum`, `::enum`). Additionally, non-enum → enum conversions don’t create the enum type.

### Issue Context
Postgres enum columns are emitted via `buildEnumName(...)` in `buildCreateColumnSql`, and enum types are created in `addColumn` via `hasEnumType/createEnumTypeSql`. The new ALTER TYPE path should match that behavior.

### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1647]
- src/driver/postgres/PostgresQueryRunner.ts[5031-5061]
- src/driver/postgres/PostgresQueryRunner.ts[1020-1037]
- src/driver/postgres/PostgresDriver.ts[1269-1330]

### Expected change (high level)
- Introduce a small local helper inside `changeColumn` to compute `newTypeSql` / `oldTypeSql`:
 - if enum/simple-enum: use `this.buildEnumName(table, col)` (+ `[]` if array)
 - else: use `this.driver.createFullType(col)
- If converting non-enum → enum/simple-enum:
 - ensure enum type exists (`hasEnumType` / `createEnumTypeSql`) before the ALTER TYPE
 - consider a safer `USING` expression (possibly `&quot;col&quot;::text::enum_type`) similar to existing enum-change logic.
- Use these computed type SQL strings in both up/down ALTER TYPE queries.

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


2. Non-portable raw SQL quotes🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;quot;post&amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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


3. PG type change default risk 🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now alters type/length via ALTER COLUMN ... TYPE ... USING, but it does not
drop/recreate an existing DEFAULT, which can make ALTER TYPE fail when the default can’t be cast.
TypeORM already drops DEFAULT for enum type changes to avoid casting issues, so the new generic path
is inconsistent and may introduce migration failures for columns with defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1596]

+                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 ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                  ),
              )
              downQueries.push(
                  new Query(
                      `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                          newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                  ),
Evidence
The new generic type/length/precision/scale change path adds a USING cast but does not touch column
defaults, so any existing DEFAULT remains in place during ALTER TYPE. In the same method, enum type
changes explicitly drop DEFAULT to avoid type-casting issues, and the generic default-update logic
only runs when the default expression changes—meaning type-only changes won’t adjust defaults at
all.

src/driver/postgres/PostgresQueryRunner.ts[1569-1597]
src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
src/driver/postgres/PostgresQueryRunner.ts[2218-2223]

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

## Issue description
`PostgresQueryRunner.changeColumn` now uses `ALTER COLUMN ... TYPE ... USING` for type/length changes, but it leaves existing `DEFAULT` expressions in place. PostgreSQL can fail `ALTER COLUMN TYPE` when a column has a default that cannot be cast to the new type. TypeORM already mitigates this in the enum-type-change flow by dropping the default before the type change.
### Issue Context
- The new generic type/length/precision/scale change block adds a USING cast but does not drop/recreate defaults.
- Default updates later in the method only run when `newColumn.default !== oldColumn.default`, so type-only changes won’t re-apply defaults if we drop them.
- `executeMemoryDownSql()` runs `downQueries.reverse()`, so order of `downQueries.push(...)` matters.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1598]
- src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
- src/driver/postgres/PostgresQueryRunner.ts[2218-2270]
### Implementation notes
- In the generic type-change block, if either old/new column has a default:
- set `defaultValueChanged = true`
- add an `upQueries` step to `DROP DEFAULT` **before** the `ALTER COLUMN ... TYPE ... USING` query
- add an `upQueries` step to `SET DEFAULT &amp;amp;lt;newColumn.default&amp;amp;gt;` (or keep dropped if new default is null/undefined) **after** the type change
- add corresponding `downQueries` that restore the old default (or drop it if old had none)
- Ensure you don’t double-apply defaults via the later default-update section (the `defaultValueChanged` flag should prevent that).

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


View more (3)
4. Non-portable raw SQL quotes🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;amp;quot;post&amp;amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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


5. PG type change default risk 🐞 Bug ⛯ Reliability
Description
PostgresQueryRunner now alters type/length via ALTER COLUMN ... TYPE ... USING, but it does not
drop/recreate an existing DEFAULT, which can make ALTER TYPE fail when the default can’t be cast.
TypeORM already drops DEFAULT for enum type changes to avoid casting issues, so the new generic path
is inconsistent and may introduce migration failures for columns with defaults.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1578-1596]

+                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 ${this.driver.createFullType(newColumn)}`,
+                        }" TYPE ${newFullType} USING "${
+                            newColumn.name
+                        }"::${newFullType}`,
                   ),
               )
               downQueries.push(
                   new Query(
                       `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
                           newColumn.name
-                        }" TYPE ${this.driver.createFullType(oldColumn)}`,
+                        }" TYPE ${oldFullType} USING "${
+                            newColumn.name
+                        }"::${oldFullType}`,
                   ),
Evidence
The new generic type/length/precision/scale change path adds a USING cast but does not touch column
defaults, so any existing DEFAULT remains in place during ALTER TYPE. In the same method, enum type
changes explicitly drop DEFAULT to avoid type-casting issues, and the generic default-update logic
only runs when the default expression changes—meaning type-only changes won’t adjust defaults at
all.

src/driver/postgres/PostgresQueryRunner.ts[1569-1597]
src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
src/driver/postgres/PostgresQueryRunner.ts[2218-2223]

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

## Issue description
`PostgresQueryRunner.changeColumn` now uses `ALTER COLUMN ... TYPE ... USING` for type/length changes, but it leaves existing `DEFAULT` expressions in place. PostgreSQL can fail `ALTER COLUMN TYPE` when a column has a default that cannot be cast to the new type. TypeORM already mitigates this in the enum-type-change flow by dropping the default before the type change.
### Issue Context
- The new generic type/length/precision/scale change block adds a USING cast but does not drop/recreate defaults.
- Default updates later in the method only run when `newColumn.default !== oldColumn.default`, so type-only changes won’t re-apply defaults if we drop them.
- `executeMemoryDownSql()` runs `downQueries.reverse()`, so order of `downQueries.push(...)` matters.
### Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1569-1598]
- src/driver/postgres/PostgresQueryRunner.ts[1719-1744]
- src/driver/postgres/PostgresQueryRunner.ts[2218-2270]
### Implementation notes
- In the generic type-change block, if either old/new column has a default:
- set `defaultValueChanged = true`
- add an `upQueries` step to `DROP DEFAULT` **before** the `ALTER COLUMN ... TYPE ... USING` query
- add an `upQueries` step to `SET DEFAULT &amp;lt;newColumn.default&amp;gt;` (or keep dropped if new default is null/undefined) **after** the type change
- add corresponding `downQueries` that restore the old default (or drop it if old had none)
- Ensure you don’t double-apply defaults via the later default-update section (the `defaultValueChanged` flag should prevent that).

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


6. Non-portable raw SQL quotes 🐞 Bug ✓ Correctness
Description
The new test uses raw SQL with double-quoted identifiers ("post", "name"), which is not portable to
MySQL/MariaDB (they typically require backticks) and can break CI for those drivers. Existing tests
use driver-specific quoting when issuing raw SQL, suggesting this new test should too (or avoid raw
SQL entirely).
Code

test/functional/query-runner/change-column.test.ts[R289-309]

+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
Evidence
The test runs for all connections except SQLite/Cockroach/Spanner and executes raw SQL with
double-quoted identifiers. In this codebase, MySQL/MariaDB raw SQL in tests uses backticks, and the
MySQL query runner itself emits backticks for identifiers—indicating that hard-coding double quotes
is not the expected cross-driver syntax.

test/functional/query-runner/change-column.test.ts[272-333]
test/functional/query-runner/create-comment.test.ts[43-48]
src/driver/mysql/MysqlQueryRunner.ts[1113-1123]

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 test uses `connection.manager.query()` with SQL containing double-quoted identifiers (e.g. `INSERT INTO &amp;amp;quot;post&amp;amp;quot; ...`). This is not reliably portable across drivers (notably MySQL/MariaDB) and is inconsistent with existing tests that use backticks for MySQL/MariaDB raw SQL.
### Issue Context
This test runs across all created connections except SQLite/Cockroach/Spanner, so it must be driver-agnostic.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[272-336]
### Suggested approach
- Prefer using the entity API:
- `await connection.getRepository(Post).insert({...})`
- `await connection.getRepository(Post).findOneBy({ id: 1 })`
- `await connection.getRepository(Post).delete({ id: 1 })`
- If raw SQL is required, construct identifiers with the driver escape utilities and use parameters (avoid hard-coded quoting).

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



Remediation recommended

7. Cockroach change untested 🐞 Bug ⛯ Reliability ⭐ New
Description
CockroachQueryRunner behavior was changed to use ALTER COLUMN TYPE for type/length changes, but the
newly added regression test explicitly skips cockroachdb, leaving the modified path unvalidated and
increasing regression risk for CockroachDB users.
Code

test/functional/query-runner/change-column.test.ts[R281-286]

+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
Evidence
The CockroachDB query runner now routes type/length changes through ALTER COLUMN TYPE, but the new
test introduced to validate data preservation bails out for cockroachdb (and spanner). This means
the PR’s claimed CockroachDB behavior is not covered by the new test and could fail silently until
users hit it.

src/driver/cockroachdb/CockroachQueryRunner.ts[1616-1637]
test/functional/query-runner/change-column.test.ts[273-286]

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 changes CockroachDB’s `changeColumn` implementation to use `ALTER COLUMN TYPE` for type/length changes, but the newly added regression test that verifies data preservation explicitly skips CockroachDB. This leaves the new behavior untested.

### Issue Context
If CockroachDB has restrictions for certain ALTER TYPE operations, we should either (a) test the supported subset, or (b) codify/document the unsupported subset and add a guarded fallback.

### Fix Focus Areas
- src/driver/cockroachdb/CockroachQueryRunner.ts[1616-1637]
- test/functional/query-runner/change-column.test.ts[273-286]

### Options
1) Add a CockroachDB-compatible test variant (e.g., only length increase on a string type that Cockroach supports) and remove/relax the skip.
2) If CockroachDB cannot support these changes reliably, add explicit conditional logic + comments in the driver to fall back to the prior drop/add behavior for the unsupported cases, and update tests/docs accordingly.

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


8. Test cleanup not guaranteed🐞 Bug ⛯ Reliability
Description
Row cleanup and schema revert are inside the try block; if the test throws before reaching them, the
inserted row and/or schema change may remain and can contaminate subsequent tests within the same
run. Defensive cleanup should be moved to the finally block (best effort).
Code

test/functional/query-runner/change-column.test.ts[R287-336]

+                try {
+                    // Insert test data before modifying the column
+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
+                    expect(rowsAfterLengthChange).to.have.length(1)
+                    expect(rowsAfterLengthChange[0]["name"]).to.equal(
+                        "Test Post",
+                    )
+
+                    // Verify column length was actually changed
+                    table = await queryRunner.getTable("post")
+                    table!
+                        .findColumnByName("name")!
+                        .length!.should.be.equal("500")
+
+                    // Revert changes and verify data is still intact
+                    await queryRunner.executeMemoryDownSql()
+
+                    const rowsAfterRevert = await connection.manager.query(
+                        `SELECT "name" FROM "post" WHERE "id" = 1`,
+                    )
+                    expect(rowsAfterRevert).to.have.length(1)
+                    expect(rowsAfterRevert[0]["name"]).to.equal("Test Post")
+
+                    // Clean up test data
+                    await connection.manager.query(
+                        `DELETE FROM "post" WHERE "id" = 1`,
+                    )
+                } finally {
+                    await queryRunner.release()
+                }
Evidence
The test guarantees only queryRunner.release() in finally. The row deletion and
executeMemoryDownSql() are in the try body, so they can be skipped on early failure/throw.

test/functional/query-runner/change-column.test.ts[287-336]
src/query-runner/BaseQueryRunner.ts[275-282]

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

## Issue description
Cleanup (row delete) and schema revert (`executeMemoryDownSql`) are executed inside the `try` body; an early throw (e.g., from `changeColumn` or an assertion) skips them.
### Issue Context
Within this `describe`, the schema is created once and reused across tests, so leftover rows/schema changes can contaminate subsequent tests in the same run.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[287-336]
### Suggested approach
- Track whether the row was inserted / column was changed.
- In `finally`, run best-effort cleanup:
- `await queryRunner.executeMemoryDownSql().catch(() =&amp;amp;gt; undefined)`
- delete the row (prefer repository/manager API; if raw SQL remains, ensure driver-portable quoting/parameters)
- then `await queryRunner.release()`

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


9. Cleanup swallows errors 📘 Rule violation ⛯ Reliability
Description
The new functional test silently ignores failures via .catch(() => undefined) during cleanup,
which can hide real regressions and makes failures harder to diagnose. This introduces abnormal
defensive error-handling inconsistent with the rest of the suite.
Code

test/functional/query-runner/change-column.test.ts[R333-340]

+                    // Best-effort cleanup regardless of test outcome
+                    await queryRunner
+                        .executeMemoryDownSql()
+                        .catch(() => undefined)
+                    await postRepository
+                        .delete({ id: 1 })
+                        .catch(() => undefined)
+                    await queryRunner.release()
Evidence
Compliance ID 4 disallows abnormal defensive try/catch patterns and noise; the added cleanup
explicitly suppresses errors by catching and discarding them.

Rule 4: Remove AI-generated noise
test/functional/query-runner/change-column.test.ts[333-340]

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 test cleanup uses `.catch(() =&amp;amp;gt; undefined)` which silently ignores cleanup failures and adds abnormal defensive error handling.
## Issue Context
Other tests in this file typically call `executeMemoryDownSql()` and `release()` without swallowing errors; suppressing errors can hide regressions and makes debugging harder.
## Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[333-340]

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


View more (7)
10. No #3357 reference 📘 Rule violation ✓ Correctness
Description
This PR fixes issue #3357, but the added functional regression test does not include an issue
reference comment near the test as requested. This reduces traceability from the test back to the
reported bug.
Code

test/functional/query-runner/change-column.test.ts[R273-285]

+    it("should preserve data when changing column type or length", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                // SQLite does not impose length restrictions and handles types differently
+                if (DriverUtils.isSQLiteFamily(connection.driver)) return
+
+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
+
Evidence
Compliance ID 3 requests issue-reference comments in functional tests when applicable; the newly
added it("should preserve data...") block has no nearby #3357 reference.

Rule 3: Prefer functional tests over per-issue tests
test/functional/query-runner/change-column.test.ts[273-285]

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 for this fix does not include an issue reference comment (e.g., `#3357`) near the test.
## Issue Context
Compliance requires issue fixes to live in `test/functional` and to include an issue reference comment when applicable for traceability.
## Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[273-275]

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


11. Type conversion untested 🐞 Bug ⛯ Reliability
Description
The added regression test exercises the new PostgreSQL USING-cast path via a length change, but it
doesn’t cover actual type conversions (nor default-casting scenarios). This leaves the highest-risk
edge case (type change + existing DEFAULT) unverified by tests.
Code

test/functional/query-runner/change-column.test.ts[R273-309]

+    it("should preserve data when changing column type or length", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                // SQLite does not impose length restrictions and handles types differently
+                if (DriverUtils.isSQLiteFamily(connection.driver)) return
+
+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
+
+                const queryRunner = connection.createQueryRunner()
+                const postRepository = connection.getRepository(Post)
+
+                try {
+                    // Insert test data before modifying the column
+                    await postRepository.insert({
+                        id: 1,
+                        version: 1,
+                        name: "Test Post",
+                        text: "Some text",
+                        tag: "tag1",
+                    })
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
Evidence
The new test case only changes the column length and never changes TableColumn.type, so it won’t
catch failures specific to type conversions (e.g., default casting) introduced by the new ALTER TYPE
... USING path.

test/functional/query-runner/change-column.test.ts[273-309]

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 regression test validates length changes but not true type conversions (e.g., `int` → `text`, `varchar` → `int`). The new Postgres `ALTER COLUMN ... TYPE ... USING` path is most likely to break on type conversions, especially when defaults exist.
### Issue Context
- The test name claims &amp;amp;quot;type or length&amp;amp;quot;, but current coverage is length-only.
- A type-conversion test would also surface default-casting problems.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[273-343]
### Suggested test additions
- Add a second sub-case in the same test (or a new test) that:
- adds/ensures a default on a column (or uses an existing default column)
- changes `TableColumn.type` (e.g., `varchar` → `text`, or `int` → `bigint`) using `changeColumn`
- verifies data is preserved after the type change and after `executeMemoryDownSql()`
- Keep driver skips where type conversion is known unsupported, but avoid over-skipping drivers that should support the scenario.

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


12. Test cleanup not guaranteed🐞 Bug ⛯ Reliability
Description
Row cleanup and schema revert are inside the try block; if the test throws before reaching them, the
inserted row and/or schema change may remain and can contaminate subsequent tests within the same
run. Defensive cleanup should be moved to the finally block (best effort).
Code

test/functional/query-runner/change-column.test.ts[R287-336]

+                try {
+                    // Insert test data before modifying the column
+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
+                    expect(rowsAfterLengthChange).to.have.length(1)
+                    expect(rowsAfterLengthChange[0]["name"]).to.equal(
+                        "Test Post",
+                    )
+
+                    // Verify column length was actually changed
+                    table = await queryRunner.getTable("post")
+                    table!
+                        .findColumnByName("name")!
+                        .length!.should.be.equal("500")
+
+                    // Revert changes and verify data is still intact
+                    await queryRunner.executeMemoryDownSql()
+
+                    const rowsAfterRevert = await connection.manager.query(
+                        `SELECT "name" FROM "post" WHERE "id" = 1`,
+                    )
+                    expect(rowsAfterRevert).to.have.length(1)
+                    expect(rowsAfterRevert[0]["name"]).to.equal("Test Post")
+
+                    // Clean up test data
+                    await connection.manager.query(
+                        `DELETE FROM "post" WHERE "id" = 1`,
+                    )
+                } finally {
+                    await queryRunner.release()
+                }
Evidence
The test guarantees only queryRunner.release() in finally. The row deletion and
executeMemoryDownSql() are in the try body, so they can be skipped on early failure/throw.

test/functional/query-runner/change-column.test.ts[287-336]
src/query-runner/BaseQueryRunner.ts[275-282]

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

## Issue description
Cleanup (row delete) and schema revert (`executeMemoryDownSql`) are executed inside the `try` body; an early throw (e.g., from `changeColumn` or an assertion) skips them.
### Issue Context
Within this `describe`, the schema is created once and reused across tests, so leftover rows/schema changes can contaminate subsequent tests in the same run.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[287-336]
### Suggested approach
- Track whether the row was inserted / column was changed.
- In `finally`, run best-effort cleanup:
- `await queryRunner.executeMemoryDownSql().catch(() =&amp;amp;amp;gt; undefined)`
- delete the row (prefer repository/manager API; if raw SQL remains, ensure driver-portable quoting/parameters)
- then `await queryRunner.release()`

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


13. Cleanup swallows errors 📘 Rule violation ⛯ Reliability
Description
The new functional test silently ignores failures via .catch(() => undefined) during cleanup,
which can hide real regressions and makes failures harder to diagnose. This introduces abnormal
defensive error-handling inconsistent with the rest of the suite.
Code

test/functional/query-runner/change-column.test.ts[R333-340]

+                    // Best-effort cleanup regardless of test outcome
+                    await queryRunner
+                        .executeMemoryDownSql()
+                        .catch(() => undefined)
+                    await postRepository
+                        .delete({ id: 1 })
+                        .catch(() => undefined)
+                    await queryRunner.release()
Evidence
Compliance ID 4 disallows abnormal defensive try/catch patterns and noise; the added cleanup
explicitly suppresses errors by catching and discarding them.

Rule 4: Remove AI-generated noise
test/functional/query-runner/change-column.test.ts[333-340]

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 test cleanup uses `.catch(() =&amp;gt; undefined)` which silently ignores cleanup failures and adds abnormal defensive error handling.
## Issue Context
Other tests in this file typically call `executeMemoryDownSql()` and `release()` without swallowing errors; suppressing errors can hide regressions and makes debugging harder.
## Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[333-340]

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


14. No #3357 reference 📘 Rule violation ✓ Correctness
Description
This PR fixes issue #3357, but the added functional regression test does not include an issue
reference comment near the test as requested. This reduces traceability from the test back to the
reported bug.
Code

test/functional/query-runner/change-column.test.ts[R273-285]

+    it("should preserve data when changing column type or length", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                // SQLite does not impose length restrictions and handles types differently
+                if (DriverUtils.isSQLiteFamily(connection.driver)) return
+
+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
+
Evidence
Compliance ID 3 requests issue-reference comments in functional tests when applicable; the newly
added it("should preserve data...") block has no nearby #3357 reference.

Rule 3: Prefer functional tests over per-issue tests
test/functional/query-runner/change-column.test.ts[273-285]

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 for this fix does not include an issue reference comment (e.g., `#3357`) near the test.
## Issue Context
Compliance requires issue fixes to live in `test/functional` and to include an issue reference comment when applicable for traceability.
## Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[273-275]

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


15. Type conversion untested 🐞 Bug ⛯ Reliability
Description
The added regression test exercises the new PostgreSQL USING-cast path via a length change, but it
doesn’t cover actual type conversions (nor default-casting scenarios). This leaves the highest-risk
edge case (type change + existing DEFAULT) unverified by tests.
Code

test/functional/query-runner/change-column.test.ts[R273-309]

+    it("should preserve data when changing column type or length", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                // SQLite does not impose length restrictions and handles types differently
+                if (DriverUtils.isSQLiteFamily(connection.driver)) return
+
+                // CockroachDB and Spanner may have restrictions on ALTER COLUMN TYPE
+                if (
+                    connection.driver.options.type === "cockroachdb" ||
+                    connection.driver.options.type === "spanner"
+                )
+                    return
+
+                const queryRunner = connection.createQueryRunner()
+                const postRepository = connection.getRepository(Post)
+
+                try {
+                    // Insert test data before modifying the column
+                    await postRepository.insert({
+                        id: 1,
+                        version: 1,
+                        name: "Test Post",
+                        text: "Some text",
+                        tag: "tag1",
+                    })
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
Evidence
The new test case only changes the column length and never changes TableColumn.type, so it won’t
catch failures specific to type conversions (e.g., default casting) introduced by the new ALTER TYPE
... USING path.

test/functional/query-runner/change-column.test.ts[273-309]

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 regression test validates length changes but not true type conversions (e.g., `int` → `text`, `varchar` → `int`). The new Postgres `ALTER COLUMN ... TYPE ... USING` path is most likely to break on type conversions, especially when defaults exist.
### Issue Context
- The test name claims &amp;quot;type or length&amp;quot;, but current coverage is length-only.
- A type-conversion test would also surface default-casting problems.
### Fix Focus Areas
- test/functional/query-runner/change-column.test.ts[273-343]
### Suggested test additions
- Add a second sub-case in the same test (or a new test) that:
- adds/ensures a default on a column (or uses an existing default column)
- changes `TableColumn.type` (e.g., `varchar` → `text`, or `int` → `bigint`) using `changeColumn`
- verifies data is preserved after the type change and after `executeMemoryDownSql()`
- Keep driver skips where type conversion is known unsupported, but avoid over-skipping drivers that should support the scenario.

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


16. Test cleanup not guaranteed 🐞 Bug ⛯ Reliability
Description
Row cleanup and schema revert are inside the try block; if the test throws before reaching them, the
inserted row and/or schema change may remain and can contaminate subsequent tests within the same
run. Defensive cleanup should be moved to the finally block (best effort).
Code

test/functional/query-runner/change-column.test.ts[R287-336]

+                try {
+                    // Insert test data before modifying the column
+                    await connection.manager.query(
+                        `INSERT INTO "post" ("id", "version", "name", "text", "tag") VALUES (1, 1, 'Test Post', 'Some text', 'tag1')`,
+                    )
+
+                    let table = await queryRunner.getTable("post")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change column length (e.g., varchar(255) -> varchar(500))
+                    const changedNameColumn = nameColumn.clone()
+                    changedNameColumn.length = "500"
+                    await queryRunner.changeColumn(
+                        table!,
+                        nameColumn,
+                        changedNameColumn,
+                    )
+
+                    // Verify data is preserved after length change
+                    const rowsAfterLengthChange =
+                        await connection.manager.query(
+                            `SELECT "name" FROM "post" WHERE "id" = 1`,
+                        )
+                    expect(rowsAfterLengthChange).to.have.length(1)
+                    expect(rowsAfterLengthChange[0]["name"]).to.equal(
+                        "Test Post",
+                    )
+
+                    // Verify column length was actually changed
+                    table = await queryRunner.getTable("post")
+                    table!
+                        .findColumnByName("name")!
+                        .length!.should.be.equal("500")
+
+                    // Revert changes and verify data is still intact
+                    await queryRunner.executeMemoryDownSql()
+
+                    const rowsAfterRevert = await connection.manager.query(
+                        `SELECT "name" FROM "post" WHERE "id" = 1`,
+                    )
+                    expect(rowsAfterRevert).to.have.length(1)
+                    expect(rowsAfterRevert[0]["name"]).to.equal("Test Post")
+
+                    // Clean up test data
+                    await connection.manager.query(
+                        `DELETE FROM "post" WHERE...

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

2 participants