Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(postgres): improve handling of shared ENUMs during schema sync#12000

Open
baszczewski wants to merge 1 commit intotypeorm:masterfrom
baszczewski:fix/enum-name-recreated
Open

fix(postgres): improve handling of shared ENUMs during schema sync#12000
baszczewski wants to merge 1 commit intotypeorm:masterfrom
baszczewski:fix/enum-name-recreated

Conversation

@baszczewski
Copy link

Problem

Synchronizing PostgreSQL ENUM types shared across multiple columns or tables could:

  • Attempt to re-create or drop ENUM types still in use
  • Fail when adding values that already exist
  • Mis-handle shared ENUM renaming These issues caused errors during migrations or schema sync.

Root Cause

Schema builder did not properly handle:

  • Reuse of ENUM types across multiple metadata entries
  • Existing ENUM values in the database
  • Dependency tracking of tables referencing a shared ENUM

Solution

  • Prevent dropping ENUM types still referenced by other columns
  • Use ALTER TYPE ... ADD VALUE IF NOT EXISTS (Postgres 12+)
  • Improve detection logic for shared ENUM reuse and renaming
  • Ensure idempotent ENUM modifications during schema synchronization

Tests

Added functional tests covering:

  • Shared ENUM reuse
  • Adding existing enum values
  • ENUM renaming scenarios

Closes #9233

Description of change

Pull-Request Checklist

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

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

User description

Problem

Synchronizing PostgreSQL ENUM types shared across multiple columns or tables could:

  • Attempt to re-create or drop ENUM types still in use
  • Fail when adding values that already exist
  • Mis-handle shared ENUM renaming These issues caused errors during migrations or schema sync.

Root Cause

Schema builder did not properly handle:

  • Reuse of ENUM types across multiple metadata entries
  • Existing ENUM values in the database
  • Dependency tracking of tables referencing a shared ENUM

Solution

  • Prevent dropping ENUM types still referenced by other columns
  • Use ALTER TYPE ... ADD VALUE IF NOT EXISTS (Postgres 12+)
  • Improve detection logic for shared ENUM reuse and renaming
  • Ensure idempotent ENUM modifications during schema synchronization

Tests

Added functional tests covering:

  • Shared ENUM reuse
  • Adding existing enum values
  • ENUM renaming scenarios

Closes #9233

Description of change

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • This pull request links relevant issues as Fixes #9233
  • 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, Tests


Description

  • Prevent dropping shared ENUMs still referenced by other columns

  • Use ALTER TYPE ADD VALUE IF NOT EXISTS for idempotent modifications

  • Add isEnumUsed() method to detect ENUM reuse across tables

  • Improve ENUM renaming logic with proper schema handling

  • Add comprehensive test coverage for shared ENUM scenarios


Diagram Walkthrough

flowchart LR
  A["ENUM Sync Logic"] --> B["Check if ENUM Used"]
  B --> C["isEnumUsed Method"]
  C --> D["Query information_schema"]
  D --> E["Count Other Usages"]
  E --> F{Used by Others?}
  F -->|Yes| G["Skip Drop/Create"]
  F -->|No| H["Drop/Create ENUM"]
  A --> I["ALTER TYPE ADD VALUE"]
  I --> J["Add IF NOT EXISTS Clause"]
  J --> K["Idempotent Modifications"]
Loading

File Walkthrough

Relevant files
Bug fix
PostgresQueryRunner.ts
Implement ENUM usage detection and idempotent modifications

src/driver/postgres/PostgresQueryRunner.ts

  • Added isEnumUsed() method to check if ENUM is referenced by other
    columns
  • Added checkEnumUsage() helper method for counting ENUM column usages
  • Modified dropColumn() to skip ENUM drop if still in use by other
    columns
  • Modified changeColumn() to check ENUM usage before creating/dropping
    types
  • Updated ALTER TYPE ADD VALUE to use IF NOT EXISTS clause for
    idempotency
  • Improved ENUM renaming logic with proper schema detection and usage
    checks
  • Added raw ENUM name variants for accurate usage detection
+120/-24
Tests
postgres-enum-shared.test.ts
Add comprehensive shared ENUM synchronization tests           

test/functional/database-schema/column-types/postgres-enum-shared/postgres-enum-shared.test.ts

  • Added test for dropping shared ENUM column without dropping the type
  • Added test for changing column on shared ENUM (adding values)
  • Added test for failing scenario when removing ENUM values from shared
    type
  • Added test for renaming ENUM when used by another table with default
    schema
  • Added test for reusing existing ENUM when renaming to existing enum
    name
  • Added test for idempotent ADD VALUE using IF NOT EXISTS clause
+413/-0 
issue-9233.ts
Add regression test for issue #9233                                           

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

+79/-0   

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

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

PR Code Suggestions ✨

Latest suggestions up to 011554d

CategorySuggestion                                                                                                                                    Impact
Possible issue
Normalize enum name before querying

In the isEnumUsed method, normalize the enumName to be unqualified before
querying information_schema.columns to prevent incorrect results when a
schema-qualified name is provided.

src/driver/postgres/PostgresQueryRunner.ts [4565-4589]

 protected async isEnumUsed(
     table: Table,
     column: TableColumn,
     enumName: string,
     schema: string,
 ): Promise<boolean> {
-    // check if enum is used in any other table/column...
+    const unqualifiedEnumName = enumName.split(".").pop()!
+
     const sql =
         `SELECT "table_name", "column_name" FROM "information_schema"."columns" ` +
         `WHERE "table_schema" = $1 AND "udt_name" = $2`
-    const results = await this.query(sql, [schema, enumName])
+    const results = await this.query(sql, [schema, unqualifiedEnumName])
 
     const { tableName } = this.driver.parseTableName(table)
 
-    // ...except for the table/column we are about to drop
     const otherUsages = results.filter(
         (row: any) =>
             !(
                 row.table_name === tableName &&
                 row.column_name === column.name
             ),
     )
 
     return otherUsages.length > 0
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a critical bug where a schema-qualified enumName would cause isEnumUsed to fail, potentially leading to incorrect data schema modifications like dropping a shared enum that is still in use.

Medium
Ensure existing enum matches values

When an enum is found to be in use, ensure all values from the newColumn
definition exist in the enum type by adding any missing values using ALTER TYPE
... ADD VALUE IF NOT EXISTS.

src/driver/postgres/PostgresQueryRunner.ts [1719-1743]

 const newEnumNameRaw = this.buildEnumName(
     table,
     newColumn,
     false,
     true,
 )
 const newEnumIsUsed = await this.isEnumUsed(
     table,
     newColumn,
     newEnumNameRaw,
     schema!,
 )
 
 if (!newEnumIsUsed) {
     upQueries.push(
         this.createEnumTypeSql(
             table,
             newColumn,
             newEnumName,
         ),
     )
     downQueries.push(
         this.dropEnumTypeSql(table, newColumn, newEnumName),
     )
+} else if (newColumn.enum?.length) {
+    for (const item of newColumn.enum) {
+        const escapedValue = item.replaceAll("'", "''")
+        upQueries.push(
+            new Query(
+                `ALTER TYPE ${newEnumName} ADD VALUE IF NOT EXISTS '${escapedValue}'`,
+            ),
+        )
+    }
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out a scenario where an existing enum might not have all the required values, and proposes a valid fix to add missing values, improving the robustness of schema migrations.

Medium
General
Make version parsing robust

Improve the PostgreSQL version parsing in the test by using a regular expression
to more reliably extract the major version number, making the check less
brittle.

test/functional/database-schema/column-types/postgres-enum-shared/postgres-enum-shared.test.ts [337-346]

 it("should successfully add value to shared enum using IF NOT EXISTS", () =>
     Promise.all(
         dataSources.map(async (connection) => {
+            const versionText = connection.driver.version || ""
+            const major = Number((versionText.match(/\d+/) || ["0"])[0])
+
             // Skip if Postgres version is less than 12, as IF NOT EXISTS is not supported
             if (
                 connection.driver.options.type === "postgres" &&
-                parseInt(connection.driver.version || "0") < 12
+                major < 12
             ) {
                 return
             }
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion improves the robustness of version parsing in a test file, which is a good practice to avoid brittle tests, although parseInt often works for the expected version format.

Low
  • More

Previous suggestions

Suggestions up to commit 4b411ea
CategorySuggestion                                                                                                                                    Impact
High-level
Unconditionally using a Postgres 12+ feature

The implementation uses ADD VALUE IF NOT EXISTS without checking the PostgreSQL
version. This should be made conditional, with a fallback to ADD VALUE for
versions before 12 to maintain compatibility.

Examples:

src/driver/postgres/PostgresQueryRunner.ts [1664]
                                `ALTER TYPE ${oldEnumName} ADD VALUE IF NOT EXISTS '${escapedValue}'`,

Solution Walkthrough:

Before:

class PostgresQueryRunner extends BaseQueryRunner {
    async changeColumn(...) {
        // ...
        if (isEnum && newColumn.enum && oldColumn.enum) {
            const newEnumValues = newColumn.enum;
            const oldEnumValues = oldColumn.enum;
            const extraItems = newEnumValues.filter(item => oldEnumValues.indexOf(item) === -1);

            for (const item of extraItems) {
                upQueries.push(
                    new Query(`ALTER TYPE ${oldEnumName} ADD VALUE IF NOT EXISTS '${item}'`)
                );
            }
        }
        // ...
    }
}

After:

class PostgresQueryRunner extends BaseQueryRunner {
    async changeColumn(...) {
        // ...
        if (isEnum && newColumn.enum && oldColumn.enum) {
            const postgresVersion = await this.getVersion();
            const useIfNotExists = VersionUtils.isGreaterOrEqual(postgresVersion, "12");
            const addValueClause = useIfNotExists ? "ADD VALUE IF NOT EXISTS" : "ADD VALUE";

            const extraItems = newColumn.enum.filter(item => oldColumn.enum.indexOf(item) === -1);

            for (const item of extraItems) {
                upQueries.push(
                    new Query(`ALTER TYPE ${oldEnumName} ${addValueClause} '${item}'`)
                );
            }
        }
        // ...
    }
}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical compatibility issue where a PostgreSQL 12+ feature (ADD VALUE IF NOT EXISTS) is used unconditionally, which would break schema synchronization on older supported Postgres versions.

High
Security
Parameterize SQL query

In the isEnumUsed method, replace the string-interpolated SQL query with a
parameterized query to prevent potential SQL injection vulnerabilities.

src/driver/postgres/PostgresQueryRunner.ts [4574-4577]

-const sql =
-    `SELECT "table_name", "column_name" FROM "information_schema"."columns" ` +
-    `WHERE "table_schema" = '${schema}' AND "udt_name" = '${enumName}'`
-const results = await this.query(sql)
+const sql = `
+  SELECT table_name, column_name
+  FROM information_schema.columns
+  WHERE table_schema = $1 AND udt_name = $2
+`
+const results = await this.query(sql, [schema, enumName])
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential SQL injection vulnerability by using string interpolation for query values and recommends using parameterized queries, which is a critical security best practice.

Medium
General
Normalize identifier casing

Normalize table and column names to lowercase when filtering results from
information_schema to ensure case-insensitive comparison and prevent incorrect
enum usage detection.

src/driver/postgres/PostgresQueryRunner.ts [4580-4586]

 const otherUsages = results.filter(
     (row: any) =>
         !(
-            row.table_name === table.name &&
-            row.column_name === column.name
+            row.table_name.toLowerCase() === table.name.toLowerCase() &&
+            row.column_name.toLowerCase() === column.name.toLowerCase()
         ),
 )
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a potential bug where case-sensitive comparisons with information_schema results could fail, leading to incorrect behavior in the new isEnumUsed logic.

Medium
Escape enum identifiers

In the ALTER TYPE ... RENAME TO query, use an escaping utility like
this.escapePath for enum identifiers to handle special characters correctly.

src/driver/postgres/PostgresQueryRunner.ts [1709]

-`ALTER TYPE ${oldEnumName} RENAME TO ${oldEnumNameWithoutSchema_old}`,
+`ALTER TYPE ${this.escapePath(oldEnumName)} RENAME TO ${this.escapePath(oldEnumNameWithoutSchema_old)}`,
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out that raw string interpolation for identifiers in an ALTER TYPE query can lead to errors if the names contain special characters, and recommends using an existing escaping utility.

Low

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

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

Code Review by Qodo

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

Grey Divider


Action required

1. Shared enum rename corrupts other columns 🐞 Bug ✓ Correctness ⭐ New
Description
In the else branch of changeColumn, the old enum is unconditionally renamed to _old via `ALTER
TYPE ... RENAME TO`, which is a global PostgreSQL operation. When the enum is shared across multiple
tables/columns, this rename changes the type name as seen by ALL referencing columns. On the next
synchronize(), TypeORM reads those unrelated columns as having type old_enum_old while entity
definitions declare old_enum, triggering an infinite loop of schema changes.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1706-1708]

                    // rename old ENUM
                    upQueries.push(
                        new Query(
Evidence
The rename query is pushed unconditionally at lines 1707–1711, before any isEnumUsed check. The
isEnumUsed guard introduced by this PR (lines 1820–1825) only protects the DROP of the renamed
type — it cannot undo the global rename that already affected all other referencing columns. The
test 'should successfully rename enum when it is used by another table'
(postgres-enum-shared.test.ts lines 251–260) does not run a second synchronize() after the change,
so the infinite-drift scenario is not caught. buildEnumName confirms the rename target is the
globally-scoped type name.

src/driver/postgres/PostgresQueryRunner.ts[1707-1711]
src/driver/postgres/PostgresQueryRunner.ts[1820-1825]
test/functional/database-schema/column-types/postgres-enum-shared/postgres-enum-shared.test.ts[251-260]

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

## Issue description
In `changeColumn`, the `else` branch (triggered when enum values are removed or the enum name changes) unconditionally renames the old enum type to `_old`. Because `ALTER TYPE ... RENAME TO` is a global PostgreSQL operation, this rename affects ALL columns in ALL tables that reference the same enum type. When the enum is shared, subsequent calls to `synchronize()` detect the renamed type as a schema drift and attempt another `changeColumn`, creating an infinite loop.

## Issue Context
The `isEnumUsed` check was added to guard the DROP of the renamed type, but the rename itself must also be guarded. When the old enum is shared, the rename must be skipped to preserve the type name for other referencing columns.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1706-1716] — wrap the rename `upQueries.push` and its corresponding `downQueries.push` inside an `if (!isOldEnumShared)` block, where `isOldEnumShared` is computed via `isEnumUsed(table, oldColumn, oldEnumNameWithoutSchemaRaw, schema!)` before the rename is attempted
- src/driver/postgres/PostgresQueryRunner.ts[1820-1842] — the existing `isEnumUsed` guard for the drop can remain, but must be consistent with the rename guard above
- test/functional/database-schema/column-types/postgres-enum-shared/postgres-enum-shared.test.ts[251-260] — add a second `synchronize()` call after the first and assert that no further queries are generated (idempotency check)

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


2. isEnumUsed wrong guard for new enum creation 🐞 Bug ⛯ Reliability ⭐ New
Description
isEnumUsed checks information_schema.columns for column references to the type, but does NOT
check whether the type itself exists in pg_type. If the new enum type already exists in pg_type
with no column references (an orphaned type from a previously interrupted migration), isEnumUsed
returns false, the code pushes CREATE TYPE, and the query fails at runtime with `ERROR: type
already exists. The correct guard is hasEnumType`, which checks type existence in
pg_type/pg_namespace.
Code

src/driver/postgres/PostgresQueryRunner.ts[R1725-1743]

+                    const newEnumIsUsed = await this.isEnumUsed(
+                        table,
+                        newColumn,
+                        newEnumNameRaw,
+                        schema!,
                    )

+                    if (!newEnumIsUsed) {
+                        upQueries.push(
+                            this.createEnumTypeSql(
+                                table,
+                                newColumn,
+                                newEnumName,
+                            ),
+                        )
+                        downQueries.push(
+                            this.dropEnumTypeSql(table, newColumn, newEnumName),
+                        )
+                    }
Evidence
isEnumUsed queries information_schema.columns WHERE udt_name = $2, which returns zero rows for a
type that exists in pg_type but has no column assignments. hasEnumType (already present in the
codebase) queries pg_type JOIN pg_namespace and correctly identifies type existence regardless of
column usage. An orphaned type is a realistic outcome when a prior migration fails mid-execution
after the CREATE TYPE but before the ALTER COLUMN.

src/driver/postgres/PostgresQueryRunner.ts[4572-4575]
src/driver/postgres/PostgresQueryRunner.ts[4549-4555]
src/driver/postgres/PostgresQueryRunner.ts[1725-1743]

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 guard before `createEnumTypeSql` uses `isEnumUsed`, which queries `information_schema.columns` for column references. This does not detect an orphaned type (one that exists in `pg_type` but has no column assignments). The correct check is `hasEnumType`, which queries `pg_type JOIN pg_namespace` for type existence.

## Issue Context
An orphaned type can result from a previously interrupted migration where `CREATE TYPE` succeeded but the subsequent `ALTER COLUMN` failed and was rolled back without rolling back the type creation. On the next sync attempt, `isEnumUsed` returns `false` (no column references), the code pushes `CREATE TYPE`, and PostgreSQL raises `ERROR: type already exists`.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1725-1743] — replace `isEnumUsed(table, newColumn, newEnumNameRaw, schema!)` with `hasEnumType(table, newColumn)` and invert the condition: `if (!newEnumAlreadyExists)`

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


3. issue-9233.ts missing .test.ts extension 📘 Rule violation ✓ Correctness
Description
The new github-issues test file is named issue-9233.ts instead of issue-9233.test.ts. Every
other file in test/github-issues/ consistently uses the .test.ts suffix, so this file deviates
from the established naming convention.
Code

test/github-issues/9233/issue-9233.ts[1]

+import { DataSource } from "../../../src/data-source/DataSource"
Evidence
All other github-issues test files (e.g., issue-9420.test.ts, issue-9673.test.ts,
issue-8644.test.ts) use the .test.ts suffix. The new file drops this suffix, breaking the
established naming pattern required by compliance rule 1.

Rule 1: Consistent Naming Conventions
test/github-issues/9233/issue-9233.ts[1-1]

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 github-issues test file is missing the `.test.ts` extension that is universally used across the project&amp;#x27;s `test/github-issues/` directory.
## Issue Context
All existing files in `test/github-issues/` follow the pattern `issue-&amp;lt;number&amp;gt;.test.ts`. The new file `issue-9233.ts` omits the `.test` part, which may also prevent the test runner from discovering and executing it automatically.
## Fix Focus Areas
- test/github-issues/9233/issue-9233.ts[1-79] — rename file to `issue-9233.test.ts`

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


View more (5)
4. Empty if (isEnumUsed) body with comment 📘 Rule violation ✓ Correctness
Description
The new code uses an inverted if (isEnumUsed) { // comment } else { ... } pattern, leaving the
if-body empty and putting all logic in else. This is an AI-generated code smell; idiomatic code
would use if (!isEnumUsed) { ... } directly, consistent with the rest of the file.
Code

src/driver/postgres/PostgresQueryRunner.ts[R2646-2655]

+                if (isEnumUsed) {
+                    // if enum is used by other columns we should not drop it
+                } else {
+                    upQueries.push(
+                        this.dropEnumTypeSql(table, column, escapedEnumName),
+                    )
+                    downQueries.push(
+                        this.createEnumTypeSql(table, column, escapedEnumName),
+                    )
+                }
Evidence
Compliance rule 4 prohibits abnormal defensive code and style inconsistent with the file. An empty
if branch used only to hold a justifying comment, with all logic deferred to else, is a pattern
typically produced by AI tools. The same PR correctly uses if (!newEnumIsUsed) and `if
(!isEnumUsed)` elsewhere, making this inverted block a clear inconsistency.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresQueryRunner.ts[2646-2655]

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 `if (isEnumUsed) { // comment } else { ... }` block has an empty `if`-body and places all meaningful logic inside `else`. This is inconsistent with the rest of the file and the rest of this PR.
## Issue Context
The intent is to skip dropping the enum when it is still in use. This should be expressed as a guard (`if (!isEnumUsed) { ... }`) without an empty positive branch, exactly as done at lines 1732 and 1827 of the same file in this PR.
## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[2646-2655]

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


5. checkEnumUsage is dead code 📘 Rule violation ✓ Correctness
Description
The checkEnumUsage method is added to PostgresQueryRunner but is never called anywhere in the
codebase. Introducing an unused method is AI-generated noise that adds unnecessary complexity
alongside the already-used isEnumUsed method.
Code

src/driver/postgres/PostgresQueryRunner.ts[R4614-4621]

+    protected async checkEnumUsage(
+        schema: string,
+        name: string,
+    ): Promise<number> {
+        const sql = `SELECT COUNT("column_name") AS "count" FROM "information_schema"."columns" WHERE "udt_name" IN ($1, $2) AND "table_schema" = $3`
+        const result = await this.query(sql, [name, `_${name}`, schema])
+        return parseInt(result[0].count)
+    }
Evidence
A codebase-wide search for checkEnumUsage finds only the method definition itself and no call
sites, confirming it is dead code. The PR uses isEnumUsed for all enum-in-use checks, making
checkEnumUsage entirely redundant and a violation of compliance rule 4 against AI-generated noise.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresQueryRunner.ts[4614-4621]

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 `checkEnumUsage` method is defined but never invoked anywhere in the codebase. It duplicates the intent already covered by `isEnumUsed` and constitutes dead code.
## Issue Context
All enum-in-use checks introduced by this PR are performed via `isEnumUsed`. `checkEnumUsage` was likely added by an AI assistant as a leftover artefact and should be deleted.
## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4610-4621]

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


6. Non-asserting if/else with AI narration 📘 Rule violation ✓ Correctness
Description
Lines 63–73 of the github-issues test contain an if/else block whose both branches are empty
except for AI-style first-person narration comments ("My fix enables…", "// Success"), providing
zero test coverage for the scenario it describes.
Code

test/github-issues/9233/issue-9233.ts[R63-73]

+                const result2 = await queryRunner.query(
+                    `SELECT count(*) as "count" FROM "pg_type" WHERE "typname" = 'test_enum'`,
+                )
+                if (parseInt(result2[0].count) !== 0) {
+                    // Wait, if 0 usages remaining, we SHOULD drop it ideally
+                    // My fix enables dropping if usage == 1 (before drop).
+                    // So here usage was 1. Drop check passes. Drops.
+                    // So result should be 0.
+                } else {
+                    // Success
+                }
Evidence
Compliance rule 4 prohibits AI-generated comments and abnormal defensive code. A control-flow block
that performs no action and contains first-person AI commentary ("My fix enables dropping if usage
== 1 (before drop).") is a textbook AI-generated artefact. It must be replaced with a real
assertion.

Rule 4: Remove AI-generated noise
test/github-issues/9233/issue-9233.ts[63-73]

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 if/else block at lines 63-73 has no assertions in either branch and contains AI-generated first-person commentary rather than real test logic, making the second half of the test scenario completely unverified.
## Issue Context
The test is verifying that after dropping the last column using a shared enum, the enum itself is also dropped. This should be expressed as a direct assertion on `result2[0].count`, consistent with how the first assertion is written at lines 52-57.
## Fix Focus Areas
- test/github-issues/9233/issue-9233.ts[63-73]

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


7. SQL Injection in isEnumUsed() 🐞 Bug ⛨ Security
Description
The isEnumUsed method directly interpolates schema and enumName into a raw SQL string without
parameterization. A user-controlled schema name or enum name containing single-quotes or SQL
metacharacters can break out of the string literal and execute arbitrary SQL against the database.
Code

src/driver/postgres/PostgresQueryRunner.ts[R4574-4577]

+        const sql =
+            `SELECT "table_name", "column_name" FROM "information_schema"."columns" ` +
+            `WHERE "table_schema" = '${schema}' AND "udt_name" = '${enumName}'`
+        const results = await this.query(sql)
Evidence
The isEnumUsed method interpolates both schema and enumName directly into the SQL string. The
sibling method checkEnumUsage, added in the same PR just below, already demonstrates the correct
parameterized approach. Both schema (from parseTableName) and enumName (from buildEnumNamecolumn.enumName) originate from user-controlled entity metadata.

src/driver/postgres/PostgresQueryRunner.ts[4574-4577]
src/driver/postgres/PostgresQueryRunner.ts[4618-4620]
src/driver/postgres/PostgresQueryRunner.ts[4982-4985]

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 `isEnumUsed` method in `PostgresQueryRunner` interpolates `schema` and `enumName` directly into a SQL string, creating a SQL injection vulnerability. Both values originate from user-controlled entity metadata (schema name, enum name).
## Issue Context
The sibling method `checkEnumUsage` (added in the same PR) already uses the correct parameterized query pattern with `$1, $2, $3` placeholders. The fix is to apply the same pattern to `isEnumUsed`.
## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4574-4577]

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


8. isEnumUsed Filter Breaks Non-Default Schemas 🐞 Bug ✓ Correctness
Description
The filter in isEnumUsed that excludes the current table/column compares `row.table_name ===
table.name, but information_schema.columns.table_name` is always the bare table name while
table.name in TypeORM includes a schema prefix (e.g. "myschema.my_table") for non-default
schemas. The strings never match, so the current column is never excluded, causing isEnumUsed to
always return true. This means the old ENUM type is never dropped after dropColumn, and the
renamed *_old type is permanently left as an orphan after every changeColumn rename on
non-default schemas.
Code

src/driver/postgres/PostgresQueryRunner.ts[R4580-4588]

+        const otherUsages = results.filter(
+            (row: any) =>
+                !(
+                    row.table_name === table.name &&
+                    row.column_name === column.name
+                ),
+        )
+
+        return otherUsages.length > 0
Evidence
buildTableName prepends the schema to produce "schema.tableName" for non-default schemas, and
this compound string is assigned to table.name during table loading. However,
information_schema.columns.table_name always contains only the bare table name. The equality check
therefore always fails for non-default schemas, making isEnumUsed always return true.

src/driver/postgres/PostgresQueryRunner.ts[4580-4588]
src/driver/postgres/PostgresDriver.ts[1037-1044]
src/driver/postgres/PostgresQueryRunner.ts[3677-3692]

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

## Issue description
In `isEnumUsed`, the filter that excludes the current table/column from &amp;#x27;other usages&amp;#x27; compares `row.table_name === table.name`. For non-default schemas, `table.name` contains a schema prefix (e.g. `&amp;quot;myschema.my_table&amp;quot;`), but `information_schema.columns.table_name` is always the bare table name (`&amp;quot;my_table&amp;quot;`). The strings never match, so the current column is never excluded, causing `isEnumUsed` to always return `true` for non-default schemas.
## Issue Context
This causes two downstream bugs:
1. In `dropColumn` (lines 2640–2655): the ENUM type is never dropped even when it should be.
2. In `changeColumn` rename path (lines 1820–1842): the renamed `*_old` type is never dropped, permanently polluting the database schema.
The fix is to use `this.driver.parseTableName(table).tableName` to get the bare table name for the comparison.
## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4580-4586]

ⓘ 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

Grey Divider

Previous review results

Review updated until commit 011554d

Results up to commit 4b411ea


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

Grey Divider
Action required
1. issue-9233.ts missing .test.ts extension 📘 Rule violation ✓ Correctness
Description
The new github-issues test file is named issue-9233.ts instead of issue-9233.test.ts. Every
other file in test/github-issues/ consistently uses the .test.ts suffix, so this file deviates
from the established naming convention.
Code

test/github-issues/9233/issue-9233.ts[1]

+import { DataSource } from "../../../src/data-source/DataSource"
Evidence
All other github-issues test files (e.g., issue-9420.test.ts, issue-9673.test.ts,
issue-8644.test.ts) use the .test.ts suffix. The new file drops this suffix, breaking the
established naming pattern required by compliance rule 1.

Rule 1: Consistent Naming Conventions
test/github-issues/9233/issue-9233.ts[1-1]

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 github-issues test file is missing the `.test.ts` extension that is universally used across the project&#x27;s `test/github-issues/` directory.

## Issue Context
All existing files in `test/github-issues/` follow the pattern `issue-&lt;number&gt;.test.ts`. The new file `issue-9233.ts` omits the `.test` part, which may also prevent the test runner from discovering and executing it automatically.

## Fix Focus Areas
- test/github-issues/9233/issue-9233.ts[1-79] — rename file to `issue-9233.test.ts`

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


2. Empty if (isEnumUsed) body with comment 📘 Rule violation ✓ Correctness
Description
The new code uses an inverted if (isEnumUsed) { // comment } else { ... } pattern, leaving the
if-body empty and putting all logic in else. This is an AI-generated code smell; idiomatic code
would use if (!isEnumUsed) { ... } directly, consistent with the rest of the file.
Code

src/driver/postgres/PostgresQueryRunner.ts[R2646-2655]

+                if (isEnumUsed) {
+                    // if enum is used by other columns we should not drop it
+                } else {
+                    upQueries.push(
+                        this.dropEnumTypeSql(table, column, escapedEnumName),
+                    )
+                    downQueries.push(
+                        this.createEnumTypeSql(table, column, escapedEnumName),
+                    )
+                }
Evidence
Compliance rule 4 prohibits abnormal defensive code and style inconsistent with the file. An empty
if branch used only to hold a justifying comment, with all logic deferred to else, is a pattern
typically produced by AI tools. The same PR correctly uses if (!newEnumIsUsed) and `if
(!isEnumUsed)` elsewhere, making this inverted block a clear inconsistency.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresQueryRunner.ts[2646-2655]

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 `if (isEnumUsed) { // comment } else { ... }` block has an empty `if`-body and places all meaningful logic inside `else`. This is inconsistent with the rest of the file and the rest of this PR.

## Issue Context
The intent is to skip dropping the enum when it is still in use. This should be expressed as a guard (`if (!isEnumUsed) { ... }`) without an empty positive branch, exactly as done at lines 1732 and 1827 of the same file in this PR.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[2646-2655]

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


3. checkEnumUsage is dead code 📘 Rule violation ✓ Correctness
Description
The checkEnumUsage method is added to PostgresQueryRunner but is never called anywhere in the
codebase. Introducing an unused method is AI-generated noise that adds unnecessary complexity
alongside the already-used isEnumUsed method.
Code

src/driver/postgres/PostgresQueryRunner.ts[R4614-4621]

+    protected async checkEnumUsage(
+        schema: string,
+        name: string,
+    ): Promise<number> {
+        const sql = `SELECT COUNT("column_name") AS "count" FROM "information_schema"."columns" WHERE "udt_name" IN ($1, $2) AND "table_schema" = $3`
+        const result = await this.query(sql, [name, `_${name}`, schema])
+        return parseInt(result[0].count)
+    }
Evidence
A codebase-wide search for checkEnumUsage finds only the method definition itself and no call
sites, confirming it is dead code. The PR uses isEnumUsed for all enum-in-use checks, making
checkEnumUsage entirely redundant and a violation of compliance rule 4 against AI-generated noise.

Rule 4: Remove AI-generated noise
src/driver/postgres/PostgresQueryRunner.ts[4614-4621]

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 `checkEnumUsage` method is defined but never invoked anywhere in the codebase. It duplicates the intent already covered by `isEnumUsed` and constitutes dead code.

## Issue Context
All enum-in-use checks introduced by this PR are performed via `isEnumUsed`. `checkEnumUsage` was likely added by an AI assistant as a leftover artefact and should be deleted.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4610-4621]

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


View more (3)
4. Non-asserting if/else with AI narration 📘 Rule violation ✓ Correctness
Description
Lines 63–73 of the github-issues test contain an if/else block whose both branches are empty
except for AI-style first-person narration comments ("My fix enables…", "// Success"), providing
zero test coverage for the scenario it describes.
Code

test/github-issues/9233/issue-9233.ts[R63-73]

+                const result2 = await queryRunner.query(
+                    `SELECT count(*) as "count" FROM "pg_type" WHERE "typname" = 'test_enum'`,
+                )
+                if (parseInt(result2[0].count) !== 0) {
+                    // Wait, if 0 usages remaining, we SHOULD drop it ideally
+                    // My fix enables dropping if usage == 1 (before drop).
+                    // So here usage was 1. Drop check passes. Drops.
+                    // So result should be 0.
+                } else {
+                    // Success
+                }
Evidence
Compliance rule 4 prohibits AI-generated comments and abnormal defensive code. A control-flow block
that performs no action and contains first-person AI commentary ("My fix enables dropping if usage
== 1 (before drop).") is a textbook AI-generated artefact. It must be replaced with a real
assertion.

Rule 4: Remove AI-generated noise
test/github-issues/9233/issue-9233.ts[63-73]

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 if/else block at lines 63-73 has no assertions in either branch and contains AI-generated first-person commentary rather than real test logic, making the second half of the test scenario completely unverified.

## Issue Context
The test is verifying that after dropping the last column using a shared enum, the enum itself is also dropped. This should be expressed as a direct assertion on `result2[0].count`, consistent with how the first assertion is written at lines 52-57.

## Fix Focus Areas
- test/github-issues/9233/issue-9233.ts[63-73]

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


5. SQL Injection in isEnumUsed() 🐞 Bug ⛨ Security
Description
The isEnumUsed method directly interpolates schema and enumName into a raw SQL string without
parameterization. A user-controlled schema name or enum name containing single-quotes or SQL
metacharacters can break out of the string literal and execute arbitrary SQL against the database.
Code

src/driver/postgres/PostgresQueryRunner.ts[R4574-4577]

+        const sql =
+            `SELECT "table_name", "column_name" FROM "information_schema"."columns" ` +
+            `WHERE "table_schema" = '${schema}' AND "udt_name" = '${enumName}'`
+        const results = await this.query(sql)
Evidence
The isEnumUsed method interpolates both schema and enumName directly into the SQL string. The
sibling method checkEnumUsage, added in the same PR just below, already demonstrates the correct
parameterized approach. Both schema (from parseTableName) and enumName (from buildEnumNamecolumn.enumName) originate from user-controlled entity metadata.

src/driver/postgres/PostgresQueryRunner.ts[4574-4577]
src/driver/postgres/PostgresQueryRunner.ts[4618-4620]
src/driver/postgres/PostgresQueryRunner.ts[4982-4985]

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 `isEnumUsed` method in `PostgresQueryRunner` interpolates `schema` and `enumName` directly into a SQL string, creating a SQL injection vulnerability. Both values originate from user-controlled entity metadata (schema name, enum name).

## Issue Context
The sibling method `checkEnumUsage` (added in the same PR) already uses the correct parameterized query pattern with `$1, $2, $3` placeholders. The fix is to apply the same pattern to `isEnumUsed`.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4574-4577]

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


6. isEnumUsed Filter Breaks Non-Default Schemas 🐞 Bug ✓ Correctness
Description
The filter in isEnumUsed that excludes the current table/column compares `row.table_name ===
table.name, but information_schema.columns.table_name` is always the bare table name while
table.name in TypeORM includes a schema prefix (e.g. "myschema.my_table") for non-default
schemas. The strings never match, so the current column is never excluded, causing isEnumUsed to
always return true. This means the old ENUM type is never dropped after dropColumn, and the
renamed *_old type is permanently left as an orphan after every changeColumn rename on
non-default schemas.
Code

src/driver/postgres/PostgresQueryRunner.ts[R4580-4588]

+        const otherUsages = results.filter(
+            (row: any) =>
+                !(
+                    row.table_name === table.name &&
+                    row.column_name === column.name
+                ),
+        )
+
+        return otherUsages.length > 0
Evidence
buildTableName prepends the schema to produce "schema.tableName" for non-default schemas, and
this compound string is assigned to table.name during table loading. However,
information_schema.columns.table_name always contains only the bare table name. The equality check
therefore always fails for non-default schemas, making isEnumUsed always return true.

src/driver/postgres/PostgresQueryRunner.ts[4580-4588]
src/driver/postgres/PostgresDriver.ts[1037-1044]
src/driver/postgres/PostgresQueryRunner.ts[3677-3692]

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

## Issue description
In `isEnumUsed`, the filter that excludes the current table/column from &#x27;other usages&#x27; compares `row.table_name === table.name`. For non-default schemas, `table.name` contains a schema prefix (e.g. `&quot;myschema.my_table&quot;`), but `information_schema.columns.table_name` is always the bare table name (`&quot;my_table&quot;`). The strings never match, so the current column is never excluded, causing `isEnumUsed` to always return `true` for non-default schemas.

## Issue Context
This causes two downstream bugs:
1. In `dropColumn` (lines 2640–2655): the ENUM type is never dropped even when it should be.
2. In `changeColumn` rename path (lines 1820–1842): the renamed `*_old` type is never dropped, permanently polluting the database schema.

The fix is to use `this.driver.parseTableName(table).tableName` to get the bare table name for the comparison.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4580-4586]

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


Grey Divider Grey Divider

Qodo Logo

@@ -0,0 +1,79 @@
import { DataSource } from "../../../src/data-source/DataSource"

Choose a reason for hiding this comment

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

Action required

1. issue-9233.ts missing .test.ts extension 📘 Rule violation ✓ Correctness

The new github-issues test file is named issue-9233.ts instead of issue-9233.test.ts. Every
other file in test/github-issues/ consistently uses the .test.ts suffix, so this file deviates
from the established naming convention.
Agent Prompt
## Issue description
The new github-issues test file is missing the `.test.ts` extension that is universally used across the project's `test/github-issues/` directory.

## Issue Context
All existing files in `test/github-issues/` follow the pattern `issue-<number>.test.ts`. The new file `issue-9233.ts` omits the `.test` part, which may also prevent the test runner from discovering and executing it automatically.

## Fix Focus Areas
- test/github-issues/9233/issue-9233.ts[1-79] — rename file to `issue-9233.test.ts`

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

Comment on lines +2646 to +2655
if (isEnumUsed) {
// if enum is used by other columns we should not drop it
} else {
upQueries.push(
this.dropEnumTypeSql(table, column, escapedEnumName),
)
downQueries.push(
this.createEnumTypeSql(table, column, escapedEnumName),
)
}

Choose a reason for hiding this comment

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

Action required

2. Empty if (isenumused) body with comment 📘 Rule violation ✓ Correctness

The new code uses an inverted if (isEnumUsed) { // comment } else { ... } pattern, leaving the
if-body empty and putting all logic in else. This is an AI-generated code smell; idiomatic code
would use if (!isEnumUsed) { ... } directly, consistent with the rest of the file.
Agent Prompt
## Issue description
The `if (isEnumUsed) { // comment } else { ... }` block has an empty `if`-body and places all meaningful logic inside `else`. This is inconsistent with the rest of the file and the rest of this PR.

## Issue Context
The intent is to skip dropping the enum when it is still in use. This should be expressed as a guard (`if (!isEnumUsed) { ... }`) without an empty positive branch, exactly as done at lines 1732 and 1827 of the same file in this PR.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[2646-2655]

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

Comment on lines +4614 to +4621
protected async checkEnumUsage(
schema: string,
name: string,
): Promise<number> {
const sql = `SELECT COUNT("column_name") AS "count" FROM "information_schema"."columns" WHERE "udt_name" IN ($1, $2) AND "table_schema" = $3`
const result = await this.query(sql, [name, `_${name}`, schema])
return parseInt(result[0].count)
}

Choose a reason for hiding this comment

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

Action required

3. checkenumusage is dead code 📘 Rule violation ✓ Correctness

The checkEnumUsage method is added to PostgresQueryRunner but is never called anywhere in the
codebase. Introducing an unused method is AI-generated noise that adds unnecessary complexity
alongside the already-used isEnumUsed method.
Agent Prompt
## Issue description
The `checkEnumUsage` method is defined but never invoked anywhere in the codebase. It duplicates the intent already covered by `isEnumUsed` and constitutes dead code.

## Issue Context
All enum-in-use checks introduced by this PR are performed via `isEnumUsed`. `checkEnumUsage` was likely added by an AI assistant as a leftover artefact and should be deleted.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4610-4621]

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

Comment on lines +63 to +73
const result2 = await queryRunner.query(
`SELECT count(*) as "count" FROM "pg_type" WHERE "typname" = 'test_enum'`,
)
if (parseInt(result2[0].count) !== 0) {
// Wait, if 0 usages remaining, we SHOULD drop it ideally
// My fix enables dropping if usage == 1 (before drop).
// So here usage was 1. Drop check passes. Drops.
// So result should be 0.
} else {
// Success
}

Choose a reason for hiding this comment

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

Action required

4. Non-asserting if/else with ai narration 📘 Rule violation ✓ Correctness

Lines 63–73 of the github-issues test contain an if/else block whose both branches are empty
except for AI-style first-person narration comments ("My fix enables…", "// Success"), providing
zero test coverage for the scenario it describes.
Agent Prompt
## Issue description
The if/else block at lines 63-73 has no assertions in either branch and contains AI-generated first-person commentary rather than real test logic, making the second half of the test scenario completely unverified.

## Issue Context
The test is verifying that after dropping the last column using a shared enum, the enum itself is also dropped. This should be expressed as a direct assertion on `result2[0].count`, consistent with how the first assertion is written at lines 52-57.

## Fix Focus Areas
- test/github-issues/9233/issue-9233.ts[63-73]

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

Comment on lines +4574 to +4577
const sql =
`SELECT "table_name", "column_name" FROM "information_schema"."columns" ` +
`WHERE "table_schema" = '${schema}' AND "udt_name" = '${enumName}'`
const results = await this.query(sql)

Choose a reason for hiding this comment

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

Action required

5. Sql injection in isenumused() 🐞 Bug ⛨ Security

The isEnumUsed method directly interpolates schema and enumName into a raw SQL string without
parameterization. A user-controlled schema name or enum name containing single-quotes or SQL
metacharacters can break out of the string literal and execute arbitrary SQL against the database.
Agent Prompt
## Issue description
The `isEnumUsed` method in `PostgresQueryRunner` interpolates `schema` and `enumName` directly into a SQL string, creating a SQL injection vulnerability. Both values originate from user-controlled entity metadata (schema name, enum name).

## Issue Context
The sibling method `checkEnumUsage` (added in the same PR) already uses the correct parameterized query pattern with `$1, $2, $3` placeholders. The fix is to apply the same pattern to `isEnumUsed`.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4574-4577]

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

Comment on lines +4580 to +4588
const otherUsages = results.filter(
(row: any) =>
!(
row.table_name === table.name &&
row.column_name === column.name
),
)

return otherUsages.length > 0

Choose a reason for hiding this comment

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

Action required

6. Isenumused filter breaks non-default schemas 🐞 Bug ✓ Correctness

The filter in isEnumUsed that excludes the current table/column compares `row.table_name ===
table.name, but information_schema.columns.table_name` is always the bare table name while
table.name in TypeORM includes a schema prefix (e.g. "myschema.my_table") for non-default
schemas. The strings never match, so the current column is never excluded, causing isEnumUsed to
always return true. This means the old ENUM type is never dropped after dropColumn, and the
renamed *_old type is permanently left as an orphan after every changeColumn rename on
non-default schemas.
Agent Prompt
## Issue description
In `isEnumUsed`, the filter that excludes the current table/column from 'other usages' compares `row.table_name === table.name`. For non-default schemas, `table.name` contains a schema prefix (e.g. `"myschema.my_table"`), but `information_schema.columns.table_name` is always the bare table name (`"my_table"`). The strings never match, so the current column is never excluded, causing `isEnumUsed` to always return `true` for non-default schemas.

## Issue Context
This causes two downstream bugs:
1. In `dropColumn` (lines 2640–2655): the ENUM type is never dropped even when it should be.
2. In `changeColumn` rename path (lines 1820–1842): the renamed `*_old` type is never dropped, permanently polluting the database schema.

The fix is to use `this.driver.parseTableName(table).tableName` to get the bare table name for the comparison.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[4580-4586]

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

Problem:
Synchronizing PostgreSQL ENUM types shared across multiple columns or tables could:
- Attempt to re-create or drop ENUM types still in use
- Fail when adding values that already exist
- Mis-handle shared ENUM renaming
These issues caused errors during migrations or schema sync.

Root Cause:
Schema builder did not properly handle:
- Reuse of ENUM types across multiple metadata entries
- Existing ENUM values in the database
- Dependency tracking of tables referencing a shared ENUM

Solution:
- Prevent dropping ENUM types still referenced by other columns
- Use ALTER TYPE ... ADD VALUE IF NOT EXISTS (Postgres 12+)
- Improve detection logic for shared ENUM reuse and renaming
- Ensure idempotent ENUM modifications during schema synchronization

Tests:
Added functional tests covering:
- Shared ENUM reuse
- Adding existing enum values
- ENUM renaming scenarios

Closes typeorm#9233
@baszczewski baszczewski force-pushed the fix/enum-name-recreated branch from 4b411ea to 011554d Compare February 18, 2026 10:24
@qodo-free-for-open-source-projects

Persistent review updated to latest commit 011554d

Comment on lines 1706 to 1708
// rename old ENUM
upQueries.push(
new Query(

Choose a reason for hiding this comment

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

Action required

1. Shared enum rename corrupts other columns 🐞 Bug ✓ Correctness

In the else branch of changeColumn, the old enum is unconditionally renamed to _old via `ALTER
TYPE ... RENAME TO`, which is a global PostgreSQL operation. When the enum is shared across multiple
tables/columns, this rename changes the type name as seen by ALL referencing columns. On the next
synchronize(), TypeORM reads those unrelated columns as having type old_enum_old while entity
definitions declare old_enum, triggering an infinite loop of schema changes.
Agent Prompt
## Issue description
In `changeColumn`, the `else` branch (triggered when enum values are removed or the enum name changes) unconditionally renames the old enum type to `_old`. Because `ALTER TYPE ... RENAME TO` is a global PostgreSQL operation, this rename affects ALL columns in ALL tables that reference the same enum type. When the enum is shared, subsequent calls to `synchronize()` detect the renamed type as a schema drift and attempt another `changeColumn`, creating an infinite loop.

## Issue Context
The `isEnumUsed` check was added to guard the DROP of the renamed type, but the rename itself must also be guarded. When the old enum is shared, the rename must be skipped to preserve the type name for other referencing columns.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1706-1716] — wrap the rename `upQueries.push` and its corresponding `downQueries.push` inside an `if (!isOldEnumShared)` block, where `isOldEnumShared` is computed via `isEnumUsed(table, oldColumn, oldEnumNameWithoutSchemaRaw, schema!)` before the rename is attempted
- src/driver/postgres/PostgresQueryRunner.ts[1820-1842] — the existing `isEnumUsed` guard for the drop can remain, but must be consistent with the rename guard above
- test/functional/database-schema/column-types/postgres-enum-shared/postgres-enum-shared.test.ts[251-260] — add a second `synchronize()` call after the first and assert that no further queries are generated (idempotency check)

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

Comment on lines +1725 to +1743
const newEnumIsUsed = await this.isEnumUsed(
table,
newColumn,
newEnumNameRaw,
schema!,
)

if (!newEnumIsUsed) {
upQueries.push(
this.createEnumTypeSql(
table,
newColumn,
newEnumName,
),
)
downQueries.push(
this.dropEnumTypeSql(table, newColumn, newEnumName),
)
}

Choose a reason for hiding this comment

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

Action required

2. isenumused wrong guard for new enum creation 🐞 Bug ⛯ Reliability

isEnumUsed checks information_schema.columns for column references to the type, but does NOT
check whether the type itself exists in pg_type. If the new enum type already exists in pg_type
with no column references (an orphaned type from a previously interrupted migration), isEnumUsed
returns false, the code pushes CREATE TYPE, and the query fails at runtime with `ERROR: type
already exists. The correct guard is hasEnumType`, which checks type existence in
pg_type/pg_namespace.
Agent Prompt
## Issue description
The guard before `createEnumTypeSql` uses `isEnumUsed`, which queries `information_schema.columns` for column references. This does not detect an orphaned type (one that exists in `pg_type` but has no column assignments). The correct check is `hasEnumType`, which queries `pg_type JOIN pg_namespace` for type existence.

## Issue Context
An orphaned type can result from a previously interrupted migration where `CREATE TYPE` succeeded but the subsequent `ALTER COLUMN` failed and was rolled back without rolling back the type creation. On the next sync attempt, `isEnumUsed` returns `false` (no column references), the code pushes `CREATE TYPE`, and PostgreSQL raises `ERROR: type already exists`.

## Fix Focus Areas
- src/driver/postgres/PostgresQueryRunner.ts[1725-1743] — replace `isEnumUsed(table, newColumn, newEnumNameRaw, schema!)` with `hasEnumType(table, newColumn)` and invert the condition: `if (!newEnumAlreadyExists)`

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

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.

Postgres: Enum with explicit default enumName is recreated in every migration

1 participant