diff --git a/README.md b/README.md index ad7bada1..ca4e3491 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ We encourage you to read the [contribution guide](https://github.com/adonisjs/.g Easiest way to run tests is to launch the redis cluster using docker-compose and `docker-compose.yml` file. ```sh -docker-compose up +docker-compose up -d npm run test ``` diff --git a/commands/migration/rollback.ts b/commands/migration/rollback.ts index 592baee6..5a4d1fa9 100644 --- a/commands/migration/rollback.ts +++ b/commands/migration/rollback.ts @@ -52,6 +52,14 @@ export default class Rollback extends MigrationsBase { }) declare batch: number + /** + * Define custom step, instead of rolling back to the latest batch + */ + @flags.number({ + description: 'The number of migrations to be reverted', + }) + declare step: number + /** * Display migrations result in one compact single-line output */ @@ -74,6 +82,7 @@ export default class Rollback extends MigrationsBase { direction: 'down', connectionName: this.connection, batch: this.batch, + step: this.step, dryRun: this.dryRun, disableLocks: this.disableLocks, }) diff --git a/src/migration/runner.ts b/src/migration/runner.ts index 4531b645..8337ecb1 100644 --- a/src/migration/runner.ts +++ b/src/migration/runner.ts @@ -467,7 +467,7 @@ export class MigrationRunner extends EventEmitter { /** * Migrate down (aka rollback) */ - private async runDown(batch?: number) { + private async runDown(batch?: number, step?: number) { if (this.isInProduction && this.migrationsConfig.disableRollbacksInProduction) { throw new Error( 'Rollback in production environment is disabled. Check "config/database" file for options.' @@ -481,6 +481,12 @@ export class MigrationRunner extends EventEmitter { const existing = await this.getMigratedFilesTillBatch(batch) const collected = await this.migrationSource.getMigrations() + if (step === undefined || step <= 0) { + step = 0 + } else { + batch = (await this.getLatestBatch()) - 1 + } + /** * Finding schema files for migrations to rollback. We do not perform * rollback when any of the files are missing @@ -499,7 +505,7 @@ export class MigrationRunner extends EventEmitter { } }) - const filesToMigrate = Object.keys(this.migratedFiles) + const filesToMigrate = Object.keys(this.migratedFiles).slice(-step) for (let name of filesToMigrate) { await this.executeMigration(this.migratedFiles[name].file) } @@ -583,7 +589,7 @@ export class MigrationRunner extends EventEmitter { if (this.direction === 'up') { await this.runUp() } else if (this.options.direction === 'down') { - await this.runDown(this.options.batch) + await this.runDown(this.options.batch, this.options.step) } } catch (error) { this.error = error diff --git a/src/types/migrator.ts b/src/types/migrator.ts index a628cc2a..141f1541 100644 --- a/src/types/migrator.ts +++ b/src/types/migrator.ts @@ -22,6 +22,7 @@ export type MigratorOptions = | { direction: 'down' batch?: number + step?: number connectionName?: string dryRun?: boolean disableLocks?: boolean diff --git a/test/migrations/migrator.spec.ts b/test/migrations/migrator.spec.ts index 261f9641..4530f57e 100644 --- a/test/migrations/migrator.spec.ts +++ b/test/migrations/migrator.spec.ts @@ -640,6 +640,182 @@ test.group('Migrator', (group) => { ]) }) + test('rollback database using schema files to a given step', async ({ fs, assert, cleanup }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + cleanup(() => db.manager.closeAll()) + + await fs.create( + 'database/migrations/0_users_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_users', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_users') + } + } + ` + ) + + await fs.create( + 'database/migrations/1_accounts_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_accounts', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_accounts') + } + } + ` + ) + + const migrator = getMigrator(db, app, { direction: 'up', connectionName: 'primary' }) + await migrator.run() + + const migrator1 = getMigrator(db, app, { + direction: 'down', + step: 1, + connectionName: 'primary', + }) + await migrator1.run() + + const migrated = await db.connection().from('adonis_schema').select('*') + const hasUsersTable = await db.connection().schema.hasTable('schema_users') + const hasAccountsTable = await db.connection().schema.hasTable('schema_accounts') + const migratedFiles = Object.keys(migrator1.migratedFiles).map((file) => { + return { + status: migrator1.migratedFiles[file].status, + file: file, + queries: migrator1.migratedFiles[file].queries, + } + }) + + assert.lengthOf(migrated, 1) + assert.isFalse(hasUsersTable) + assert.isTrue(hasAccountsTable) + assert.deepEqual(migratedFiles, [ + { + status: 'pending', + file: 'database/migrations/1_accounts_v6', + queries: [], + }, + { + status: 'completed', + file: 'database/migrations/0_users_v6', + queries: [], + }, + ]) + }) + + test('negative numbers specified by the step option must rollback all the migrated files to the current batch', async ({ + fs, + assert, + cleanup, + }) => { + const app = new AppFactory().create(fs.baseUrl, () => {}) + await app.init() + const db = getDb() + cleanup(() => db.manager.closeAll()) + + await fs.create( + 'database/migrations/0_users_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_users', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_users') + } + } + ` + ) + + const migrator = getMigrator(db, app, { direction: 'up', connectionName: 'primary' }) + await migrator.run() + + await fs.create( + 'database/migrations/1_accounts_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_accounts', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_accounts') + } + } + ` + ) + + await fs.create( + 'database/migrations/2_roles_v6.ts', + ` + import { BaseSchema as Schema } from '../../../../src/schema/main.js' + export default class extends Schema { + public async up () { + this.schema.createTable('schema_roles', (table) => { + table.increments() + }) + } + + public async down () { + this.schema.dropTable('schema_roles') + } + } + ` + ) + + const migrator1 = getMigrator(db, app, { direction: 'up', connectionName: 'primary' }) + await migrator.run() + + const migrator2 = getMigrator(db, app, { + direction: 'down', + step: -1, + connectionName: 'primary', + }) + await migrator2.run() + + const migrated = await db.connection().from('adonis_schema').select('*') + const hasUsersTable = await db.connection().schema.hasTable('schema_users') + const hasAccountsTable = await db.connection().schema.hasTable('schema_accounts') + const hasRolesTable = await db.connection().schema.hasTable('schema_roles') + const migratedFiles = Object.keys(migrator1.migratedFiles).map((file) => { + return { + status: migrator2.migratedFiles[file].status, + file: file, + queries: migrator2.migratedFiles[file].queries, + } + }) + + assert.lengthOf(migrated, 0) + assert.isFalse(hasUsersTable) + assert.isFalse(hasAccountsTable) + assert.isFalse(hasRolesTable) + assert.deepEqual(migratedFiles, []) + }) + test('rollback multiple times must be a noop', async ({ fs, assert, cleanup }) => { const app = new AppFactory().create(fs.baseUrl, () => {}) await app.init()