@prairielearn/migrations 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +145 -0
  4. package/dist/batched-migrations/batched-migration-job.d.ts +42 -0
  5. package/dist/batched-migrations/batched-migration-job.js +25 -0
  6. package/dist/batched-migrations/batched-migration-job.js.map +1 -0
  7. package/dist/batched-migrations/batched-migration-job.sql +12 -0
  8. package/dist/batched-migrations/batched-migration-runner.d.ts +29 -0
  9. package/dist/batched-migrations/batched-migration-runner.js +136 -0
  10. package/dist/batched-migrations/batched-migration-runner.js.map +1 -0
  11. package/dist/batched-migrations/batched-migration-runner.sql +93 -0
  12. package/dist/batched-migrations/batched-migration-runner.test.js +185 -0
  13. package/dist/batched-migrations/batched-migration-runner.test.js.map +1 -0
  14. package/dist/batched-migrations/batched-migration.d.ts +79 -0
  15. package/dist/batched-migrations/batched-migration.js +73 -0
  16. package/dist/batched-migrations/batched-migration.js.map +1 -0
  17. package/dist/batched-migrations/batched-migration.sql +95 -0
  18. package/dist/batched-migrations/batched-migrations-runner.d.ts +63 -0
  19. package/dist/batched-migrations/batched-migrations-runner.js +272 -0
  20. package/dist/batched-migrations/batched-migrations-runner.js.map +1 -0
  21. package/dist/batched-migrations/batched-migrations-runner.sql +35 -0
  22. package/dist/batched-migrations/batched-migrations-runner.test.js +116 -0
  23. package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -0
  24. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.d.ts +9 -0
  25. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js +14 -0
  26. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js.map +1 -0
  27. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.d.ts +8 -0
  28. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js +16 -0
  29. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js.map +1 -0
  30. package/dist/batched-migrations/index.d.ts +3 -0
  31. package/dist/batched-migrations/index.js +18 -0
  32. package/dist/batched-migrations/index.js.map +1 -0
  33. package/dist/index.d.ts +3 -12
  34. package/dist/index.js +15 -192
  35. package/dist/index.js.map +1 -1
  36. package/dist/load-migrations.d.ts +8 -0
  37. package/dist/load-migrations.js +60 -0
  38. package/dist/load-migrations.js.map +1 -0
  39. package/dist/load-migrations.test.d.ts +1 -0
  40. package/dist/{index.test.js → load-migrations.test.js} +12 -65
  41. package/dist/load-migrations.test.js.map +1 -0
  42. package/dist/migrations/fixtures/20230407210430_insert_user.d.ts +1 -0
  43. package/dist/migrations/fixtures/20230407210430_insert_user.js.map +1 -0
  44. package/dist/migrations/index.d.ts +1 -0
  45. package/dist/migrations/index.js +6 -0
  46. package/dist/migrations/index.js.map +1 -0
  47. package/dist/migrations/migrations.d.ts +6 -0
  48. package/dist/migrations/migrations.js +158 -0
  49. package/dist/migrations/migrations.js.map +1 -0
  50. package/dist/migrations/migrations.test.d.ts +1 -0
  51. package/dist/migrations/migrations.test.js +78 -0
  52. package/dist/migrations/migrations.test.js.map +1 -0
  53. package/package.json +15 -12
  54. package/schema-migrations/20230303193423_batched_migrations__create.sql +49 -0
  55. package/src/batched-migrations/batched-migration-job.sql +12 -0
  56. package/src/batched-migrations/batched-migration-job.ts +34 -0
  57. package/src/batched-migrations/batched-migration-runner.sql +93 -0
  58. package/src/batched-migrations/batched-migration-runner.test.ts +208 -0
  59. package/src/batched-migrations/batched-migration-runner.ts +215 -0
  60. package/src/batched-migrations/batched-migration.sql +95 -0
  61. package/src/batched-migrations/batched-migration.ts +129 -0
  62. package/src/batched-migrations/batched-migrations-runner.sql +35 -0
  63. package/src/batched-migrations/batched-migrations-runner.test.ts +111 -0
  64. package/src/batched-migrations/batched-migrations-runner.ts +327 -0
  65. package/src/batched-migrations/fixtures/20230406184103_successful_migration.ts +13 -0
  66. package/src/batched-migrations/fixtures/20230406184107_failing_migration.js +16 -0
  67. package/src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts +15 -0
  68. package/src/batched-migrations/index.ts +21 -0
  69. package/src/index.ts +20 -201
  70. package/src/{index.test.ts → load-migrations.test.ts} +8 -73
  71. package/src/load-migrations.ts +76 -0
  72. package/src/migrations/index.ts +1 -0
  73. package/src/migrations/migrations.test.ts +80 -0
  74. package/src/migrations/migrations.ts +149 -0
  75. package/tsconfig.json +1 -1
  76. package/dist/fixtures/20230407210430_insert_user.js.map +0 -1
  77. package/dist/index.test.js.map +0 -1
  78. /package/dist/{fixtures/20230407210430_insert_user.d.ts → batched-migrations/batched-migration-runner.test.d.ts} +0 -0
  79. /package/dist/{index.test.d.ts → batched-migrations/batched-migrations-runner.test.d.ts} +0 -0
  80. /package/dist/{fixtures → migrations/fixtures}/20230407210409_create_users.sql +0 -0
  81. /package/dist/{fixtures → migrations/fixtures}/20230407210430_insert_user.js +0 -0
  82. /package/dist/{index.sql → migrations/migrations.sql} +0 -0
  83. /package/src/{fixtures → migrations/fixtures}/20230407210409_create_users.sql +0 -0
  84. /package/src/{fixtures → migrations/fixtures}/20230407210430_insert_user.ts +0 -0
  85. /package/src/{index.sql → migrations/migrations.sql} +0 -0
@@ -0,0 +1,215 @@
1
+ import {
2
+ loadSqlEquiv,
3
+ queryAsync,
4
+ queryValidatedOneRow,
5
+ queryValidatedSingleColumnOneRow,
6
+ queryValidatedZeroOrOneRow,
7
+ } from '@prairielearn/postgres';
8
+ import { logger } from '@prairielearn/logger';
9
+ import { serializeError } from 'serialize-error';
10
+ import { z } from 'zod';
11
+
12
+ import {
13
+ BatchedMigrationStatus,
14
+ BatchedMigrationRow,
15
+ updateBatchedMigrationStatus,
16
+ BatchedMigrationStatusSchema,
17
+ BatchedMigrationImplementation,
18
+ } from './batched-migration';
19
+ import {
20
+ BatchedMigrationJobRowSchema,
21
+ BatchedMigrationJobStatus,
22
+ BatchedMigrationJobRow,
23
+ } from './batched-migration-job';
24
+
25
+ const sql = loadSqlEquiv(__filename);
26
+
27
+ interface BatchedMigrationRunnerOptions {
28
+ logProgress?: boolean;
29
+ }
30
+
31
+ export class BatchedMigrationRunner {
32
+ private options: BatchedMigrationRunnerOptions;
33
+ private migration: BatchedMigrationRow;
34
+ private migrationImplementation: BatchedMigrationImplementation;
35
+ private migrationStatus: BatchedMigrationStatus;
36
+
37
+ constructor(
38
+ migration: BatchedMigrationRow,
39
+ migrationImplementation: BatchedMigrationImplementation,
40
+ options: BatchedMigrationRunnerOptions = {}
41
+ ) {
42
+ this.options = options;
43
+ this.migration = migration;
44
+ this.migrationImplementation = migrationImplementation;
45
+ this.migrationStatus = migration.status;
46
+ }
47
+
48
+ private log(message: string, ...meta: any[]) {
49
+ if (this.options.logProgress) {
50
+ logger.info(`[${this.migration.filename}] ${message}`, ...meta);
51
+ }
52
+ }
53
+
54
+ private async hasIncompleteJobs(migration: BatchedMigrationRow): Promise<boolean> {
55
+ return queryValidatedSingleColumnOneRow(
56
+ sql.batched_migration_has_incomplete_jobs,
57
+ { batched_migration_id: migration.id },
58
+ z.boolean()
59
+ );
60
+ }
61
+
62
+ private async hasFailedJobs(migration: BatchedMigrationRow): Promise<boolean> {
63
+ return queryValidatedSingleColumnOneRow(
64
+ sql.batched_migration_has_failed_jobs,
65
+ { batched_migration_id: migration.id },
66
+ z.boolean()
67
+ );
68
+ }
69
+
70
+ private async refreshMigrationStatus(migration: BatchedMigrationRow) {
71
+ this.migrationStatus = await queryValidatedSingleColumnOneRow(
72
+ sql.get_migration_status,
73
+ {
74
+ id: migration.id,
75
+ },
76
+ BatchedMigrationStatusSchema
77
+ );
78
+ }
79
+
80
+ private async finishRunningMigration(migration: BatchedMigrationRow) {
81
+ // Safety check: if there are any pending jobs, don't mark this
82
+ // migration as finished.
83
+ if (await this.hasIncompleteJobs(migration)) {
84
+ this.log(`Incomplete jobs found, not marking as finished`);
85
+ return;
86
+ }
87
+
88
+ const hasFailedJobs = await this.hasFailedJobs(migration);
89
+ const finalStatus = hasFailedJobs ? 'failed' : 'succeeded';
90
+ await updateBatchedMigrationStatus(migration.id, finalStatus);
91
+ this.log(`Finished with status '${finalStatus}'`);
92
+ }
93
+
94
+ private async getNextBatchBounds(
95
+ migration: BatchedMigrationRow
96
+ ): Promise<null | [bigint, bigint]> {
97
+ const lastJob = await queryValidatedZeroOrOneRow(
98
+ sql.select_last_batched_migration_job,
99
+ { batched_migration_id: migration.id },
100
+ BatchedMigrationJobRowSchema
101
+ );
102
+
103
+ const nextMin = lastJob ? lastJob.max_value + 1n : migration.min_value;
104
+ if (nextMin > migration.max_value) return null;
105
+
106
+ let nextMax = nextMin + BigInt(migration.batch_size) - 1n;
107
+ if (nextMax > migration.max_value) nextMax = migration.max_value;
108
+
109
+ return [nextMin, nextMax];
110
+ }
111
+
112
+ private async startJob(job: BatchedMigrationJobRow) {
113
+ await queryAsync(sql.start_batched_migration_job, { id: job.id });
114
+ const jobRange = `[${job.min_value}, ${job.max_value}]`;
115
+ const migrationRange = `[${this.migration.min_value}, ${this.migration.max_value}]`;
116
+ this.log(`Started job ${job.id} for range ${jobRange} in ${migrationRange}`);
117
+ }
118
+
119
+ private serializeJobData(data: unknown) {
120
+ if (data == null) return null;
121
+
122
+ // Return JSON-stringified data. Convert BigInts to strings.
123
+ return JSON.stringify(data, (_key, value) => {
124
+ if (typeof value === 'bigint') return value.toString();
125
+ return value;
126
+ });
127
+ }
128
+
129
+ private async finishJob(
130
+ job: BatchedMigrationJobRow,
131
+ status: Extract<BatchedMigrationJobStatus, 'failed' | 'succeeded'>,
132
+ data?: unknown
133
+ ) {
134
+ await queryAsync(sql.finish_batched_migration_job, {
135
+ id: job.id,
136
+ status,
137
+ data: this.serializeJobData(data),
138
+ });
139
+ this.log(`Job ${job.id} finished with status '${status}'`);
140
+ }
141
+
142
+ private async getOrCreateNextMigrationJob(
143
+ migration: BatchedMigrationRow
144
+ ): Promise<BatchedMigrationJobRow | null> {
145
+ const nextBatchBounds = await this.getNextBatchBounds(migration);
146
+ if (nextBatchBounds) {
147
+ return queryValidatedOneRow(
148
+ sql.insert_batched_migration_job,
149
+ {
150
+ batched_migration_id: migration.id,
151
+ min_value: nextBatchBounds[0],
152
+ max_value: nextBatchBounds[1],
153
+ },
154
+ BatchedMigrationJobRowSchema
155
+ );
156
+ } else {
157
+ // Pick up any old pending jobs from this migration. These will only exist if
158
+ // an admin manually elected to retry all failed jobs; we'll never automatically
159
+ // transition failed jobs back to pending.
160
+ return queryValidatedZeroOrOneRow(
161
+ sql.select_first_pending_batched_migration_job,
162
+ { batched_migration_id: migration.id },
163
+ BatchedMigrationJobRowSchema
164
+ );
165
+ }
166
+ }
167
+
168
+ private async runMigrationJob(
169
+ migration: BatchedMigrationRow,
170
+ migrationImplementation: BatchedMigrationImplementation
171
+ ) {
172
+ const nextJob = await this.getOrCreateNextMigrationJob(migration);
173
+ if (nextJob) {
174
+ await this.startJob(nextJob);
175
+
176
+ let error = null;
177
+ try {
178
+ // We'll only handle errors thrown by the migration itself. If any of
179
+ // our own execution machinery throws an error, we'll let it bubble up.
180
+ await migrationImplementation.execute(nextJob.min_value, nextJob.max_value);
181
+ } catch (err) {
182
+ error = err;
183
+ }
184
+
185
+ if (error) {
186
+ await this.finishJob(nextJob, 'failed', { error: serializeError(error) });
187
+ } else {
188
+ await this.finishJob(nextJob, 'succeeded');
189
+ }
190
+ } else {
191
+ await this.finishRunningMigration(migration);
192
+ }
193
+ }
194
+
195
+ async run({
196
+ signal,
197
+ iterations,
198
+ durationMs,
199
+ }: { signal?: AbortSignal; iterations?: number; durationMs?: number } = {}) {
200
+ let iterationCount = 0;
201
+ const endTime = durationMs ? Date.now() + durationMs : null;
202
+ while (
203
+ !signal?.aborted &&
204
+ (iterations ? iterationCount < iterations : true) &&
205
+ (endTime ? Date.now() < endTime : true) &&
206
+ (this.migrationStatus === 'running' || this.migrationStatus === 'finalizing')
207
+ ) {
208
+ await this.runMigrationJob(this.migration, this.migrationImplementation);
209
+ iterationCount += 1;
210
+ // Always refresh the status so we can detect if the migration was marked
211
+ // as paused by another process.
212
+ await this.refreshMigrationStatus(this.migration);
213
+ }
214
+ }
215
+ }
@@ -0,0 +1,95 @@
1
+ -- BLOCK insert_batched_migration
2
+ INSERT INTO
3
+ batched_migrations (
4
+ project,
5
+ filename,
6
+ timestamp,
7
+ batch_size,
8
+ min_value,
9
+ max_value,
10
+ status,
11
+ started_at
12
+ )
13
+ VALUES
14
+ (
15
+ $project,
16
+ $filename,
17
+ $timestamp,
18
+ $batch_size,
19
+ $min_value,
20
+ $max_value,
21
+ $status,
22
+ -- If the migration is marked as already having succeeded, set `started_at`
23
+ -- since the migration did technically start.
24
+ CASE
25
+ WHEN $status::enum_batched_migration_status = 'succeeded' THEN CURRENT_TIMESTAMP
26
+ ELSE NULL
27
+ END
28
+ )
29
+ ON CONFLICT DO NOTHING
30
+ RETURNING
31
+ *;
32
+
33
+ -- BLOCK select_all_batched_migrations
34
+ SELECT
35
+ *
36
+ FROM
37
+ batched_migrations
38
+ WHERE
39
+ project = $project
40
+ ORDER BY
41
+ id ASC;
42
+
43
+ -- BLOCK select_batched_migration
44
+ SELECT
45
+ *
46
+ FROM
47
+ batched_migrations
48
+ WHERE
49
+ project = $project
50
+ AND id = $id;
51
+
52
+ -- BLOCK select_batched_migration_for_timestamp
53
+ SELECT
54
+ *
55
+ FROM
56
+ batched_migrations
57
+ WHERE
58
+ project = $project
59
+ AND timestamp = $timestamp;
60
+
61
+ -- BLOCK update_batched_migration_status
62
+ UPDATE batched_migrations
63
+ SET
64
+ status = $status,
65
+ updated_at = CURRENT_TIMESTAMP
66
+ WHERE
67
+ id = $id
68
+ RETURNING
69
+ *;
70
+
71
+ -- BLOCK retry_failed_jobs
72
+ WITH
73
+ updated_batched_migration AS (
74
+ UPDATE batched_migrations
75
+ SET
76
+ status = 'running',
77
+ started_at = CURRENT_TIMESTAMP,
78
+ updated_at = CURRENT_TIMESTAMP
79
+ WHERE
80
+ project = $project
81
+ AND id = $id
82
+ RETURNING
83
+ *
84
+ )
85
+ UPDATE batched_migration_jobs
86
+ SET
87
+ status = 'pending',
88
+ started_at = NULL,
89
+ finished_at = NULL,
90
+ updated_at = CURRENT_TIMESTAMP
91
+ FROM
92
+ updated_batched_migration
93
+ WHERE
94
+ batched_migration_id = updated_batched_migration.id
95
+ AND batched_migration_jobs.status = 'failed';
@@ -0,0 +1,129 @@
1
+ import {
2
+ loadSqlEquiv,
3
+ queryAsync,
4
+ queryValidatedOneRow,
5
+ queryValidatedRows,
6
+ queryValidatedZeroOrOneRow,
7
+ } from '@prairielearn/postgres';
8
+ import { z } from 'zod';
9
+
10
+ const sql = loadSqlEquiv(__filename);
11
+
12
+ export const BatchedMigrationStatusSchema = z.enum([
13
+ 'pending',
14
+ 'paused',
15
+ 'running',
16
+ 'finalizing',
17
+ 'failed',
18
+ 'succeeded',
19
+ ]);
20
+ export type BatchedMigrationStatus = z.infer<typeof BatchedMigrationStatusSchema>;
21
+
22
+ export const BatchedMigrationRowSchema = z.object({
23
+ id: z.string(),
24
+ project: z.string(),
25
+ filename: z.string(),
26
+ timestamp: z.string(),
27
+ batch_size: z.number(),
28
+ min_value: z.bigint({ coerce: true }),
29
+ max_value: z.bigint({ coerce: true }),
30
+ status: BatchedMigrationStatusSchema,
31
+ created_at: z.date(),
32
+ updated_at: z.date(),
33
+ started_at: z.date().nullable(),
34
+ });
35
+ export type BatchedMigrationRow = z.infer<typeof BatchedMigrationRowSchema>;
36
+
37
+ export interface BatchedMigrationParameters {
38
+ min?: bigint | null;
39
+ max: bigint | null;
40
+ batchSize?: number;
41
+ }
42
+
43
+ export interface BatchedMigrationImplementation {
44
+ getParameters(): Promise<BatchedMigrationParameters>;
45
+ execute(start: bigint, end: bigint): Promise<void>;
46
+ }
47
+
48
+ /**
49
+ * Identity function that helps to write correct batched migrations.
50
+ */
51
+ export function makeBatchedMigration<T extends BatchedMigrationImplementation>(fns: T): T {
52
+ validateBatchedMigrationImplementation(fns);
53
+ return fns;
54
+ }
55
+
56
+ export function validateBatchedMigrationImplementation(
57
+ fns: BatchedMigrationImplementation
58
+ ): asserts fns is BatchedMigrationImplementation {
59
+ if (typeof fns.getParameters !== 'function') {
60
+ throw new Error('getParameters() must be a function');
61
+ }
62
+ if (typeof fns.execute !== 'function') {
63
+ throw new Error('execute() must be a function');
64
+ }
65
+ }
66
+
67
+ type NewBatchedMigration = Pick<
68
+ BatchedMigrationRow,
69
+ 'project' | 'filename' | 'timestamp' | 'batch_size' | 'min_value' | 'max_value' | 'status'
70
+ >;
71
+
72
+ /**
73
+ * Inserts a new batched migration. If one already exists for the given
74
+ * project/timestamp pair, returns null, otherwise returns the inserted row.
75
+ */
76
+ export async function insertBatchedMigration(
77
+ migration: NewBatchedMigration
78
+ ): Promise<BatchedMigrationRow | null> {
79
+ return queryValidatedZeroOrOneRow(
80
+ sql.insert_batched_migration,
81
+ migration,
82
+ BatchedMigrationRowSchema
83
+ );
84
+ }
85
+
86
+ export async function selectAllBatchedMigrations(project: string) {
87
+ return queryValidatedRows(
88
+ sql.select_all_batched_migrations,
89
+ { project },
90
+ BatchedMigrationRowSchema
91
+ );
92
+ }
93
+
94
+ export async function selectBatchedMigration(
95
+ project: string,
96
+ id: string
97
+ ): Promise<BatchedMigrationRow> {
98
+ return queryValidatedOneRow(
99
+ sql.select_batched_migration,
100
+ { project, id },
101
+ BatchedMigrationRowSchema
102
+ );
103
+ }
104
+
105
+ export async function selectBatchedMigrationForTimestamp(
106
+ project: string,
107
+ timestamp: string
108
+ ): Promise<BatchedMigrationRow> {
109
+ return queryValidatedOneRow(
110
+ sql.select_batched_migration_for_timestamp,
111
+ { project, timestamp },
112
+ BatchedMigrationRowSchema
113
+ );
114
+ }
115
+
116
+ export async function updateBatchedMigrationStatus(
117
+ id: string,
118
+ status: BatchedMigrationStatus
119
+ ): Promise<BatchedMigrationRow> {
120
+ return queryValidatedOneRow(
121
+ sql.update_batched_migration_status,
122
+ { id, status },
123
+ BatchedMigrationRowSchema
124
+ );
125
+ }
126
+
127
+ export async function retryFailedBatchedMigrationJobs(project: string, id: string): Promise<void> {
128
+ await queryAsync(sql.retry_failed_jobs, { project, id });
129
+ }
@@ -0,0 +1,35 @@
1
+ -- BLOCK select_running_migration
2
+ SELECT
3
+ *
4
+ FROM
5
+ batched_migrations
6
+ WHERE
7
+ status = 'running'
8
+ AND project = $project
9
+ ORDER BY
10
+ id ASC
11
+ LIMIT
12
+ 1;
13
+
14
+ -- BLOCK start_next_pending_migration
15
+ UPDATE batched_migrations
16
+ SET
17
+ status = 'running',
18
+ started_at = CURRENT_TIMESTAMP,
19
+ updated_at = CURRENT_TIMESTAMP
20
+ WHERE
21
+ id = (
22
+ SELECT
23
+ id
24
+ FROM
25
+ batched_migrations
26
+ WHERE
27
+ status = 'pending'
28
+ AND project = $project
29
+ ORDER BY
30
+ id ASC
31
+ LIMIT
32
+ 1
33
+ )
34
+ RETURNING
35
+ *;
@@ -0,0 +1,111 @@
1
+ import chai, { assert } from 'chai';
2
+ import chaiAsPromised from 'chai-as-promised';
3
+ import path from 'node:path';
4
+ import { makePostgresTestUtils } from '@prairielearn/postgres';
5
+ import * as namedLocks from '@prairielearn/named-locks';
6
+
7
+ import { SCHEMA_MIGRATIONS_PATH, init } from '../index';
8
+ import { BatchedMigrationsRunner } from './batched-migrations-runner';
9
+ import { selectAllBatchedMigrations } from './batched-migration';
10
+
11
+ chai.use(chaiAsPromised);
12
+
13
+ const postgresTestUtils = makePostgresTestUtils({
14
+ database: 'prairielearn_migrations',
15
+ });
16
+
17
+ describe('BatchedMigrationsRunner', () => {
18
+ before(async () => {
19
+ await postgresTestUtils.createDatabase();
20
+ await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {
21
+ throw err;
22
+ });
23
+ await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');
24
+ });
25
+
26
+ afterEach(async () => {
27
+ await postgresTestUtils.resetDatabase();
28
+ });
29
+
30
+ after(async () => {
31
+ await namedLocks.close();
32
+ await postgresTestUtils.dropDatabase();
33
+ });
34
+
35
+ it('enqueues migrations', async () => {
36
+ const runner = new BatchedMigrationsRunner({
37
+ project: 'test',
38
+ directories: [path.join(__dirname, 'fixtures')],
39
+ });
40
+
41
+ await runner.enqueueBatchedMigration('20230406184103_successful_migration');
42
+ await runner.enqueueBatchedMigration('20230406184107_failing_migration');
43
+ await runner.enqueueBatchedMigration('20230407230446_no_rows_migration');
44
+
45
+ const migrations = await selectAllBatchedMigrations('test');
46
+
47
+ assert.lengthOf(migrations, 3);
48
+ assert.equal(migrations[0].timestamp, '20230406184103');
49
+ assert.equal(migrations[0].filename, '20230406184103_successful_migration.ts');
50
+ assert.equal(migrations[0].status, 'pending');
51
+ assert.equal(migrations[1].timestamp, '20230406184107');
52
+ assert.equal(migrations[1].filename, '20230406184107_failing_migration.js');
53
+ assert.equal(migrations[1].status, 'pending');
54
+ assert.equal(migrations[2].timestamp, '20230407230446');
55
+ assert.equal(migrations[2].filename, '20230407230446_no_rows_migration.ts');
56
+ assert.equal(migrations[2].status, 'succeeded');
57
+ });
58
+
59
+ it('safely enqueues migrations multiple times', async () => {
60
+ const runner = new BatchedMigrationsRunner({
61
+ project: 'test',
62
+ directories: [path.join(__dirname, 'fixtures')],
63
+ });
64
+
65
+ await runner.enqueueBatchedMigration('20230406184103_successful_migration');
66
+ await runner.enqueueBatchedMigration('20230406184103_successful_migration');
67
+ await runner.enqueueBatchedMigration('20230406184103_successful_migration');
68
+
69
+ const migrations = await selectAllBatchedMigrations('test');
70
+
71
+ assert.lengthOf(migrations, 1);
72
+ });
73
+
74
+ it('finalizes a successful migration', async () => {
75
+ const runner = new BatchedMigrationsRunner({
76
+ project: 'test',
77
+ directories: [path.join(__dirname, 'fixtures')],
78
+ });
79
+
80
+ await runner.enqueueBatchedMigration('20230406184103_successful_migration');
81
+ await runner.finalizeBatchedMigration('20230406184103_successful_migration', {
82
+ logProgress: false,
83
+ });
84
+
85
+ const migrations = await selectAllBatchedMigrations('test');
86
+ assert.lengthOf(migrations, 1);
87
+ assert.equal(migrations[0].timestamp, '20230406184103');
88
+ assert.equal(migrations[0].status, 'succeeded');
89
+ });
90
+
91
+ it('finalizes a failing migration', async () => {
92
+ const runner = new BatchedMigrationsRunner({
93
+ project: 'test',
94
+ directories: [path.join(__dirname, 'fixtures')],
95
+ });
96
+
97
+ await runner.enqueueBatchedMigration('20230406184107_failing_migration');
98
+
99
+ await assert.isRejected(
100
+ runner.finalizeBatchedMigration('20230406184107_failing_migration', {
101
+ logProgress: false,
102
+ }),
103
+ "but it is 'failed'"
104
+ );
105
+
106
+ const migrations = await selectAllBatchedMigrations('test');
107
+ assert.lengthOf(migrations, 1);
108
+ assert.equal(migrations[0].timestamp, '20230406184107');
109
+ assert.equal(migrations[0].status, 'failed');
110
+ });
111
+ });