Skip to content

Commit

Permalink
feat(Migrator): add step option
Browse files Browse the repository at this point in the history
  • Loading branch information
MaximeMRF committed Mar 14, 2024
1 parent 8c333e0 commit 4012129
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
9 changes: 9 additions & 0 deletions commands/migration/rollback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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,
})
Expand Down
12 changes: 9 additions & 3 deletions src/migration/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand All @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/types/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type MigratorOptions =
| {
direction: 'down'
batch?: number
step?: number
connectionName?: string
dryRun?: boolean
disableLocks?: boolean
Expand Down
176 changes: 176 additions & 0 deletions test/migrations/migrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 4012129

Please sign in to comment.