Skip to content

fix(sqlserver): use ALTER COLUMN instead of DROP+ADD for type/length changes (#3357)#12258

Open
DeekRoumy wants to merge 2 commits intotypeorm:masterfrom
DeekRoumy:fix/issue-3357-preserve-data-on-column-change
Open

fix(sqlserver): use ALTER COLUMN instead of DROP+ADD for type/length changes (#3357)#12258
DeekRoumy wants to merge 2 commits intotypeorm:masterfrom
DeekRoumy:fix/issue-3357-preserve-data-on-column-change

Conversation

@DeekRoumy
Copy link
Copy Markdown

Summary

Fixes #3357

When a column's type or length changes in SQL Server, TypeORM was generating destructive SQL:

ALTER TABLE [bug] DROP COLUMN [example]
ALTER TABLE [bug] ADD [example] varchar(51)

This causes data loss. SQL Server supports ALTER TABLE ... ALTER COLUMN for type/length changes:

ALTER TABLE [bug] ALTER COLUMN [example] varchar(51) NOT NULL

What this PR does

  • SQL Server / MSSQL: Uses ALTER COLUMN ... new_type for type/length changes, preserving data
  • Properly handles DEFAULT constraints: SQL Server requires dropping the default constraint before ALTER COLUMN and re-adding it after
  • DROP + ADD is only used for truly incompatible changes:
    • Changing IDENTITY property (SQL Server limitation)
    • Changing computed column expressions (asExpression / generatedType)

Relationship to PR #12218

PR #12218 addresses the same issue for PostgreSQL, MySQL, Aurora MySQL, CockroachDB, Oracle, and Spanner. This PR extends the same data-safe fix to SQL Server (MSSQL) which was not covered.

Test plan

  • Added test/github-issues/3357/ with test entity and integration test
  • Tests verify data is preserved after column length change
  • Tests verify column definition is updated correctly

What stays the same

  • IDENTITY column changes → DROP + ADD (SQL Server limitation)
  • Computed column expression changes → DROP + ADD
  • Column renames, nullable, default, primary key changes → unchanged (handled by existing code below)
  • SQLite → unchanged (table recreation is the only approach)

/claim #3357

…changes (typeorm#3357)

Fixes typeorm#3357

SQL Server supports ALTER TABLE ... ALTER COLUMN for changing column
type and length, which preserves data. Previously, TypeORM was using
DROP COLUMN + ADD COLUMN even for simple type/length changes, causing
data loss.

Changes:
- Only drop+recreate when truly required: IDENTITY property changes or
  computed column (asExpression/generatedType) changes
- For type/length changes: use ALTER COLUMN with proper handling of
  DEFAULT constraints (which must be dropped and re-added around an
  ALTER COLUMN in SQL Server)
- Name changes, nullable, and other properties continue to work
  correctly since they run in the same transaction

Builds on the same approach taken for PostgreSQL/MySQL/CockroachDB,
extending data-safe column alterations to SQL Server.

Note: The competing PR typeorm#12218 covers Postgres/MySQL/Aurora/CockroachDB/
Oracle/Spanner. This PR extends the same fix to SQL Server (MSSQL).
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Fix SQL Server column type changes to preserve data

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Prevents data loss when changing SQL Server column type/length
• Uses ALTER COLUMN instead of destructive DROP+ADD operations
• Properly handles DEFAULT constraints around ALTER COLUMN operations
• Preserves existing behavior for IDENTITY and computed column changes
Diagram
flowchart LR
  A["Column Type/Length Change"] --> B{Change Type}
  B -->|IDENTITY or Computed| C["DROP + ADD Column"]
  B -->|Type/Length Only| D["ALTER COLUMN"]
  D --> E["Drop DEFAULT Constraint"]
  E --> F["Execute ALTER COLUMN"]
  F --> G["Restore DEFAULT Constraint"]
  C --> H["Data Preserved"]
  G --> H
Loading

Grey Divider

File Changes

1. src/driver/sqlserver/SqlServerQueryRunner.ts 🐞 Bug fix +92/-6

Implement ALTER COLUMN for SQL Server type changes

• Refactored column change logic to distinguish between destructive changes and type/length changes
• Added requiresRecreation flag for IDENTITY and computed column changes only
• Implemented ALTER COLUMN path with DEFAULT constraint handling for type/length changes
• Properly drops and restores DEFAULT constraints before and after ALTER COLUMN operations

src/driver/sqlserver/SqlServerQueryRunner.ts


2. test/github-issues/3357/entity/TestEntity.ts 🧪 Tests +10/-0

Add test entity for issue 3357

• Created test entity with id and name columns
• Name column has varchar(50) length constraint for testing length changes

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


3. test/github-issues/3357/issue-3357.ts 🧪 Tests +73/-0

Add integration test for column alteration

• Added integration test verifying data preservation on column length changes
• Tests ALTER COLUMN behavior across postgres, mysql, mssql, and cockroachdb
• Validates that existing data is preserved after column alteration
• Confirms column definition is updated to new length specification

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


Grey Divider

Qodo Logo

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

qodo-free-for-open-source-projects bot commented Mar 24, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Default constraint double-handled 🐞 Bug ✓ Correctness ⭐ New
Description
SqlServerQueryRunner.changeColumn() now drops/re-adds DEFAULT constraints inside the new
typeOrLengthChanged branch, but the method still also performs default/rename-default operations
later. When type/length changes are combined with default addition/removal/change, this produces
invalid SQL (e.g., dropping the same constraint twice or adding an already-existing constraint),
breaking schema sync/migration generation.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1367-1451]

+            if (typeOrLengthChanged) {
+                // Use ALTER COLUMN to preserve data instead of DROP + ADD
+                // SQL Server supports: ALTER TABLE t ALTER COLUMN col_name new_type [NULL | NOT NULL]
+                // Drop default constraint first if one exists (SQL Server requires this before ALTER COLUMN)
+                if (
+                    oldColumn.default !== null &&
+                    oldColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            oldColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} DROP CONSTRAINT "${defaultName}"`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${oldColumn.default} FOR "${oldColumn.name}"`,
+                        ),
+                    )
+                }
+
+                // Build ALTER COLUMN using the current (old) column name since rename hasn't run yet
+                const alterColumnForType = newColumn.clone()
+                alterColumnForType.name = oldColumn.name
+
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            alterColumnForType,
+                            true,
+                            false,
+                            true,
+                        )}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            oldColumn,
+                            true,
+                            false,
+                            true,
+                        )}`,
+                    ),
+                )
+
+                // Restore default constraint after type change (use old name — rename hasn't run yet)
+                if (
+                    newColumn.default !== null &&
+                    newColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            oldColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${newColumn.default} FOR "${oldColumn.name}"`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} DROP CONSTRAINT "${defaultName}"`,
+                        ),
+                    )
+                }
Evidence
In the new type/length-change path, the code unconditionally emits DEFAULT constraint DDL around
ALTER COLUMN (drop old default if present; add new default if present). Later in the same method,
defaults are still handled again via the existing if (newColumn.default !== oldColumn.default)
block. This creates concrete failing sequences:

- **Default removed + type/length changed**: the new branch drops the old default (1367-1394) and
does not re-add it; later, the existing default-change block sees `newColumn.default !==
oldColumn.default` and tries to drop the old default constraint again (2006-2023), which will fail
because it was already dropped.
- **Default added + type/length changed**: the new branch adds the new default constraint
(1427-1451) and later the existing default-change block will add it again (2035-2052), failing
because the constraint already exists.

Additionally, the rename-default-constraints logic runs based on oldColumn.default (1710-1759) and
assumes the old default constraint still exists; with the new early drop in the type/length branch,
rename-time drop/add can target a constraint that was already removed, or reintroduce an old default
even when the new schema removes it (until later blocks attempt to correct it).

src/driver/sqlserver/SqlServerQueryRunner.ts[1335-1452]
src/driver/sqlserver/SqlServerQueryRunner.ts[1688-1759]
src/driver/sqlserver/SqlServerQueryRunner.ts[2006-2061]

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

## Issue description
`SqlServerQueryRunner.changeColumn()` now manages DEFAULT constraints in two separate places:
1) inside the new `typeOrLengthChanged` ALTER COLUMN path
2) later via the existing default-change and rename-default logic

When type/length changes are combined with default add/remove/change, SQL can attempt to DROP or ADD the same default constraint twice, causing SQL Server errors.

## Issue Context
SQL Server requires dropping DEFAULT constraints before `ALTER COLUMN`, but the drop/re-add needs to be coordinated with existing logic that already handles default changes and rename-time default constraint updates.

## Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1451]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1710-1759]
- src/driver/sqlserver/SqlServerQueryRunner.ts[2006-2061]

## Implementation notes
- Ensure the old default constraint is dropped exactly once before `ALTER COLUMN`.
- Ensure the default constraint is added exactly once after the type/length change, in a way that correctly handles:
 - default unchanged (drop+re-add same default)
 - default removed (drop only; do not re-add)
 - default added/changed (drop old if existed, then add new)
- Avoid rename-default operations assuming a dropped constraint still exists; either:
 - defer default re-creation until after rename/default-change logic runs, or
 - track a flag like `droppedDefaultForAlterColumn` and conditionally skip later drops/adds that would duplicate work.

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


2. Test added under github-issues 📘 Rule violation ⚙ Maintainability
Description
This PR adds the regression test only under test/github-issues/3357 rather than the functional
test suite, without any clear justification in the code. This violates the guideline to keep issue
fixes in test/functional to avoid per-issue test sprawl.
Code

test/github-issues/3357/issue-3357.ts[R10-73]

+describe("github issues > #3357 migration generation drops and creates columns instead of altering", () => {
+    let connections: DataSource[]
+
+    before(async () => {
+        connections = await createTestingConnections({
+            entities: [__dirname + "/entity/*{.js,.ts}"],
+            enabledDrivers: ["postgres", "mysql", "mssql", "cockroachdb"],
+            schemaCreate: true,
+            dropSchema: true,
+        })
+    })
+    beforeEach(() => reloadTestingDatabases(connections))
+    after(() => closeTestingConnections(connections))
+
+    it("should use ALTER COLUMN instead of DROP+ADD when changing column length to preserve data", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                const queryRunner = connection.createQueryRunner()
+
+                try {
+                    // Insert test data
+                    await queryRunner.manager.query(
+                        `INSERT INTO "test_entity" ("name") VALUES ('test-value')`,
+                    )
+
+                    const table = await queryRunner.getTable("test_entity")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change length from 50 to 100
+                    const newColumn = nameColumn.clone()
+                    newColumn.length = "100"
+
+                    const sqlsInMemory =
+                        await queryRunner.getMemorySql()
+                    sqlsInMemory.upQueries = []
+                    sqlsInMemory.downQueries = []
+
+                    await queryRunner.changeColumn(table!, nameColumn, newColumn)
+
+                    // Verify no DROP COLUMN was generated
+                    const allSqls = [
+                        ...sqlsInMemory.upQueries,
+                    ]
+                        .map((q) => q.query)
+                        .join(" ")
+
+                    // allSqls.should.not.contain("DROP COLUMN") - this is hard to test without spying
+                    // Instead verify data is preserved
+                    const rows = await queryRunner.query(
+                        `SELECT "name" FROM "test_entity"`,
+                    )
+                    rows.should.have.length(1)
+                    rows[0].name.should.equal("test-value")
+
+                    // Verify the column was actually changed
+                    const updatedTable = await queryRunner.getTable("test_entity")
+                    const updatedColumn = updatedTable!.findColumnByName("name")!
+                    updatedColumn.length.should.equal("100")
+                } finally {
+                    await queryRunner.release()
+                }
+            }),
+        ))
+})
Evidence
Compliance ID 3 requires issue fix tests to be added/updated in test/functional and considers it a
failure when tests are added only under test/github-issues without a clear reason; this PR
introduces only test/github-issues/3357/issue-3357.ts for the fix.

Rule 3: Prefer functional tests over per-issue tests
test/github-issues/3357/issue-3357.ts[10-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 regression test for #3357 was added under `test/github-issues/3357`, but compliance requires issue fixes to live in the functional test suite unless there is a clear reason.
## Issue Context
Per-project compliance, prefer `test/functional` coverage to avoid accumulating per-issue tests.
## Fix Focus Areas
- test/github-issues/3357/issue-3357.ts[10-73]

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


3. Unused TableColumn import📘 Rule violation ⚙ Maintainability
Description
The new test file imports TableColumn but never uses it, adding noise and potentially failing
linting. This violates the requirement to avoid unnecessary/AI-like clutter in changes.
Code

test/github-issues/3357/issue-3357.ts[R2-3]

+import { DataSource } from "../../../src/data-source/DataSource"
+import { TableColumn } from "../../../src/schema-builder/table/TableColumn"
Evidence
Compliance ID 4 requires removing AI-generated noise and avoiding unnecessary additions;
TableColumn is newly imported but not referenced anywhere in the added test file.

Rule 4: Remove AI-generated noise
test/github-issues/3357/issue-3357.ts[2-3]

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

## Issue description
`TableColumn` is imported but never used in the new test.
## Issue Context
This adds noise and may fail lint/ts checks.
## Fix Focus Areas
- test/github-issues/3357/issue-3357.ts[2-3]

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


View more (3)
4. Enum ALTER COLUMN invalid🐞 Bug ✓ Correctness
Description
SqlServerQueryRunner.changeColumn uses buildCreateColumnSql() without skipEnum in an ALTER COLUMN
statement, which can inline a CONSTRAINT ... CHECK(...) clause and produce invalid SQL Server
syntax.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1398-1405]

+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            newColumn,
+                            true,
+                            false,
+                        )}`,
Evidence
In the new type/length-change path, ALTER COLUMN is constructed using buildCreateColumnSql(table,
newColumn, true, false) (skipEnum not set). buildCreateColumnSql appends an enum CHECK constraint
when column.enum is defined unless skipEnum=true, but SQL Server ALTER COLUMN cannot include
constraint definitions. The existing (unchanged) ALTER COLUMN path later in changeColumn explicitly
passes skipEnum=true, showing the intended usage pattern.

src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1420]
src/driver/sqlserver/SqlServerQueryRunner.ts[4306-4339]
src/driver/sqlserver/SqlServerQueryRunner.ts[4317-4326]
src/driver/sqlserver/SqlServerQueryRunner.ts[1771-1799]

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

## Issue description
`changeColumn` builds an `ALTER TABLE ... ALTER COLUMN` statement via `buildCreateColumnSql` without passing `skipEnum=true`. For enum columns this inlines a `CONSTRAINT ... CHECK(...)` fragment, which is not valid in SQL Server `ALTER COLUMN` syntax.
### Issue Context
`buildCreateColumnSql` appends enum `CHECK` constraints unless the `skipEnum` parameter is set.
### Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1419]
- src/driver/sqlserver/SqlServerQueryRunner.ts[4306-4326]
### What to change
- Update both up/down ALTER COLUMN calls in the new `typeOrLengthChanged` block to pass the 5th argument `true` (skipEnum), matching the existing ALTER COLUMN call later in the method:
- `this.buildCreateColumnSql(table, newColumn, true, false, true)`
- `this.buildCreateColumnSql(table, oldColumn, true, false, true)`

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


5. Rename/type order mismatch🐞 Bug ✓ Correctness
Description
When a column is renamed and its type/length changes in the same changeColumn call, the new ALTER
COLUMN/default-restore SQL uses newColumn.name before the sp_rename runs, so it can reference a
non-existent column name and fail.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1421-1436]

+                // Restore default constraint after type change
+                if (
+                    newColumn.default !== null &&
+                    newColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            newColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${newColumn.default} FOR "${newColumn.name}"`,
+                        ),
Evidence
The type/length-change block executes before the rename block. Inside it, the code restores the
default using FOR "${newColumn.name}", but the rename (EXEC sp_rename ...) only happens
afterward; at that point the physical column name is still oldColumn.name. This makes the generated
SQL incorrect for combined rename+type/length changes.

src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1446]
src/driver/sqlserver/SqlServerQueryRunner.ts[1448-1483]

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 `SqlServerQueryRunner.changeColumn`, the new `typeOrLengthChanged` block runs before the rename logic. It currently emits `ALTER COLUMN` and restores default constraints using `newColumn.name`, which is wrong if the column will be renamed later in the same call.
### Issue Context
The rename is performed via `sp_rename` in the `if (newColumn.name !== oldColumn.name)` block, which runs after the new ALTER COLUMN block.
### Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1446]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1448-1483]
### What to change
- Ensure the ALTER COLUMN statement targets the *current* column name at execution time:
- Build an "alter column" TableColumn based on `newColumn` but with `name = oldColumn.name`, and use that in `buildCreateColumnSql`.
- When restoring the default inside the type/length block, use `oldColumn.name` in the `FOR` clause (and consider using the old default constraint name until after the rename/default-rename logic runs).
- Keep/verify the later default-constraint rename logic still works after these adjustments.

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


6. MySQL test quoting breaks🐞 Bug ✓ Correctness
Description
The new test uses double-quoted identifiers in raw SQL while running against MySQL, which by default
expects backticks and may reject the queries.
Code

test/github-issues/3357/issue-3357.ts[R30-33]

+                    // Insert test data
+                    await queryRunner.manager.query(
+                        `INSERT INTO "test_entity" ("name") VALUES ('test-value')`,
+                    )
Evidence
The test runs with enabledDrivers including "mysql" and executes raw SQL strings containing
double-quoted identifiers (e.g., "test_entity"). The MySQL driver’s identifier escaping uses
backticks, and the sample test configuration does not set any MySQL sql_mode that would make double
quotes behave as identifier quotes, making the test likely to fail on MySQL.

test/github-issues/3357/issue-3357.ts[14-33]
src/driver/mysql/MysqlDriver.ts[502-508]
ormconfig.sample.json[1-21]
test/utils/test-utils.ts[220-299]

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 test uses raw SQL with double-quoted identifiers, which is not portable to MySQL.
### Issue Context
This suite enables the MySQL driver and executes raw SQL via `queryRunner.manager.query` / `queryRunner.query`.
### Fix Focus Areas
- test/github-issues/3357/issue-3357.ts[24-67]
### What to change
- Replace raw SQL with driver-agnostic operations:
- Use `connection.getRepository(TestEntity).save({ name: "test-value" })` for insert.
- Use `connection.getRepository(TestEntity).find()` (or `findOneBy`) to assert data preserved.
- If you must keep raw SQL, build it with driver escaping (e.g., `connection.driver.escape(...)`) and parameter binding rather than hard-coded double quotes.

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



Remediation recommended

7. No docs for MSSQL change 📘 Rule violation ⚙ Maintainability
Description
The PR changes SQL Server migration generation behavior (switching to ALTER COLUMN and handling
default constraints), which is user-visible, but no documentation or samples are updated in the
provided diff. This can leave users unaware of behavioral changes or limitations.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1367-1445]

+            if (typeOrLengthChanged) {
+                // Use ALTER COLUMN to preserve data instead of DROP + ADD
+                // SQL Server supports: ALTER TABLE t ALTER COLUMN col_name new_type [NULL | NOT NULL]
+                // Drop default constraint first if one exists (SQL Server requires this before ALTER COLUMN)
+                if (
+                    oldColumn.default !== null &&
+                    oldColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            oldColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} DROP CONSTRAINT "${defaultName}"`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${oldColumn.default} FOR "${oldColumn.name}"`,
+                        ),
+                    )
+                }
+
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            newColumn,
+                            true,
+                            false,
+                        )}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            oldColumn,
+                            true,
+                            false,
+                        )}`,
+                    ),
+                )
+
+                // Restore default constraint after type change
+                if (
+                    newColumn.default !== null &&
+                    newColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            newColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${newColumn.default} FOR "${newColumn.name}"`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} DROP CONSTRAINT "${defaultName}"`,
+                        ),
+                    )
+                }
Evidence
Compliance ID 2 requires docs updates for user-facing changes; the diff shows a user-facing behavior
change in MSSQL DDL generation but contains no accompanying documentation updates.

Rule 2: Docs updated for user-facing changes
src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1445]

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

## Issue description
SQL Server migration generation behavior changed (type/length changes now use `ALTER COLUMN` with default constraint drop/re-add), but no docs/samples were updated in this PR.
## Issue Context
This is a user-visible change in generated migrations and may warrant documentation or release notes.
## Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1445]

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


8. Duplicate ALTER COLUMN emitted🐞 Bug ⛯ Reliability
Description
The new type/length-change path can emit an ALTER COLUMN, and the existing isColumnChanged() branch
can emit a second ALTER COLUMN in the same changeColumn call when other properties (e.g.,
precision/scale/nullability/collation) also differ.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1354-1419]

+        // Pure type/length changes can use ALTER COLUMN to preserve data
+        const typeOrLengthChanged =
+            newColumn.type !== oldColumn.type ||
+            newColumn.length !== oldColumn.length
+
+        if (requiresRecreation) {
+            // Must drop and recreate for identity/computed column changes
          await this.dropColumn(table, oldColumn)
          await this.addColumn(table, newColumn)
          // update cloned table
          clonedTable = table.clone()
      } else {
+            if (typeOrLengthChanged) {
+                // Use ALTER COLUMN to preserve data instead of DROP + ADD
+                // SQL Server supports: ALTER TABLE t ALTER COLUMN col_name new_type [NULL | NOT NULL]
+                // Drop default constraint first if one exists (SQL Server requires this before ALTER COLUMN)
+                if (
+                    oldColumn.default !== null &&
+                    oldColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            oldColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} DROP CONSTRAINT "${defaultName}"`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${oldColumn.default} FOR "${oldColumn.name}"`,
+                        ),
+                    )
+                }
+
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            newColumn,
+                            true,
+                            false,
+                        )}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            oldColumn,
+                            true,
+                            false,
+                        )}`,
+                    ),
+                )
Evidence
BaseQueryRunner.isColumnChanged explicitly excludes type/length but includes
precision/scale/collation/isNullable. Therefore, a type change that also changes precision/scale (or
other tracked properties) will satisfy both typeOrLengthChanged and isColumnChanged, resulting
in two ALTER COLUMN queries. This is newly introduced by adding an ALTER COLUMN for type/length
changes without guarding the later ALTER COLUMN branch.

src/query-runner/BaseQueryRunner.ts[548-604]
src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1420]
src/driver/sqlserver/SqlServerQueryRunner.ts[1771-1800]

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

## Issue description
`changeColumn` can generate two `ALTER COLUMN` statements for a single change when `typeOrLengthChanged` is true and `isColumnChanged` is also true due to other property differences (precision/scale/collation/nullability).
### Issue Context
- The new block adds `ALTER COLUMN` for type/length changes.
- The existing block later adds another `ALTER COLUMN` when `isColumnChanged(...)` is true.
### Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1354-1450]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1771-1800]
- src/query-runner/BaseQueryRunner.ts[548-604]
### What to change
- Introduce a guard so the later `isColumnChanged` ALTER COLUMN does not run when the earlier type/length ALTER COLUMN already applied the full column definition (including precision/scale/collation/nullability).
- Example: wrap the later block with `if (!typeOrLengthChanged && this.isColumnChanged(...))`.
- Alternatively, unify into a single ALTER COLUMN path that triggers when either `typeOrLengthChanged` or `isColumnChanged` is true.

ⓘ 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 0ec3556

Results up to commit N/A


🐞 Bugs (0) 📘 Rule violations (2) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider
Action required
1. Test added under github-issues 📘 Rule violation ⚙ Maintainability
Description
This PR adds the regression test only under test/github-issues/3357 rather than the functional
test suite, without any clear justification in the code. This violates the guideline to keep issue
fixes in test/functional to avoid per-issue test sprawl.
Code

test/github-issues/3357/issue-3357.ts[R10-73]

+describe("github issues > #3357 migration generation drops and creates columns instead of altering", () => {
+    let connections: DataSource[]
+
+    before(async () => {
+        connections = await createTestingConnections({
+            entities: [__dirname + "/entity/*{.js,.ts}"],
+            enabledDrivers: ["postgres", "mysql", "mssql", "cockroachdb"],
+            schemaCreate: true,
+            dropSchema: true,
+        })
+    })
+    beforeEach(() => reloadTestingDatabases(connections))
+    after(() => closeTestingConnections(connections))
+
+    it("should use ALTER COLUMN instead of DROP+ADD when changing column length to preserve data", () =>
+        Promise.all(
+            connections.map(async (connection) => {
+                const queryRunner = connection.createQueryRunner()
+
+                try {
+                    // Insert test data
+                    await queryRunner.manager.query(
+                        `INSERT INTO "test_entity" ("name") VALUES ('test-value')`,
+                    )
+
+                    const table = await queryRunner.getTable("test_entity")
+                    const nameColumn = table!.findColumnByName("name")!
+
+                    // Change length from 50 to 100
+                    const newColumn = nameColumn.clone()
+                    newColumn.length = "100"
+
+                    const sqlsInMemory =
+                        await queryRunner.getMemorySql()
+                    sqlsInMemory.upQueries = []
+                    sqlsInMemory.downQueries = []
+
+                    await queryRunner.changeColumn(table!, nameColumn, newColumn)
+
+                    // Verify no DROP COLUMN was generated
+                    const allSqls = [
+                        ...sqlsInMemory.upQueries,
+                    ]
+                        .map((q) => q.query)
+                        .join(" ")
+
+                    // allSqls.should.not.contain("DROP COLUMN") - this is hard to test without spying
+                    // Instead verify data is preserved
+                    const rows = await queryRunner.query(
+                        `SELECT "name" FROM "test_entity"`,
+                    )
+                    rows.should.have.length(1)
+                    rows[0].name.should.equal("test-value")
+
+                    // Verify the column was actually changed
+                    const updatedTable = await queryRunner.getTable("test_entity")
+                    const updatedColumn = updatedTable!.findColumnByName("name")!
+                    updatedColumn.length.should.equal("100")
+                } finally {
+                    await queryRunner.release()
+                }
+            }),
+        ))
+})
Evidence
Compliance ID 3 requires issue fix tests to be added/updated in test/functional and considers it a
failure when tests are added only under test/github-issues without a clear reason; this PR
introduces only test/github-issues/3357/issue-3357.ts for the fix.

Rule 3: Prefer functional tests over per-issue tests
test/github-issues/3357/issue-3357.ts[10-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 regression test for #3357 was added under `test/github-issues/3357`, but compliance requires issue fixes to live in the functional test suite unless there is a clear reason.
## Issue Context
Per-project compliance, prefer `test/functional` coverage to avoid accumulating per-issue tests.
## Fix Focus Areas
- test/github-issues/3357/issue-3357.ts[10-73]

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


2. Unused TableColumn import📘 Rule violation ⚙ Maintainability
Description
The new test file imports TableColumn but never uses it, adding noise and potentially failing
linting. This violates the requirement to avoid unnecessary/AI-like clutter in changes.
Code

test/github-issues/3357/issue-3357.ts[R2-3]

+import { DataSource } from "../../../src/data-source/DataSource"
+import { TableColumn } from "../../../src/schema-builder/table/TableColumn"
Evidence
Compliance ID 4 requires removing AI-generated noise and avoiding unnecessary additions;
TableColumn is newly imported but not referenced anywhere in the added test file.

Rule 4: Remove AI-generated noise
test/github-issues/3357/issue-3357.ts[2-3]

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

## Issue description
`TableColumn` is imported but never used in the new test.
## Issue Context
This adds noise and may fail lint/ts checks.
## Fix Focus Areas
- test/github-issues/3357/issue-3357.ts[2-3]

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


3. Enum ALTER COLUMN invalid🐞 Bug ✓ Correctness
Description
SqlServerQueryRunner.changeColumn uses buildCreateColumnSql() without skipEnum in an ALTER COLUMN
statement, which can inline a CONSTRAINT ... CHECK(...) clause and produce invalid SQL Server
syntax.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1398-1405]

+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            newColumn,
+                            true,
+                            false,
+                        )}`,
Evidence
In the new type/length-change path, ALTER COLUMN is constructed using buildCreateColumnSql(table,
newColumn, true, false) (skipEnum not set). buildCreateColumnSql appends an enum CHECK constraint
when column.enum is defined unless skipEnum=true, but SQL Server ALTER COLUMN cannot include
constraint definitions. The existing (unchanged) ALTER COLUMN path later in changeColumn explicitly
passes skipEnum=true, showing the intended usage pattern.

src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1420]
src/driver/sqlserver/SqlServerQueryRunner.ts[4306-4339]
src/driver/sqlserver/SqlServerQueryRunner.ts[4317-4326]
src/driver/sqlserver/SqlServerQueryRunner.ts[1771-1799]

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

## Issue description
`changeColumn` builds an `ALTER TABLE ... ALTER COLUMN` statement via `buildCreateColumnSql` without passing `skipEnum=true`. For enum columns this inlines a `CONSTRAINT ... CHECK(...)` fragment, which is not valid in SQL Server `ALTER COLUMN` syntax.
### Issue Context
`buildCreateColumnSql` appends enum `CHECK` constraints unless the `skipEnum` parameter is set.
### Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1419]
- src/driver/sqlserver/SqlServerQueryRunner.ts[4306-4326]
### What to change
- Update both up/down ALTER COLUMN calls in the new `typeOrLengthChanged` block to pass the 5th argument `true` (skipEnum), matching the existing ALTER COLUMN call later in the method:
- `this.buildCreateColumnSql(table, newColumn, true, false, true)`
- `this.buildCreateColumnSql(table, oldColumn, true, false, true)`

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


View more (2)
4. Rename/type order mismatch🐞 Bug ✓ Correctness
Description
When a column is renamed and its type/length changes in the same changeColumn call, the new ALTER
COLUMN/default-restore SQL uses newColumn.name before the sp_rename runs, so it can reference a
non-existent column name and fail.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1421-1436]

+                // Restore default constraint after type change
+                if (
+                    newColumn.default !== null &&
+                    newColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            newColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${newColumn.default} FOR "${newColumn.name}"`,
+                        ),
Evidence
The type/length-change block executes before the rename block. Inside it, the code restores the
default using FOR "${newColumn.name}", but the rename (EXEC sp_rename ...) only happens
afterward; at that point the physical column name is still oldColumn.name. This makes the generated
SQL incorrect for combined rename+type/length changes.

src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1446]
src/driver/sqlserver/SqlServerQueryRunner.ts[1448-1483]

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 `SqlServerQueryRunner.changeColumn`, the new `typeOrLengthChanged` block runs before the rename logic. It currently emits `ALTER COLUMN` and restores default constraints using `newColumn.name`, which is wrong if the column will be renamed later in the same call.
### Issue Context
The rename is performed via `sp_rename` in the `if (newColumn.name !== oldColumn.name)` block, which runs after the new ALTER COLUMN block.
### Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1446]
- src/driver/sqlserver/SqlServerQueryRunner.ts[1448-1483]
### What to change
- Ensure the ALTER COLUMN statement targets the *current* column name at execution time:
- Build an "alter column" TableColumn based on `newColumn` but with `name = oldColumn.name`, and use that in `buildCreateColumnSql`.
- When restoring the default inside the type/length block, use `oldColumn.name` in the `FOR` clause (and consider using the old default constraint name until after the rename/default-rename logic runs).
- Keep/verify the later default-constraint rename logic still works after these adjustments.

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


5. MySQL test quoting breaks🐞 Bug ✓ Correctness
Description
The new test uses double-quoted identifiers in raw SQL while running against MySQL, which by default
expects backticks and may reject the queries.
Code

test/github-issues/3357/issue-3357.ts[R30-33]

+                    // Insert test data
+                    await queryRunner.manager.query(
+                        `INSERT INTO "test_entity" ("name") VALUES ('test-value')`,
+                    )
Evidence
The test runs with enabledDrivers including "mysql" and executes raw SQL strings containing
double-quoted identifiers (e.g., "test_entity"). The MySQL driver’s identifier escaping uses
backticks, and the sample test configuration does not set any MySQL sql_mode that would make double
quotes behave as identifier quotes, making the test likely to fail on MySQL.

test/github-issues/3357/issue-3357.ts[14-33]
src/driver/mysql/MysqlDriver.ts[502-508]
ormconfig.sample.json[1-21]
test/utils/test-utils.ts[220-299]

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 test uses raw SQL with double-quoted identifiers, which is not portable to MySQL.
### Issue Context
This suite enables the MySQL driver and executes raw SQL via `queryRunner.manager.query` / `queryRunner.query`.
### Fix Focus Areas
- test/github-issues/3357/issue-3357.ts[24-67]
### What to change
- Replace raw SQL with driver-agnostic operations:
- Use `connection.getRepository(TestEntity).save({ name: "test-value" })` for insert.
- Use `connection.getRepository(TestEntity).find()` (or `findOneBy`) to assert data preserved.
- If you must keep raw SQL, build it with driver escaping (e.g., `connection.driver.escape(...)`) and parameter binding rather than hard-coded double quotes.

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



Remediation recommended
6. No docs for MSSQL change 📘 Rule violation ⚙ Maintainability
Description
The PR changes SQL Server migration generation behavior (switching to ALTER COLUMN and handling
default constraints), which is user-visible, but no documentation or samples are updated in the
provided diff. This can leave users unaware of behavioral changes or limitations.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1367-1445]

+            if (typeOrLengthChanged) {
+                // Use ALTER COLUMN to preserve data instead of DROP + ADD
+                // SQL Server supports: ALTER TABLE t ALTER COLUMN col_name new_type [NULL | NOT NULL]
+                // Drop default constraint first if one exists (SQL Server requires this before ALTER COLUMN)
+                if (
+                    oldColumn.default !== null &&
+                    oldColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            oldColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} DROP CONSTRAINT "${defaultName}"`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${oldColumn.default} FOR "${oldColumn.name}"`,
+                        ),
+                    )
+                }
+
+                upQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            newColumn,
+                            true,
+                            false,
+                        )}`,
+                    ),
+                )
+                downQueries.push(
+                    new Query(
+                        `ALTER TABLE ${this.escapePath(
+                            table,
+                        )} ALTER COLUMN ${this.buildCreateColumnSql(
+                            table,
+                            oldColumn,
+                            true,
+                            false,
+                        )}`,
+                    ),
+                )
+
+                // Restore default constraint after type change
+                if (
+                    newColumn.default !== null &&
+                    newColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            newColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} ADD CONSTRAINT "${defaultName}" DEFAULT ${newColumn.default} FOR "${newColumn.name}"`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} DROP CONSTRAINT "${defaultName}"`,
+                        ),
+                    )
+                }
Evidence
Compliance ID 2 requires docs updates for user-facing changes; the diff shows a user-facing behavior
change in MSSQL DDL generation but contains no accompanying documentation updates.

Rule 2: Docs updated for user-facing changes
src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1445]

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

## Issue description
SQL Server migration generation behavior changed (type/length changes now use `ALTER COLUMN` with default constraint drop/re-add), but no docs/samples were updated in this PR.
## Issue Context
This is a user-visible change in generated migrations and may warrant documentation or release notes.
## Fix Focus Areas
- src/driver/sqlserver/SqlServerQueryRunner.ts[1367-1445]

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


7. Duplicate ALTER COLUMN emitted🐞 Bug ⛯ Reliability
Description
The new type/length-change path can emit an ALTER COLUMN, and the existing isColumnChanged() branch
can emit a second ALTER COLUMN in the same changeColumn call when other properties (e.g.,
precision/scale/nullability/collation) also differ.
Code

src/driver/sqlserver/SqlServerQueryRunner.ts[R1354-1419]

+        // Pure type/length changes can use ALTER COLUMN to preserve data
+        const typeOrLengthChanged =
+            newColumn.type !== oldColumn.type ||
+            newColumn.length !== oldColumn.length
+
+        if (requiresRecreation) {
+            // Must drop and recreate for identity/computed column changes
           await this.dropColumn(table, oldColumn)
           await this.addColumn(table, newColumn)

           // update cloned table
           clonedTable = table.clone()
       } else {
+            if (typeOrLengthChanged) {
+                // Use ALTER COLUMN to preserve data instead of DROP + ADD
+                // SQL Server supports: ALTER TABLE t ALTER COLUMN col_name new_type [NULL | NOT NULL]
+                // Drop default constraint first if one exists (SQL Server requires this before ALTER COLUMN)
+                if (
+                    oldColumn.default !== null &&
+                    oldColumn.default !== undefined
+                ) {
+                    const defaultName =
+                        this.dataSource.namingStrategy.defaultConstraintName(
+                            table,
+                            oldColumn.name,
+                        )
+                    upQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )} DROP CONSTRAINT "${defaultName}"`,
+                        ),
+                    )
+                    downQueries.push(
+                        new Query(
+                            `ALTER TABLE ${this.escapePath(
+                                table,
+                            )...

- Pass skipEnum=true (5th arg) to buildCreateColumnSql in ALTER COLUMN
  paths to avoid invalid CHECK constraint syntax in ALTER COLUMN statements
- Fix rename+type order: use oldColumn.name when building ALTER COLUMN
  since rename (sp_rename) hasn't run yet at that point
- Guard isColumnChanged ALTER COLUMN with !typeOrLengthChanged to prevent
  duplicate ALTER COLUMN statements when type/length changed alongside
  other column properties
- Remove unused TableColumn import from test file
- Replace raw SQL with repository API in test to fix MySQL double-quote
  identifier quoting issue
@DeekRoumy
Copy link
Copy Markdown
Author

Thanks for the thorough automated review! I've addressed all the issues flagged by Qodo in the latest commit:

  1. skipEnum=true — Both buildCreateColumnSql calls in the new typeOrLengthChanged block now pass true as the 5th argument to prevent invalid CHECK constraint fragments in ALTER COLUMN statements.

  2. Rename/type order fix — The ALTER COLUMN and default-restore queries now use oldColumn.name (via a cloned column) since sp_rename runs afterward. This ensures the SQL references the column's current physical name.

  3. Duplicate ALTER COLUMN guard — Added !typeOrLengthChanged && guard before the isColumnChanged block, so a second ALTER COLUMN is never emitted when type/length already handled the change.

  4. Removed unused TableColumn import from the test file.

  5. MySQL quoting fix — Replaced raw SQL with connection.getRepository(TestEntity).save() and .find() so the test is driver-agnostic and works correctly on MySQL.

Re the test placement concern: the existing TypeORM codebase has many tests under test/github-issues/ for regression cases tied to specific issues — this follows that same pattern. Happy to move it to test/functional/ if that's the project preference.

Let me know if anything else needs attention!

@sonarqubecloud
Copy link
Copy Markdown

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

Persistent review updated to latest commit 0ec3556

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant