Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/docs/migrations/04-generating.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,18 @@ export class PostRefactoringTIMESTAMP {
See, you don't need to write the queries on your own.

The rule of thumb for generating migrations is that you generate them after **each** change you made to your models. To apply multi-line formatting to your generated migration queries, use the `p` (alias for `--pretty`) flag.

## Column type and length changes

When TypeORM detects that only a column's length changed within the same base type (e.g. `varchar(50)` → `varchar(200)`), it generates a non-destructive in-place `ALTER` statement instead of a destructive `DROP COLUMN` + `ADD COLUMN` pair. This preserves all existing row data during the migration.

For example, after changing `length: "50"` to `length: "200"` on a `varchar` column:

| Driver | Generated SQL |
|--------|---------------|
| MySQL / Aurora MySQL | `ALTER TABLE \`post\` CHANGE \`description\` \`description\` varchar(200) NOT NULL` |
| PostgreSQL / CockroachDB | `ALTER TABLE "post" ALTER COLUMN "description" TYPE varchar(200)` |
| Spanner | `ALTER TABLE \`post\` ALTER COLUMN "description" TYPE string(200)` |
| Oracle | `ALTER TABLE "post" MODIFY "description" varchar2(200)` |

Changes that require a full column recreation (e.g. switching between incompatible types, enum changes, or identity column type changes) still use the `DROP COLUMN` + `ADD COLUMN` approach.
58 changes: 51 additions & 7 deletions src/driver/aurora-mysql/AuroraMysqlQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -851,8 +851,6 @@ export class AuroraMysqlQueryRunner
if (
(newColumn.isGenerated !== oldColumn.isGenerated &&
newColumn.generationStrategy !== "uuid") ||
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length ||
oldColumn.generatedType !== newColumn.generatedType
) {
await this.dropColumn(table, oldColumn)
Expand All @@ -861,14 +859,58 @@ export class AuroraMysqlQueryRunner
// update cloned table
clonedTable = table.clone()
} else {
const typeOrLengthChanged =
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length

if (typeOrLengthChanged && newColumn.name === oldColumn.name) {
// Type or length changed without rename: use CHANGE to preserve data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
newColumn,
true,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
)}`,
),
)

// Update clonedTable so replaceCachedTable() propagates the
// correct column definition. Preserve oldColumn.name so that
// the rename block below can still locate this entry; it will
// update .name to newColumn.name when it runs.
const clonedColIdx = clonedTable.columns.findIndex(
(c) => c.name === oldColumn.name,
)
if (clonedColIdx !== -1) {
const updatedCol = newColumn.clone()
updatedCol.name = oldColumn.name
clonedTable.columns[clonedColIdx] = updatedCol
}
}

if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
// Column rename, possibly combined with a type/length change.
// A single CHANGE handles both atomically.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
newColumn,
true,
true,
)}`,
Expand Down Expand Up @@ -991,17 +1033,19 @@ export class AuroraMysqlQueryRunner
foreignKey.name = newForeignKeyName
})

// rename old column in the Table object
// rename old column in the Table object, propagating all
// column changes (name, type, length) so the cache stays
// accurate even when rename and type/length change together.
const oldTableColumn = clonedTable.columns.find(
(column) => column.name === oldColumn.name,
)
clonedTable.columns[
clonedTable.columns.indexOf(oldTableColumn!)
].name = newColumn.name
] = newColumn.clone()
oldColumn.name = newColumn.name
}

if (this.isColumnChanged(oldColumn, newColumn, true)) {
if (!(typeOrLengthChanged && newColumn.name === oldColumn.name) && this.isColumnChanged(oldColumn, newColumn, true)) {
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
Expand Down
58 changes: 53 additions & 5 deletions src/driver/cockroachdb/CockroachQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1377,20 +1377,68 @@ export class CockroachQueryRunner
`Column "${oldTableColumnOrName}" was not found in the "${table.name}" table.`,
)

// Enum type changes must use DROP + ADD because createFullType() returns
// the generic keyword "enum" rather than the actual type name (e.g.
// "table_col_enum"), so ALTER COLUMN ... TYPE would fail.
const typeChanged = oldColumn.type !== newColumn.type
const isArrayChanged = newColumn.isArray !== oldColumn.isArray
const lengthOnlyChanged =
!typeChanged &&
!isArrayChanged &&
oldColumn.length !== newColumn.length
const typeOrLengthChanged =
typeChanged || oldColumn.length !== newColumn.length || isArrayChanged
const isEnumChange =
oldColumn.type === "enum" ||
oldColumn.type === "simple-enum" ||
newColumn.type === "enum" ||
newColumn.type === "simple-enum"

if (
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length ||
newColumn.isArray !== oldColumn.isArray ||
oldColumn.generatedType !== newColumn.generatedType ||
oldColumn.asExpression !== newColumn.asExpression
oldColumn.asExpression !== newColumn.asExpression ||
(typeOrLengthChanged && (isEnumChange || typeChanged || isArrayChanged))
) {
// To avoid data conversion, we just recreate column
// Generated/computed columns, enum changes, actual type changes, and array
// dimension changes all require full DROP + ADD recreation.
await this.dropColumn(table, oldColumn)
await this.addColumn(table, newColumn)

// update cloned table
clonedTable = table.clone()
} else {
if (lengthOnlyChanged) {
// Only the length changed within the same base type (e.g. varchar(50)
// → varchar(200)). Use ALTER COLUMN ... TYPE to preserve row data.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${this.driver.createFullType(newColumn)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${
oldColumn.name
}" TYPE ${this.driver.createFullType(oldColumn)}`,
),
)

// Update clonedTable so replaceCachedTable() propagates the
// correct column definition. Preserve oldColumn.name so that
// the rename block below can still locate this entry; it will
// update .name to newColumn.name when it runs.
const clonedColIdx = clonedTable.columns.findIndex(
(c) => c.name === oldColumn.name,
)
if (clonedColIdx !== -1) {
const updatedCol = newColumn.clone()
updatedCol.name = oldColumn.name
clonedTable.columns[clonedColIdx] = updatedCol
}
}

if (oldColumn.name !== newColumn.name) {
// rename column
upQueries.push(
Expand Down
56 changes: 52 additions & 4 deletions src/driver/mysql/MysqlQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1110,8 +1110,9 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
if (
(newColumn.isGenerated !== oldColumn.isGenerated &&
newColumn.generationStrategy !== "uuid") ||
// A change in base type requires column recreation (DROP + ADD) so that
// MySQL can properly re-validate data and constraints for the new type.
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length ||
(oldColumn.generatedType &&
newColumn.generatedType &&
oldColumn.generatedType !== newColumn.generatedType) ||
Expand All @@ -1125,14 +1126,61 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
// update cloned table
clonedTable = table.clone()
} else {
// At this point oldColumn.type === newColumn.type (type change was handled above).
const lengthOnlyChanged =
oldColumn.length !== newColumn.length

if (lengthOnlyChanged && newColumn.name === oldColumn.name) {
// Only the length changed within the same base type (e.g. varchar(50)
// → varchar(200)). Use CHANGE to preserve existing row data instead
// of the destructive DROP + ADD path.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
newColumn,
true,
true,
)}`,
),
)
downQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
true,
true,
)}`,
),
)

// Update clonedTable so replaceCachedTable() propagates the
// correct column definition. Preserve oldColumn.name so that
// the rename block below can still locate this entry; it will
// update .name to newColumn.name when it runs.
const clonedColIdx = clonedTable.columns.findIndex(
(c) => c.name === oldColumn.name,
)
if (clonedColIdx !== -1) {
const updatedCol = newColumn.clone()
updatedCol.name = oldColumn.name
clonedTable.columns[clonedColIdx] = updatedCol
}
}

if (newColumn.name !== oldColumn.name) {
// We don't change any column properties, just rename it.
// Column rename, possibly combined with a type/length change.
// A single CHANGE statement handles both atomically, avoiding the
// revert bug that occurs when two separate statements are used.
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
oldColumn.name
}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(
oldColumn,
newColumn,
true,
true,
)}`,
Expand Down Expand Up @@ -1292,7 +1340,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
oldColumn.name = newColumn.name
}

if (this.isColumnChanged(oldColumn, newColumn, true, true)) {
if (!lengthOnlyChanged && this.isColumnChanged(oldColumn, newColumn, true, true)) {
upQueries.push(
new Query(
`ALTER TABLE ${this.escapePath(table)} CHANGE \`${
Expand Down
29 changes: 23 additions & 6 deletions src/driver/oracle/OracleQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1129,19 +1129,27 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
if (
(newColumn.isGenerated !== oldColumn.isGenerated &&
newColumn.generationStrategy !== "uuid") ||
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length ||
oldColumn.generatedType !== newColumn.generatedType ||
oldColumn.asExpression !== newColumn.asExpression
oldColumn.asExpression !== newColumn.asExpression ||
// Can't MODIFY an identity column to a different type; requires recreation
(oldColumn.type !== newColumn.type && oldColumn.isGenerated)
) {
// Oracle does not support changing of IDENTITY column, so we must drop column and recreate it again.
// Also, we recreate column if column type changed
// Oracle does not support changing of IDENTITY column or computed columns
// without full recreation; identity + type change also requires recreation
await this.dropColumn(table, oldColumn)
await this.addColumn(table, newColumn)

// update cloned table
clonedTable = table.clone()
} else {
// Cover both type and length changes: non-identity type changes and any
// length changes within the same type can be handled non-destructively with
// ALTER TABLE … MODIFY (identity-column type changes are excluded by the
// DROP+ADD branch above).
const typeOrLengthChanged =
oldColumn.type !== newColumn.type ||
oldColumn.length !== newColumn.length

if (newColumn.name !== oldColumn.name) {
// rename column
upQueries.push(
Expand Down Expand Up @@ -1354,7 +1362,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
oldColumn.name = newColumn.name
}

if (this.isColumnChanged(oldColumn, newColumn, true)) {
if (typeOrLengthChanged || this.isColumnChanged(oldColumn, newColumn, true)) {
let defaultUp: string = ""
let defaultDown: string = ""
let nullableUp: string = ""
Expand Down Expand Up @@ -1412,6 +1420,15 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
)} ${defaultDown} ${nullableDown}`,
),
)

// Update clonedTable so replaceCachedTable() propagates the
// correct column definition (oldColumn.name may have been
// updated to newColumn.name by the rename block above).
const clonedColIdx = clonedTable.columns.findIndex(
(c) => c.name === newColumn.name,
)
if (clonedColIdx !== -1)
clonedTable.columns[clonedColIdx] = newColumn.clone()
}

if (newColumn.isPrimary !== oldColumn.isPrimary) {
Expand Down
Loading