feat(up): add --limit option to limit the number of migrations to run
This commit is contained in:
parent
bf4d596980
commit
02c142e39a
7 changed files with 176 additions and 5 deletions
5
.changeset/sharp-buses-draw.md
Normal file
5
.changeset/sharp-buses-draw.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add --limit option to the "up" command, for limiting the number of migrations to run
|
||||||
|
|
@ -64,6 +64,11 @@ Show command help and exit
|
||||||
|
|
||||||
List the pending migrations that would be run without actually running them
|
List the pending migrations that would be run without actually running them
|
||||||
|
|
||||||
|
### `-l, --limit <count>`
|
||||||
|
|
||||||
|
Limit the number of migrations to run. Can be combined with `--dry` which will show "pending" for the migrations that would be run if not in dry-run mode,
|
||||||
|
and "skipped" for the migrations that also haven't been run but won't because of the set limit.
|
||||||
|
|
||||||
### `-d`, `--directory <path>`
|
### `-d`, `--directory <path>`
|
||||||
|
|
||||||
The directory where the migration files are located. The given path should be absolute or relative to the current working directory.
|
The directory where the migration files are located. The given path should be absolute or relative to the current working directory.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,35 @@ bun add @emigrate/cli
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: emigrate up [options]
|
||||||
|
|
||||||
|
Run all pending migrations
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
-d, --directory <path> The directory where the migration files are located (required)
|
||||||
|
-i, --import <module> Additional modules/packages to import before running the migrations (can be specified multiple times)
|
||||||
|
For example if you want to use Dotenv to load environment variables or when using TypeScript
|
||||||
|
-s, --storage <name> The storage to use for where to store the migration history (required)
|
||||||
|
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
||||||
|
-r, --reporter <name> The reporter to use for reporting the migration progress
|
||||||
|
-l, --limit <count> Limit the number of migrations to run
|
||||||
|
--dry List the pending migrations that would be run without actually running them
|
||||||
|
--color Force color output (this option is passed to the reporter)
|
||||||
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
emigrate up --directory src/migrations -s fs
|
||||||
|
emigrate up -d ./migrations --storage @emigrate/mysql
|
||||||
|
emigrate up -d src/migrations -s postgres -r json --dry
|
||||||
|
emigrate up -d ./migrations -s mysql --import dotenv/config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
Create a new migration:
|
Create a new migration:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ const up: Action = async (args) => {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
short: 's',
|
short: 's',
|
||||||
},
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'l',
|
||||||
|
},
|
||||||
dry: {
|
dry: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
|
@ -80,6 +84,7 @@ Options:
|
||||||
-s, --storage <name> The storage to use for where to store the migration history (required)
|
-s, --storage <name> The storage to use for where to store the migration history (required)
|
||||||
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
-p, --plugin <name> The plugin(s) to use (can be specified multiple times)
|
||||||
-r, --reporter <name> The reporter to use for reporting the migration progress
|
-r, --reporter <name> The reporter to use for reporting the migration progress
|
||||||
|
-l, --limit <count> Limit the number of migrations to run
|
||||||
--dry List the pending migrations that would be run without actually running them
|
--dry List the pending migrations that would be run without actually running them
|
||||||
--color Force color output (this option is passed to the reporter)
|
--color Force color output (this option is passed to the reporter)
|
||||||
--no-color Disable color output (this option is passed to the reporter)
|
--no-color Disable color output (this option is passed to the reporter)
|
||||||
|
|
@ -104,15 +109,34 @@ Examples:
|
||||||
storage = config.storage,
|
storage = config.storage,
|
||||||
reporter = config.reporter,
|
reporter = config.reporter,
|
||||||
dry,
|
dry,
|
||||||
|
limit: limitString,
|
||||||
import: imports = [],
|
import: imports = [],
|
||||||
} = values;
|
} = values;
|
||||||
const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])];
|
const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])];
|
||||||
|
|
||||||
|
const limit = limitString === undefined ? undefined : Number.parseInt(limitString, 10);
|
||||||
|
|
||||||
|
if (Number.isNaN(limit)) {
|
||||||
|
console.error('Invalid limit value, expected an integer but was:', limitString);
|
||||||
|
console.log(usage);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await importAll(cwd, imports);
|
await importAll(cwd, imports);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { default: upCommand } = await import('./commands/up.js');
|
const { default: upCommand } = await import('./commands/up.js');
|
||||||
process.exitCode = await upCommand({ storage, reporter, directory, plugins, cwd, dry, color: useColors(values) });
|
process.exitCode = await upCommand({
|
||||||
|
storage,
|
||||||
|
reporter,
|
||||||
|
directory,
|
||||||
|
plugins,
|
||||||
|
cwd,
|
||||||
|
dry,
|
||||||
|
limit,
|
||||||
|
color: useColors(values),
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ShowUsageError) {
|
if (error instanceof ShowUsageError) {
|
||||||
console.error(error.message, '\n');
|
console.error(error.message, '\n');
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ describe('up', () => {
|
||||||
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
const failedEntry = toEntry('some_failed_migration.js', 'failed');
|
||||||
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry]));
|
const { reporter, run } = getUpCommand([failedEntry.name, 'some_file.js'], getStorage([failedEntry]));
|
||||||
|
|
||||||
const exitCode = await run(true);
|
const exitCode = await run({ dry: true });
|
||||||
|
|
||||||
assert.strictEqual(exitCode, 1);
|
assert.strictEqual(exitCode, 1);
|
||||||
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
||||||
|
|
@ -344,6 +344,88 @@ describe('up', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns 0 and finishes without an error when the given number of pending migrations are run successfully', async () => {
|
||||||
|
const { reporter, run } = getUpCommand(
|
||||||
|
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||||
|
getStorage(['some_already_run_migration.js']),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
loadableExtensions: ['.js'],
|
||||||
|
async loadMigration() {
|
||||||
|
return async () => {
|
||||||
|
// Success
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const exitCode = await run({ limit: 1 });
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 0);
|
||||||
|
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
||||||
|
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
||||||
|
{
|
||||||
|
command: 'up',
|
||||||
|
cwd: '/emigrate',
|
||||||
|
version,
|
||||||
|
dry: false,
|
||||||
|
color: undefined,
|
||||||
|
directory: 'migrations',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0);
|
||||||
|
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
||||||
|
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||||
|
assert.strictEqual(error, undefined);
|
||||||
|
assert.strictEqual(entries?.length, 2);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||||
|
['some_migration.js (done)', 'some_other_migration.js (skipped)'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 and finishes without an error with the given number of pending migrations are validated and listed successfully in dry-mode', async () => {
|
||||||
|
const { reporter, run } = getUpCommand(
|
||||||
|
['some_already_run_migration.js', 'some_migration.js', 'some_other_migration.js'],
|
||||||
|
getStorage(['some_already_run_migration.js']),
|
||||||
|
);
|
||||||
|
|
||||||
|
const exitCode = await run({ dry: true, limit: 1 });
|
||||||
|
|
||||||
|
assert.strictEqual(exitCode, 0);
|
||||||
|
assert.strictEqual(reporter.onInit.mock.calls.length, 1);
|
||||||
|
assert.deepStrictEqual(reporter.onInit.mock.calls[0]?.arguments, [
|
||||||
|
{
|
||||||
|
command: 'up',
|
||||||
|
cwd: '/emigrate',
|
||||||
|
version,
|
||||||
|
dry: true,
|
||||||
|
color: undefined,
|
||||||
|
directory: 'migrations',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
assert.strictEqual(reporter.onCollectedMigrations.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(reporter.onLockedMigrations.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(reporter.onMigrationStart.mock.calls.length, 0);
|
||||||
|
assert.strictEqual(reporter.onMigrationSuccess.mock.calls.length, 0);
|
||||||
|
assert.strictEqual(reporter.onMigrationError.mock.calls.length, 0);
|
||||||
|
assert.strictEqual(reporter.onMigrationSkip.mock.calls.length, 2);
|
||||||
|
assert.strictEqual(reporter.onFinished.mock.calls.length, 1);
|
||||||
|
const [entries, error] = reporter.onFinished.mock.calls[0]?.arguments ?? [];
|
||||||
|
assert.strictEqual(error, undefined);
|
||||||
|
assert.strictEqual(entries?.length, 2);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
entries.map((entry) => `${entry.name} (${entry.status})`),
|
||||||
|
['some_migration.js (pending)', 'some_other_migration.js (skipped)'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
|
it('returns 1 and finishes with an error when a pending migration throw when run', async () => {
|
||||||
const { reporter, run } = getUpCommand(
|
const { reporter, run } = getUpCommand(
|
||||||
['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'],
|
['some_already_run_migration.js', 'some_migration.js', 'fail.js', 'some_other_migration.js'],
|
||||||
|
|
@ -495,7 +577,12 @@ function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugi
|
||||||
onMigrationSkip: mock.fn(noop),
|
onMigrationSkip: mock.fn(noop),
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = async (dry = false) => {
|
const run = async (
|
||||||
|
options?: Omit<
|
||||||
|
Parameters<typeof upCommand>[0],
|
||||||
|
'cwd' | 'directory' | 'storage' | 'reporter' | 'plugins' | 'getMigrations'
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
return upCommand({
|
return upCommand({
|
||||||
cwd: '/emigrate',
|
cwd: '/emigrate',
|
||||||
directory: 'migrations',
|
directory: 'migrations',
|
||||||
|
|
@ -509,11 +596,11 @@ function getUpCommand(migrationFiles: string[], storage?: Mocked<Storage>, plugi
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reporter,
|
reporter,
|
||||||
dry,
|
|
||||||
plugins: plugins ?? [],
|
plugins: plugins ?? [],
|
||||||
async getMigrations(cwd, directory) {
|
async getMigrations(cwd, directory) {
|
||||||
return toMigrations(cwd, directory, migrationFiles);
|
return toMigrations(cwd, directory, migrationFiles);
|
||||||
},
|
},
|
||||||
|
...options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { version } from '../get-package-info.js';
|
||||||
type ExtraFlags = {
|
type ExtraFlags = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
dry?: boolean;
|
dry?: boolean;
|
||||||
|
limit?: number;
|
||||||
getMigrations?: GetMigrationsFunction;
|
getMigrations?: GetMigrationsFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ export default async function upCommand({
|
||||||
reporter: reporterConfig,
|
reporter: reporterConfig,
|
||||||
directory,
|
directory,
|
||||||
color,
|
color,
|
||||||
|
limit,
|
||||||
dry = false,
|
dry = false,
|
||||||
plugins = [],
|
plugins = [],
|
||||||
cwd,
|
cwd,
|
||||||
|
|
@ -83,6 +85,7 @@ export default async function upCommand({
|
||||||
|
|
||||||
const error = await migrationRunner({
|
const error = await migrationRunner({
|
||||||
dry,
|
dry,
|
||||||
|
limit,
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
storage,
|
||||||
migrations: await arrayFromAsync(collectedMigrations),
|
migrations: await arrayFromAsync(collectedMigrations),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { getDuration } from './get-duration.js';
|
||||||
|
|
||||||
type MigrationRunnerParameters = {
|
type MigrationRunnerParameters = {
|
||||||
dry: boolean;
|
dry: boolean;
|
||||||
|
limit?: number;
|
||||||
reporter: EmigrateReporter;
|
reporter: EmigrateReporter;
|
||||||
storage: Storage;
|
storage: Storage;
|
||||||
migrations: Array<MigrationMetadata | MigrationMetadataFinished>;
|
migrations: Array<MigrationMetadata | MigrationMetadataFinished>;
|
||||||
|
|
@ -24,6 +25,7 @@ type MigrationRunnerParameters = {
|
||||||
|
|
||||||
export const migrationRunner = async ({
|
export const migrationRunner = async ({
|
||||||
dry,
|
dry,
|
||||||
|
limit,
|
||||||
reporter,
|
reporter,
|
||||||
storage,
|
storage,
|
||||||
migrations,
|
migrations,
|
||||||
|
|
@ -70,7 +72,12 @@ export const migrationRunner = async ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lockedMigrations, lockError] = dry ? [migrationsToRun] : await exec(async () => storage.lock(migrationsToRun));
|
const migrationsToLock = limit ? migrationsToRun.slice(0, limit) : migrationsToRun;
|
||||||
|
const migrationsToSkip = limit ? migrationsToRun.slice(limit) : [];
|
||||||
|
|
||||||
|
const [lockedMigrations, lockError] = dry
|
||||||
|
? [migrationsToLock]
|
||||||
|
: await exec(async () => storage.lock(migrationsToLock));
|
||||||
|
|
||||||
if (lockError) {
|
if (lockError) {
|
||||||
for await (const migration of migrationsToRun) {
|
for await (const migration of migrationsToRun) {
|
||||||
|
|
@ -152,6 +159,17 @@ export const migrationRunner = async ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for await (const migration of migrationsToSkip) {
|
||||||
|
const finishedMigration: MigrationMetadataFinished = {
|
||||||
|
...migration,
|
||||||
|
status: 'skipped',
|
||||||
|
};
|
||||||
|
|
||||||
|
await reporter.onMigrationSkip?.(finishedMigration);
|
||||||
|
|
||||||
|
finishedMigrations.push(finishedMigration);
|
||||||
|
}
|
||||||
|
|
||||||
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
|
const [, unlockError] = dry ? [] : await exec(async () => storage.unlock(lockedMigrations ?? []));
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue