@prairielearn/migrations 1.0.0 → 1.2.0

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 (82) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +145 -0
  3. package/dist/batched-migrations/batched-migration-job.d.ts +42 -0
  4. package/dist/batched-migrations/batched-migration-job.js +25 -0
  5. package/dist/batched-migrations/batched-migration-job.js.map +1 -0
  6. package/dist/batched-migrations/batched-migration-job.sql +12 -0
  7. package/dist/batched-migrations/batched-migration-runner.d.ts +28 -0
  8. package/dist/batched-migrations/batched-migration-runner.js +136 -0
  9. package/dist/batched-migrations/batched-migration-runner.js.map +1 -0
  10. package/dist/batched-migrations/batched-migration-runner.sql +93 -0
  11. package/dist/batched-migrations/batched-migration-runner.test.js +185 -0
  12. package/dist/batched-migrations/batched-migration-runner.test.js.map +1 -0
  13. package/dist/batched-migrations/batched-migration.d.ts +79 -0
  14. package/dist/batched-migrations/batched-migration.js +73 -0
  15. package/dist/batched-migrations/batched-migration.js.map +1 -0
  16. package/dist/batched-migrations/batched-migration.sql +95 -0
  17. package/dist/batched-migrations/batched-migrations-runner.d.ts +63 -0
  18. package/dist/batched-migrations/batched-migrations-runner.js +273 -0
  19. package/dist/batched-migrations/batched-migrations-runner.js.map +1 -0
  20. package/dist/batched-migrations/batched-migrations-runner.sql +35 -0
  21. package/dist/batched-migrations/batched-migrations-runner.test.d.ts +1 -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 -11
  34. package/dist/index.js +15 -167
  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 -44
  41. package/dist/load-migrations.test.js.map +1 -0
  42. package/dist/migrations/fixtures/20230407210409_create_users.sql +2 -0
  43. package/dist/migrations/fixtures/20230407210430_insert_user.d.ts +1 -0
  44. package/dist/migrations/fixtures/20230407210430_insert_user.js +7 -0
  45. package/dist/migrations/fixtures/20230407210430_insert_user.js.map +1 -0
  46. package/dist/migrations/index.d.ts +1 -0
  47. package/dist/migrations/index.js +6 -0
  48. package/dist/migrations/index.js.map +1 -0
  49. package/dist/migrations/migrations.d.ts +6 -0
  50. package/dist/migrations/migrations.js +159 -0
  51. package/dist/migrations/migrations.js.map +1 -0
  52. package/dist/migrations/migrations.test.d.ts +1 -0
  53. package/dist/migrations/migrations.test.js +78 -0
  54. package/dist/migrations/migrations.test.js.map +1 -0
  55. package/package.json +16 -8
  56. package/schema-migrations/20230303193423_batched_migrations__create.sql +49 -0
  57. package/src/batched-migrations/batched-migration-job.sql +12 -0
  58. package/src/batched-migrations/batched-migration-job.ts +34 -0
  59. package/src/batched-migrations/batched-migration-runner.sql +93 -0
  60. package/src/batched-migrations/batched-migration-runner.test.ts +208 -0
  61. package/src/batched-migrations/batched-migration-runner.ts +215 -0
  62. package/src/batched-migrations/batched-migration.sql +95 -0
  63. package/src/batched-migrations/batched-migration.ts +129 -0
  64. package/src/batched-migrations/batched-migrations-runner.sql +35 -0
  65. package/src/batched-migrations/batched-migrations-runner.test.ts +111 -0
  66. package/src/batched-migrations/batched-migrations-runner.ts +327 -0
  67. package/src/batched-migrations/fixtures/20230406184103_successful_migration.ts +13 -0
  68. package/src/batched-migrations/fixtures/20230406184107_failing_migration.js +16 -0
  69. package/src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts +15 -0
  70. package/src/batched-migrations/index.ts +21 -0
  71. package/src/index.ts +20 -173
  72. package/src/{index.test.ts → load-migrations.test.ts} +10 -48
  73. package/src/load-migrations.ts +76 -0
  74. package/src/migrations/fixtures/20230407210409_create_users.sql +2 -0
  75. package/src/migrations/fixtures/20230407210430_insert_user.ts +5 -0
  76. package/src/migrations/index.ts +1 -0
  77. package/src/migrations/migrations.test.ts +80 -0
  78. package/src/migrations/migrations.ts +149 -0
  79. package/dist/index.test.js.map +0 -1
  80. /package/dist/{index.test.d.ts → batched-migrations/batched-migration-runner.test.d.ts} +0 -0
  81. /package/dist/{index.sql → migrations/migrations.sql} +0 -0
  82. /package/src/{index.sql → migrations/migrations.sql} +0 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # @prairielearn/migrations
2
+
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4cc962358: Add support for asynchronous batched migrations
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [4cc962358]
12
+ - @prairielearn/named-locks@1.3.0
13
+
14
+ ## 1.1.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 400a0b901: Use automatically-renewing named lock
19
+ - 751010ea3: Support JavaScript migration files
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [400a0b901]
24
+ - @prairielearn/named-locks@1.2.0
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # `@prairielearn/migrations`
2
+
3
+ This package runs two types of database migrations:
4
+
5
+ - **Regular migrations**, which run immediately and typically change the database schema (adding/removing tables, columns, indexes, etc.)
6
+ - **Batched migrations**, which run asynchronously over batches of data within a given table.
7
+
8
+ ## Usage
9
+
10
+ ### Regular migrations
11
+
12
+ Regular migrations can be authored as either SQL or JavaScript. They should be located within one or more directories. They are uniquely identified by a 14-character timestamp at the start of their filename.
13
+
14
+ ```sql
15
+ -- migrations/20230411002409_example_migration.sql
16
+ CREATE TABLE IF NOT EXISTS
17
+ examples (id BIGSERIAL PRIMARY KEY, value TEXT NOT NULL);
18
+ ```
19
+
20
+ ```ts
21
+ // migrations/20230411002409_example_migration.ts
22
+ module.exports = async function () {
23
+ console.log('something useful.');
24
+ };
25
+ ```
26
+
27
+ ### Batched migrations
28
+
29
+ Batched migrations are useful for when one needs to make changes to many rows within a table, for instance backfilling a new column from existing data. While one could technically do this with the schema migrations machinery, that has a number of disadvantages:
30
+
31
+ - Doing an update all in one go (e.g. `UPDATE table_name SET column = 'some value' WHERE column IS NULL`) has the potential to lock the table for a long time. For zero-downtime deploys, this is unacceptable.
32
+ - Schema migrations are expected to run synchronously during deploy. So even if you wrote JavaScript code to manually batch up a table to avoid locks, you'd have to babysit a long-running process.
33
+ - If errors are encountered, you'll have to figure out how to manually retry the change for the failing batches.
34
+
35
+ By using batched migrations, these problems are avoided:
36
+
37
+ - Work is done in small batches, so large numbers of rows (or even entire tables) are not locked for long periods of time.
38
+ - Work is done asynchronously in the background, so migrations that operate on very large tables won't block deploys.
39
+ - Each batch is tracked independently and failing batched can be easily retried.
40
+
41
+ #### Writing batched migrations
42
+
43
+ Batched migrations are written as an object with two functions:
44
+
45
+ - `getParameters()`: returns the minimum and maximum IDs to operate on, as well as a batch size. `min` defaults to `1` and `batchSize` defaults to `1000`. If `max === null`, that indicates that there are no rows to operate on.
46
+ - `execute(min, max)`: runs the migration on the given range of IDs, inclusive of its endpoints.
47
+
48
+ A `makeBatchedMigration()` function is available to help ensure you're writing an object with the correct shape.
49
+
50
+ ```ts
51
+ // batched-migrations/20230411002409_example_migration.ts
52
+ import { makeBatchedMigration } from '@prairielearn/migrations';
53
+ import { queryOneRowAsync, queryAsync } from '@prairielearn/postgres';
54
+
55
+ export default makeBatchedMigration({
56
+ async getParameters() {
57
+ const result = await queryOneRowAsync('SELECT MAX(id) as max from examples;', {});
58
+ return {
59
+ max: result.rows[0].max,
60
+ batchSize: 1000,
61
+ };
62
+ },
63
+
64
+ async execute(min: bigint, max: bigint) {
65
+ await queryAsync('UPDATE examples SET text = TRIM(text) WHERE id >= $min AND id <= $max', {
66
+ min,
67
+ max,
68
+ });
69
+ },
70
+ });
71
+ ```
72
+
73
+ Batched migration `execute()` functions **must** be idempotent, as they may run multiple times on the same ID range in the case of retries after failure.
74
+
75
+ #### Executing batched migrations
76
+
77
+ Unlike regular migrations, batched migrations aren't automatically started. Instead, you must write a regular migration to call `enqueueBatchedMigration()` to explicitly start a given batched migration. This provides precise control over execution order.
78
+
79
+ ```ts
80
+ // migrations/20230411002409_start_batched_migration__example_migration.ts
81
+ import { enqueueBatchedMigration } from '@prairielearn/migrations';
82
+
83
+ export default async function () {
84
+ await enqueueBatchedMigration('20230411002409_example_migration');
85
+ }
86
+ ```
87
+
88
+ This will queue the batched migration for execution.
89
+
90
+ You may need to ensure that a given batched migration has succeeded before running a subsequent regular migration. For instance, you might have a batched migration that copies a column from one table to another, and you want to ensure that all data has been copied before you delete the original column. You can achieve this by "finalizing" the migration with `finalizeBatchedMigration()`. This will synchronously execute any remaining batches, and will error if the migration ends up in a failed state. This gives you a chance to fix any errors and retry the failed jobs.
91
+
92
+ ```ts
93
+ // migrations/20230411002409_finalize_batched_migration__example_migration.ts
94
+ import { finalizeBatchedMigration } from '@prairielearn/migrations';
95
+
96
+ export default async function () {
97
+ await finalizeBatchedMigration('20230411002409_finalize_batched_migration__example_migration');
98
+ }
99
+ ```
100
+
101
+ In most cases, you'll want to do your best to ensure that the given batched migration has finished _before_ deploying a migration that finalizes it. That way, `finalizeBatchedMigration()` will just have to assert that the migration has already successfully executed. However, finalizing a migration is still an important part of preventing data loss or inconsistencies for many migrations.
102
+
103
+ ### Server setup
104
+
105
+ To execute any pending regular migrations, call `init()` early on in your application startup code. The first argument is an array of directory paths containing migration files as described above. The second argument is a project identifier, which is used to isolate multiple migration sequences from each other when two or more applications share a single database.
106
+
107
+ ```ts
108
+ import { init } from '@prairielearn/migrations';
109
+
110
+ await init([path.join(__dirname, 'migrations')], 'prairielearn');
111
+ ```
112
+
113
+ If you want to make use of batched migrations, you'll need to do some additional setup. Since batched migrations are typically used with regular migrations, you'll need to take care to call `init()` after `initBatchedMigrations()` but before `startBatchedMigrations()`.
114
+
115
+ ```ts
116
+ import {
117
+ init,
118
+ initBatchedMigrations,
119
+ startBatchedMigrations,
120
+ stopBatchedMigrations,
121
+ } from '@prairielearn/migrations';
122
+
123
+ const runner = initBatchedMigrations({
124
+ project: 'prairielearn',
125
+ directories: [path.join(__dirname, 'batched-migrations')],
126
+ });
127
+ runner.on('error', (error) => {
128
+ // Handle error, e.g. by reporting to Sentry.
129
+ });
130
+
131
+ await init([path.join(__dirname, 'migrations')], 'prairielearn');
132
+
133
+ startBatchedMigrations({
134
+ workDurationMs: 60_000,
135
+ sleepDurationMs: 30_000,
136
+ });
137
+ ```
138
+
139
+ If you want to gracefully shut down your server, you can stop processing batched migrations and wait for any in-progress jobs to finish.
140
+
141
+ ```ts
142
+ import { stopBatchedMigrations } from '@prairielearn/migrations';
143
+
144
+ await stopBatchedMigrations();
145
+ ```
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ export declare const BatchedMigrationJobStatusSchema: z.ZodEnum<["pending", "failed", "succeeded"]>;
3
+ export type BatchedMigrationJobStatus = z.infer<typeof BatchedMigrationJobStatusSchema>;
4
+ export declare const BatchedMigrationJobRowSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ batched_migration_id: z.ZodString;
7
+ min_value: z.ZodBigInt;
8
+ max_value: z.ZodBigInt;
9
+ status: z.ZodEnum<["pending", "failed", "succeeded"]>;
10
+ attempts: z.ZodNumber;
11
+ created_at: z.ZodDate;
12
+ updated_at: z.ZodDate;
13
+ started_at: z.ZodNullable<z.ZodDate>;
14
+ finished_at: z.ZodNullable<z.ZodDate>;
15
+ data: z.ZodUnknown;
16
+ }, "strip", z.ZodTypeAny, {
17
+ status: "pending" | "failed" | "succeeded";
18
+ id: string;
19
+ min_value: bigint;
20
+ max_value: bigint;
21
+ created_at: Date;
22
+ updated_at: Date;
23
+ started_at: Date | null;
24
+ batched_migration_id: string;
25
+ attempts: number;
26
+ finished_at: Date | null;
27
+ data?: unknown;
28
+ }, {
29
+ status: "pending" | "failed" | "succeeded";
30
+ id: string;
31
+ min_value: bigint;
32
+ max_value: bigint;
33
+ created_at: Date;
34
+ updated_at: Date;
35
+ started_at: Date | null;
36
+ batched_migration_id: string;
37
+ attempts: number;
38
+ finished_at: Date | null;
39
+ data?: unknown;
40
+ }>;
41
+ export type BatchedMigrationJobRow = z.infer<typeof BatchedMigrationJobRowSchema>;
42
+ export declare function selectRecentJobsWithStatus(batchedMigrationId: string, status: BatchedMigrationJobStatus, limit: number): Promise<BatchedMigrationJobRow[]>;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectRecentJobsWithStatus = exports.BatchedMigrationJobRowSchema = exports.BatchedMigrationJobStatusSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const postgres_1 = require("@prairielearn/postgres");
6
+ const sql = (0, postgres_1.loadSqlEquiv)(__filename);
7
+ exports.BatchedMigrationJobStatusSchema = zod_1.z.enum(['pending', 'failed', 'succeeded']);
8
+ exports.BatchedMigrationJobRowSchema = zod_1.z.object({
9
+ id: zod_1.z.string(),
10
+ batched_migration_id: zod_1.z.string(),
11
+ min_value: zod_1.z.bigint({ coerce: true }),
12
+ max_value: zod_1.z.bigint({ coerce: true }),
13
+ status: exports.BatchedMigrationJobStatusSchema,
14
+ attempts: zod_1.z.number(),
15
+ created_at: zod_1.z.date(),
16
+ updated_at: zod_1.z.date(),
17
+ started_at: zod_1.z.date().nullable(),
18
+ finished_at: zod_1.z.date().nullable(),
19
+ data: zod_1.z.unknown(),
20
+ });
21
+ async function selectRecentJobsWithStatus(batchedMigrationId, status, limit) {
22
+ return (0, postgres_1.queryValidatedRows)(sql.select_recent_jobs_with_status, { batched_migration_id: batchedMigrationId, status, limit }, exports.BatchedMigrationJobRowSchema);
23
+ }
24
+ exports.selectRecentJobsWithStatus = selectRecentJobsWithStatus;
25
+ //# sourceMappingURL=batched-migration-job.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batched-migration-job.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-job.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,qDAA0E;AAE1E,MAAM,GAAG,GAAG,IAAA,uBAAY,EAAC,UAAU,CAAC,CAAC;AAExB,QAAA,+BAA+B,GAAG,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;AAG7E,QAAA,4BAA4B,GAAG,OAAC,CAAC,MAAM,CAAC;IACnD,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE;IACd,oBAAoB,EAAE,OAAC,CAAC,MAAM,EAAE;IAChC,SAAS,EAAE,OAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,SAAS,EAAE,OAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,MAAM,EAAE,uCAA+B;IACvC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE;IACpB,UAAU,EAAE,OAAC,CAAC,IAAI,EAAE;IACpB,UAAU,EAAE,OAAC,CAAC,IAAI,EAAE;IACpB,UAAU,EAAE,OAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAC/B,WAAW,EAAE,OAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAChC,IAAI,EAAE,OAAC,CAAC,OAAO,EAAE;CAClB,CAAC,CAAC;AAGI,KAAK,UAAU,0BAA0B,CAC9C,kBAA0B,EAC1B,MAAiC,EACjC,KAAa;IAEb,OAAO,IAAA,6BAAkB,EACvB,GAAG,CAAC,8BAA8B,EAClC,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAK,EAAE,EAC3D,oCAA4B,CAC7B,CAAC;AACJ,CAAC;AAVD,gEAUC"}
@@ -0,0 +1,12 @@
1
+ -- BLOCK select_recent_jobs_with_status
2
+ SELECT
3
+ *
4
+ FROM
5
+ batched_migration_jobs
6
+ WHERE
7
+ batched_migration_id = $batched_migration_id
8
+ AND status = $status
9
+ ORDER BY
10
+ max_value DESC
11
+ LIMIT
12
+ $limit;
@@ -0,0 +1,28 @@
1
+ import { BatchedMigrationRow, BatchedMigrationImplementation } from './batched-migration';
2
+ interface BatchedMigrationRunnerOptions {
3
+ logProgress?: boolean;
4
+ }
5
+ export declare class BatchedMigrationRunner {
6
+ private options;
7
+ private migration;
8
+ private migrationImplementation;
9
+ private migrationStatus;
10
+ constructor(migration: BatchedMigrationRow, migrationImplementation: BatchedMigrationImplementation, options?: BatchedMigrationRunnerOptions);
11
+ private log;
12
+ private hasIncompleteJobs;
13
+ private hasFailedJobs;
14
+ private refreshMigrationStatus;
15
+ private finishRunningMigration;
16
+ private getNextBatchBounds;
17
+ private startJob;
18
+ private serializeJobData;
19
+ private finishJob;
20
+ private getOrCreateNextMigrationJob;
21
+ private runMigrationJob;
22
+ run({ signal, iterations, durationMs, }?: {
23
+ signal?: AbortSignal;
24
+ iterations?: number;
25
+ durationMs?: number;
26
+ }): Promise<void>;
27
+ }
28
+ export {};
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BatchedMigrationRunner = void 0;
4
+ const postgres_1 = require("@prairielearn/postgres");
5
+ const logger_1 = require("@prairielearn/logger");
6
+ const serialize_error_1 = require("serialize-error");
7
+ const zod_1 = require("zod");
8
+ const batched_migration_1 = require("./batched-migration");
9
+ const batched_migration_job_1 = require("./batched-migration-job");
10
+ const sql = (0, postgres_1.loadSqlEquiv)(__filename);
11
+ class BatchedMigrationRunner {
12
+ constructor(migration, migrationImplementation, options = {}) {
13
+ this.options = options;
14
+ this.migration = migration;
15
+ this.migrationImplementation = migrationImplementation;
16
+ this.migrationStatus = migration.status;
17
+ }
18
+ log(message, ...meta) {
19
+ if (this.options.logProgress) {
20
+ logger_1.logger.info(`[${this.migration.filename}] ${message}`, ...meta);
21
+ }
22
+ }
23
+ async hasIncompleteJobs(migration) {
24
+ return (0, postgres_1.queryValidatedSingleColumnOneRow)(sql.batched_migration_has_incomplete_jobs, { batched_migration_id: migration.id }, zod_1.z.boolean());
25
+ }
26
+ async hasFailedJobs(migration) {
27
+ return (0, postgres_1.queryValidatedSingleColumnOneRow)(sql.batched_migration_has_failed_jobs, { batched_migration_id: migration.id }, zod_1.z.boolean());
28
+ }
29
+ async refreshMigrationStatus(migration) {
30
+ this.migrationStatus = await (0, postgres_1.queryValidatedSingleColumnOneRow)(sql.get_migration_status, {
31
+ id: migration.id,
32
+ }, batched_migration_1.BatchedMigrationStatusSchema);
33
+ }
34
+ async finishRunningMigration(migration) {
35
+ // Safety check: if there are any pending jobs, don't mark this
36
+ // migration as finished.
37
+ if (await this.hasIncompleteJobs(migration)) {
38
+ this.log(`Incomplete jobs found, not marking as finished`);
39
+ return;
40
+ }
41
+ const hasFailedJobs = await this.hasFailedJobs(migration);
42
+ const finalStatus = hasFailedJobs ? 'failed' : 'succeeded';
43
+ await (0, batched_migration_1.updateBatchedMigrationStatus)(migration.id, finalStatus);
44
+ this.log(`Finished with status '${finalStatus}'`);
45
+ }
46
+ async getNextBatchBounds(migration) {
47
+ const lastJob = await (0, postgres_1.queryValidatedZeroOrOneRow)(sql.select_last_batched_migration_job, { batched_migration_id: migration.id }, batched_migration_job_1.BatchedMigrationJobRowSchema);
48
+ const nextMin = lastJob ? lastJob.max_value + 1n : migration.min_value;
49
+ if (nextMin > migration.max_value)
50
+ return null;
51
+ let nextMax = nextMin + BigInt(migration.batch_size) - 1n;
52
+ if (nextMax > migration.max_value)
53
+ nextMax = migration.max_value;
54
+ return [nextMin, nextMax];
55
+ }
56
+ async startJob(job) {
57
+ await (0, postgres_1.queryAsync)(sql.start_batched_migration_job, { id: job.id });
58
+ const jobRange = `[${job.min_value}, ${job.max_value}]`;
59
+ const migrationRange = `[${this.migration.min_value}, ${this.migration.max_value}]`;
60
+ this.log(`Started job ${job.id} for range ${jobRange} in ${migrationRange}`);
61
+ }
62
+ serializeJobData(data) {
63
+ if (data == null)
64
+ return null;
65
+ // Return JSON-stringified data. Convert BigInts to strings.
66
+ return JSON.stringify(data, (_key, value) => {
67
+ if (typeof value === 'bigint')
68
+ return value.toString();
69
+ return value;
70
+ });
71
+ }
72
+ async finishJob(job, status, data) {
73
+ await (0, postgres_1.queryAsync)(sql.finish_batched_migration_job, {
74
+ id: job.id,
75
+ status,
76
+ data: this.serializeJobData(data),
77
+ });
78
+ this.log(`Job ${job.id} finished with status '${status}'`);
79
+ }
80
+ async getOrCreateNextMigrationJob(migration) {
81
+ const nextBatchBounds = await this.getNextBatchBounds(migration);
82
+ if (nextBatchBounds) {
83
+ return (0, postgres_1.queryValidatedOneRow)(sql.insert_batched_migration_job, {
84
+ batched_migration_id: migration.id,
85
+ min_value: nextBatchBounds[0],
86
+ max_value: nextBatchBounds[1],
87
+ }, batched_migration_job_1.BatchedMigrationJobRowSchema);
88
+ }
89
+ else {
90
+ // Pick up any old pending jobs from this migration. These will only exist if
91
+ // an admin manually elected to retry all failed jobs; we'll never automatically
92
+ // transition failed jobs back to pending.
93
+ return (0, postgres_1.queryValidatedZeroOrOneRow)(sql.select_first_pending_batched_migration_job, { batched_migration_id: migration.id }, batched_migration_job_1.BatchedMigrationJobRowSchema);
94
+ }
95
+ }
96
+ async runMigrationJob(migration, migrationImplementation) {
97
+ const nextJob = await this.getOrCreateNextMigrationJob(migration);
98
+ if (nextJob) {
99
+ await this.startJob(nextJob);
100
+ let error = null;
101
+ try {
102
+ // We'll only handle errors thrown by the migration itself. If any of
103
+ // our own execution machinery throws an error, we'll let it bubble up.
104
+ await migrationImplementation.execute(nextJob.min_value, nextJob.max_value);
105
+ }
106
+ catch (err) {
107
+ error = err;
108
+ }
109
+ if (error) {
110
+ await this.finishJob(nextJob, 'failed', { error: (0, serialize_error_1.serializeError)(error) });
111
+ }
112
+ else {
113
+ await this.finishJob(nextJob, 'succeeded');
114
+ }
115
+ }
116
+ else {
117
+ await this.finishRunningMigration(migration);
118
+ }
119
+ }
120
+ async run({ signal, iterations, durationMs, } = {}) {
121
+ let iterationCount = 0;
122
+ const endTime = durationMs ? Date.now() + durationMs : null;
123
+ while (!signal?.aborted &&
124
+ (iterations ? iterationCount < iterations : true) &&
125
+ (endTime ? Date.now() < endTime : true) &&
126
+ (this.migrationStatus === 'running' || this.migrationStatus === 'finalizing')) {
127
+ await this.runMigrationJob(this.migration, this.migrationImplementation);
128
+ iterationCount += 1;
129
+ // Always refresh the status so we can detect if the migration was marked
130
+ // as paused by another process.
131
+ await this.refreshMigrationStatus(this.migration);
132
+ }
133
+ }
134
+ }
135
+ exports.BatchedMigrationRunner = BatchedMigrationRunner;
136
+ //# sourceMappingURL=batched-migration-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batched-migration-runner.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ -- BLOCK select_last_batched_migration_job
2
+ SELECT
3
+ *
4
+ FROM
5
+ batched_migration_jobs
6
+ WHERE
7
+ batched_migration_id = $batched_migration_id
8
+ ORDER BY
9
+ id DESC
10
+ LIMIT
11
+ 1;
12
+
13
+ -- BLOCK insert_batched_migration_job
14
+ INSERT INTO
15
+ batched_migration_jobs (
16
+ batched_migration_id,
17
+ status,
18
+ min_value,
19
+ max_value
20
+ )
21
+ VALUES
22
+ (
23
+ $batched_migration_id,
24
+ 'pending'::enum_batched_migration_job_status,
25
+ $min_value,
26
+ $max_value
27
+ )
28
+ RETURNING
29
+ *;
30
+
31
+ -- BLOCK start_batched_migration_job
32
+ UPDATE batched_migration_jobs
33
+ SET
34
+ attempts = attempts + 1,
35
+ updated_at = CURRENT_TIMESTAMP,
36
+ started_at = CURRENT_TIMESTAMP
37
+ WHERE
38
+ id = $id;
39
+
40
+ -- BLOCK finish_batched_migration_job
41
+ UPDATE batched_migration_jobs
42
+ SET
43
+ status = $status::enum_batched_migration_job_status,
44
+ updated_at = CURRENT_TIMESTAMP,
45
+ finished_at = CURRENT_TIMESTAMP,
46
+ data = $data
47
+ WHERE
48
+ id = $id;
49
+
50
+ -- BLOCK select_first_pending_batched_migration_job
51
+ SELECT
52
+ *
53
+ FROM
54
+ batched_migration_jobs
55
+ WHERE
56
+ batched_migration_id = $batched_migration_id
57
+ AND status = 'pending'
58
+ ORDER BY
59
+ id ASC
60
+ LIMIT
61
+ 1;
62
+
63
+ -- BLOCK batched_migration_has_incomplete_jobs
64
+ SELECT
65
+ EXISTS (
66
+ SELECT
67
+ 1
68
+ FROM
69
+ batched_migration_jobs
70
+ WHERE
71
+ batched_migration_id = $batched_migration_id
72
+ AND status = 'pending'
73
+ ) as exists;
74
+
75
+ -- BLOCK batched_migration_has_failed_jobs
76
+ SELECT
77
+ EXISTS (
78
+ SELECT
79
+ 1
80
+ FROM
81
+ batched_migration_jobs
82
+ WHERE
83
+ batched_migration_id = $batched_migration_id
84
+ AND status = 'failed'
85
+ ) as exists;
86
+
87
+ -- BLOCK get_migration_status
88
+ SELECT
89
+ status
90
+ FROM
91
+ batched_migrations
92
+ WHERE
93
+ id = $id;