Skip to content

Commit

Permalink
feat: add fake migrations running and reverting (#8976)
Browse files Browse the repository at this point in the history
* feat: add fake migrations running and reverting

Added a cli option to fake-run or fake-revert a migration, adding to the
executed migrations table, but not actually running it. This feature is
useful for when migrations are added after the fact or for
interoperability between applications which are desired to each keep
a consistent migration history

Closes: #6195

* changed enabled drivers in test

* added docs to the property

* fixed lint issue

Co-authored-by: Umed Khudoiberdiev <pleerock.me@gmail.com>
Co-authored-by: Dmitry Zotov <dmzt08@gmail.com>
  • Loading branch information
3 people committed Aug 25, 2022
1 parent 5e5abbd commit 340ab67
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 5 deletions.
17 changes: 17 additions & 0 deletions docs/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,23 @@ typeorm migration:revert
This command will execute `down` in the latest executed migration.
If you need to revert multiple migrations you must call this command multiple times.

### Faking Migrations and Rollbacks

You can also fake run a migration using the `--fake` flag (`-f` for short). This will add the migration
to the migrations table without running it. This is useful for migrations created after manual changes
have already been made to the database or when migrations have been run externally
(e.g. by another tool or application), and you still would like to keep a consistent migration history.

```
typeorm migration:run --fake
```

This is also possible with rollbacks.

```
typeorm migration:revert --fake
```

## Generating migrations

TypeORM is able to automatically generate migration files with schema changes you made.
Expand Down
6 changes: 6 additions & 0 deletions src/commands/MigrationRevertCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export class MigrationRevertCommand implements yargs.CommandModule {
describe:
"Indicates if transaction should be used or not for migration revert. Enabled by default.",
})
.option("fake", {
alias: "f",
type: "boolean",
default: false,
describe: "Fakes reverting the migration",
})
}

async handler(args: yargs.Arguments) {
Expand Down
9 changes: 9 additions & 0 deletions src/commands/MigrationRunCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ export class MigrationRunCommand implements yargs.CommandModule {
describe:
"Indicates if transaction should be used or not for migration run. Enabled by default.",
})
.option("fake", {
alias: "f",
type: "boolean",
default: false,
describe:
"Fakes running the migrations if table schema has already been changed manually or externally " +
"(e.g. through another project)",
})
}

async handler(args: yargs.Arguments) {
Expand All @@ -47,6 +55,7 @@ export class MigrationRunCommand implements yargs.CommandModule {
transaction:
dataSource.options.migrationsTransactionMode ??
("all" as "all" | "none" | "each"),
fake: !!args.f,
}

switch (args.t) {
Expand Down
4 changes: 4 additions & 0 deletions src/data-source/DataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,13 +366,15 @@ export class DataSource {
*/
async runMigrations(options?: {
transaction?: "all" | "none" | "each"
fake?: boolean
}): Promise<Migration[]> {
if (!this.isInitialized)
throw new CannotExecuteNotConnectedError(this.name)

const migrationExecutor = new MigrationExecutor(this)
migrationExecutor.transaction =
(options && options.transaction) || "all"
migrationExecutor.fake = (options && options.fake) || false

const successMigrations =
await migrationExecutor.executePendingMigrations()
Expand All @@ -385,13 +387,15 @@ export class DataSource {
*/
async undoLastMigration(options?: {
transaction?: "all" | "none" | "each"
fake?: boolean
}): Promise<void> {
if (!this.isInitialized)
throw new CannotExecuteNotConnectedError(this.name)

const migrationExecutor = new MigrationExecutor(this)
migrationExecutor.transaction =
(options && options.transaction) || "all"
migrationExecutor.fake = (options && options.fake) || false

await migrationExecutor.undoLastMigration()
}
Expand Down
33 changes: 28 additions & 5 deletions src/migration/MigrationExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export class MigrationExecutor {
*/
transaction: "all" | "none" | "each" = "all"

/**
* Option to fake-run or fake-revert a migration, adding to the
* executed migrations table, but not actually running it. This feature is
* useful for when migrations are added after the fact or for
* interoperability between applications which are desired to each keep
* a consistent migration history.
*/
fake: boolean

// -------------------------------------------------------------------------
// Private Properties
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -251,6 +260,14 @@ export class MigrationExecutor {
// run all pending migrations in a sequence
try {
for (const migration of pendingMigrations) {
if (this.fake) {
// directly insert migration record into the database if it is fake
await this.insertExecutedMigration(queryRunner, migration)

// nothing else needs to be done, continue to next migration
continue
}

if (
this.transaction === "each" &&
!queryRunner.isTransactionActive
Expand Down Expand Up @@ -285,7 +302,9 @@ export class MigrationExecutor {
// informative log about migration success
successMigrations.push(migration)
this.connection.logger.logSchemaBuild(
`Migration ${migration.name} has been executed successfully.`,
`Migration ${migration.name} has been ${
this.fake ? "(fake)" : ""
} executed successfully.`,
)
})
}
Expand Down Expand Up @@ -372,13 +391,17 @@ export class MigrationExecutor {
}

try {
await queryRunner.beforeMigration()
await migrationToRevert.instance!.down(queryRunner)
await queryRunner.afterMigration()
if (!this.fake) {
await queryRunner.beforeMigration()
await migrationToRevert.instance!.down(queryRunner)
await queryRunner.afterMigration()
}

await this.deleteExecutedMigration(queryRunner, migrationToRevert)
this.connection.logger.logSchemaBuild(
`Migration ${migrationToRevert.name} has been reverted successfully.`,
`Migration ${migrationToRevert.name} has been ${
this.fake ? "(fake)" : ""
} reverted successfully.`,
)

// commit transaction if we started it
Expand Down
115 changes: 115 additions & 0 deletions test/github-issues/6195/issue-6195.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import "reflect-metadata"
import { expect } from "chai"

import { DataSource, QueryRunner, Table } from "../../../src"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"

export const testTableName = "test_table"
export const testColumnName = "test_column"
export const nonExistentColumnName = "nonexistent_column"

const createTestTable = async (queryRunner: QueryRunner) => {
await queryRunner.createTable(
new Table({
name: testTableName,
columns: [
{
name: "id",
type: "integer",
isPrimary: true,
},
{
name: testColumnName,
type: "varchar",
},
],
}),
)
}

describe("github issues > #6195 feature: fake migrations for existing tables", () => {
let dataSources: DataSource[]

before(async () => {
dataSources = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: false,
dropSchema: false,
migrations: [__dirname + "/migrations/**/*{.ts,.js}"],
// logging: true,
})

await reloadTestingDatabases(dataSources)

for (const dataSource of dataSources) {
const queryRunner = dataSource.createQueryRunner()
await dataSource.showMigrations() // To initialize migrations table
await createTestTable(queryRunner)
await queryRunner.release()
}
})

after(async () => {
await closeTestingConnections(dataSources)
})

describe("fake run tests", () => {
it("should fail for duplicate column", async () => {
for (const dataSource of dataSources) {
if (dataSource.options.type === "mongodb") return
await expect(
dataSource.runMigrations({ transaction: "all" }),
).to.be.rejectedWith(Error)
}
})

it("should not fail for duplicate column when run with the fake option", async () => {
for (const dataSource of dataSources) {
if (dataSource.options.type === "mongodb") return
await expect(
dataSource.runMigrations({
transaction: "all",
fake: true,
}),
).not.to.be.rejectedWith(Error)
}
})
})

describe("fake rollback tests", () => {
before(async () => {
for (const dataSource of dataSources) {
if (dataSource.options.type === "mongodb") return
await dataSource.runMigrations({
transaction: "all",
fake: true,
})
}
})

it("should fail for non-existent column", async () => {
for (const dataSource of dataSources) {
if (dataSource.options.type === "mongodb") return
await expect(
dataSource.undoLastMigration({ transaction: "all" }),
).to.be.rejectedWith(Error)
}
})

it("should not fail for non-existent column when run with the fake option", async () => {
for (const dataSource of dataSources) {
if (dataSource.options.type === "mongodb") return
await expect(
dataSource.undoLastMigration({
transaction: "all",
fake: true,
}),
).not.to.be.rejectedWith(Error)
}
})
})
})
24 changes: 24 additions & 0 deletions test/github-issues/6195/migrations/MigrationToFakeRun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner, TableColumn } from "../../../../src"
import {
testColumnName,
testTableName,
nonExistentColumnName,
} from "../issue-6195"

export class MigrationToFakeRun implements MigrationInterface {
name = "MigrationToFakeRun" + Date.now()

async up(queryRunner: QueryRunner) {
await queryRunner.addColumn(
testTableName,
new TableColumn({
name: testColumnName,
type: "varchar",
}),
)
}

async down(queryRunner: QueryRunner) {
await queryRunner.dropColumn(testTableName, nonExistentColumnName)
}
}

0 comments on commit 340ab67

Please sign in to comment.