@prairielearn/migrations 1.1.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 (83) hide show
  1. package/CHANGELOG.md +11 -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.js +116 -0
  22. package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -0
  23. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.d.ts +9 -0
  24. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js +14 -0
  25. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js.map +1 -0
  26. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.d.ts +8 -0
  27. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js +16 -0
  28. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js.map +1 -0
  29. package/dist/batched-migrations/index.d.ts +3 -0
  30. package/dist/batched-migrations/index.js +18 -0
  31. package/dist/batched-migrations/index.js.map +1 -0
  32. package/dist/index.d.ts +3 -12
  33. package/dist/index.js +15 -192
  34. package/dist/index.js.map +1 -1
  35. package/dist/load-migrations.d.ts +8 -0
  36. package/dist/load-migrations.js +60 -0
  37. package/dist/load-migrations.js.map +1 -0
  38. package/dist/load-migrations.test.d.ts +1 -0
  39. package/dist/{index.test.js → load-migrations.test.js} +12 -65
  40. package/dist/load-migrations.test.js.map +1 -0
  41. package/dist/migrations/fixtures/20230407210430_insert_user.d.ts +1 -0
  42. package/dist/migrations/fixtures/20230407210430_insert_user.js.map +1 -0
  43. package/dist/migrations/index.d.ts +1 -0
  44. package/dist/migrations/index.js +6 -0
  45. package/dist/migrations/index.js.map +1 -0
  46. package/dist/migrations/migrations.d.ts +6 -0
  47. package/dist/migrations/migrations.js +159 -0
  48. package/dist/migrations/migrations.js.map +1 -0
  49. package/dist/migrations/migrations.test.d.ts +1 -0
  50. package/dist/migrations/migrations.test.js +78 -0
  51. package/dist/migrations/migrations.test.js.map +1 -0
  52. package/package.json +10 -7
  53. package/schema-migrations/20230303193423_batched_migrations__create.sql +49 -0
  54. package/src/batched-migrations/batched-migration-job.sql +12 -0
  55. package/src/batched-migrations/batched-migration-job.ts +34 -0
  56. package/src/batched-migrations/batched-migration-runner.sql +93 -0
  57. package/src/batched-migrations/batched-migration-runner.test.ts +208 -0
  58. package/src/batched-migrations/batched-migration-runner.ts +215 -0
  59. package/src/batched-migrations/batched-migration.sql +95 -0
  60. package/src/batched-migrations/batched-migration.ts +129 -0
  61. package/src/batched-migrations/batched-migrations-runner.sql +35 -0
  62. package/src/batched-migrations/batched-migrations-runner.test.ts +111 -0
  63. package/src/batched-migrations/batched-migrations-runner.ts +327 -0
  64. package/src/batched-migrations/fixtures/20230406184103_successful_migration.ts +13 -0
  65. package/src/batched-migrations/fixtures/20230406184107_failing_migration.js +16 -0
  66. package/src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts +15 -0
  67. package/src/batched-migrations/index.ts +21 -0
  68. package/src/index.ts +20 -201
  69. package/src/{index.test.ts → load-migrations.test.ts} +8 -73
  70. package/src/load-migrations.ts +76 -0
  71. package/src/migrations/index.ts +1 -0
  72. package/src/migrations/migrations.test.ts +80 -0
  73. package/src/migrations/migrations.ts +149 -0
  74. package/dist/fixtures/20230407210430_insert_user.js.map +0 -1
  75. package/dist/index.test.js.map +0 -1
  76. /package/dist/{fixtures/20230407210430_insert_user.d.ts → batched-migrations/batched-migration-runner.test.d.ts} +0 -0
  77. /package/dist/{index.test.d.ts → batched-migrations/batched-migrations-runner.test.d.ts} +0 -0
  78. /package/dist/{fixtures → migrations/fixtures}/20230407210409_create_users.sql +0 -0
  79. /package/dist/{fixtures → migrations/fixtures}/20230407210430_insert_user.js +0 -0
  80. /package/dist/{index.sql → migrations/migrations.sql} +0 -0
  81. /package/src/{fixtures → migrations/fixtures}/20230407210409_create_users.sql +0 -0
  82. /package/src/{fixtures → migrations/fixtures}/20230407210430_insert_user.ts +0 -0
  83. /package/src/{index.sql → migrations/migrations.sql} +0 -0
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const postgres_1 = require("@prairielearn/postgres");
9
+ const migrations_1 = require("./migrations");
10
+ describe('migrations', () => {
11
+ describe('getMigrationsToExecute', () => {
12
+ it('handles the case of no executed migrations', () => {
13
+ const migrationFiles = [
14
+ {
15
+ directory: 'migrations',
16
+ filename: '001_testing.sql',
17
+ timestamp: '20220101010101',
18
+ },
19
+ ];
20
+ chai_1.assert.deepEqual((0, migrations_1.getMigrationsToExecute)(migrationFiles, []), migrationFiles);
21
+ });
22
+ it('handles case where subset of migrations have been executed', () => {
23
+ const migrationFiles = [
24
+ {
25
+ directory: 'migrations',
26
+ filename: '20220101010101_testing_1.sql',
27
+ timestamp: '20220101010101',
28
+ },
29
+ {
30
+ directory: 'migrations',
31
+ filename: '20220101010102_testing_2.sql',
32
+ timestamp: '20220101010102',
33
+ },
34
+ {
35
+ directory: 'migrations',
36
+ filename: '20220101010103_testing_3.sql',
37
+ timestamp: '20220101010103',
38
+ },
39
+ ];
40
+ const executedMigrations = [
41
+ {
42
+ timestamp: '20220101010101',
43
+ },
44
+ {
45
+ timestamp: '20220101010102',
46
+ },
47
+ ];
48
+ chai_1.assert.deepEqual((0, migrations_1.getMigrationsToExecute)(migrationFiles, executedMigrations), [
49
+ {
50
+ directory: 'migrations',
51
+ timestamp: '20220101010103',
52
+ filename: '20220101010103_testing_3.sql',
53
+ },
54
+ ]);
55
+ });
56
+ });
57
+ describe('initWithLock', () => {
58
+ const postgresTestUtils = (0, postgres_1.makePostgresTestUtils)({
59
+ database: 'prairielearn_migrations',
60
+ });
61
+ before(async () => {
62
+ await postgresTestUtils.createDatabase();
63
+ });
64
+ after(async () => {
65
+ await postgresTestUtils.dropDatabase();
66
+ });
67
+ it('runs both SQL and JavaScript migrations', async () => {
68
+ const migrationDir = node_path_1.default.join(__dirname, 'fixtures');
69
+ await (0, migrations_1.initWithLock)([migrationDir], 'prairielearn_migrations');
70
+ // If both migrations ran successfully, there should be a single user
71
+ // in the database.
72
+ const users = await (0, postgres_1.queryAsync)('SELECT * FROM users', {});
73
+ chai_1.assert.lengthOf(users.rows, 1);
74
+ chai_1.assert.equal(users.rows[0].name, 'Test User');
75
+ });
76
+ });
77
+ });
78
+ //# sourceMappingURL=migrations.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.test.js","sourceRoot":"","sources":["../../src/migrations/migrations.test.ts"],"names":[],"mappings":";;;;;AAAA,+BAA8B;AAC9B,0DAA6B;AAC7B,qDAA2E;AAE3E,6CAAoE;AAEpE,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;QACtC,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;YACpD,MAAM,cAAc,GAAG;gBACrB;oBACE,SAAS,EAAE,YAAY;oBACvB,QAAQ,EAAE,iBAAiB;oBAC3B,SAAS,EAAE,gBAAgB;iBAC5B;aACF,CAAC;YACF,aAAM,CAAC,SAAS,CAAC,IAAA,mCAAsB,EAAC,cAAc,EAAE,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC;QAC/E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;YACpE,MAAM,cAAc,GAAG;gBACrB;oBACE,SAAS,EAAE,YAAY;oBACvB,QAAQ,EAAE,8BAA8B;oBACxC,SAAS,EAAE,gBAAgB;iBAC5B;gBACD;oBACE,SAAS,EAAE,YAAY;oBACvB,QAAQ,EAAE,8BAA8B;oBACxC,SAAS,EAAE,gBAAgB;iBAC5B;gBACD;oBACE,SAAS,EAAE,YAAY;oBACvB,QAAQ,EAAE,8BAA8B;oBACxC,SAAS,EAAE,gBAAgB;iBAC5B;aACF,CAAC;YACF,MAAM,kBAAkB,GAAG;gBACzB;oBACE,SAAS,EAAE,gBAAgB;iBAC5B;gBACD;oBACE,SAAS,EAAE,gBAAgB;iBAC5B;aACF,CAAC;YACF,aAAM,CAAC,SAAS,CAAC,IAAA,mCAAsB,EAAC,cAAc,EAAE,kBAAkB,CAAC,EAAE;gBAC3E;oBACE,SAAS,EAAE,YAAY;oBACvB,SAAS,EAAE,gBAAgB;oBAC3B,QAAQ,EAAE,8BAA8B;iBACzC;aACF,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,MAAM,iBAAiB,GAAG,IAAA,gCAAqB,EAAC;YAC9C,QAAQ,EAAE,yBAAyB;SACpC,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,IAAI,EAAE;YAChB,MAAM,iBAAiB,CAAC,cAAc,EAAE,CAAC;QAC3C,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,KAAK,IAAI,EAAE;YACf,MAAM,iBAAiB,CAAC,YAAY,EAAE,CAAC;QACzC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;YACvD,MAAM,YAAY,GAAG,mBAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;YACtD,MAAM,IAAA,yBAAY,EAAC,CAAC,YAAY,CAAC,EAAE,yBAAyB,CAAC,CAAC;YAE9D,qEAAqE;YACrE,mBAAmB;YACnB,MAAM,KAAK,GAAG,MAAM,IAAA,qBAAU,EAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;YAC1D,aAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;YAC/B,aAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/migrations",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "main": "./dist/index.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,10 +10,10 @@
10
10
  "scripts": {
11
11
  "build": "tsc && copyfiles -u 1 \"./src/**/*.sql\" dist",
12
12
  "dev": "tsc --watch --preserveWatchOutput",
13
- "test": "mocha --no-config --require ts-node/register src/*.test.ts"
13
+ "test": "mocha --no-config --require ts-node/register src/**/*.test.ts"
14
14
  },
15
15
  "devDependencies": {
16
- "@prairielearn/tsconfig": "*",
16
+ "@prairielearn/tsconfig": "^0.0.0",
17
17
  "@types/fs-extra": "^11.0.1",
18
18
  "@types/mocha": "^10.0.1",
19
19
  "@types/node": "^18.15.11",
@@ -23,9 +23,12 @@
23
23
  "typescript": "^4.9.5"
24
24
  },
25
25
  "dependencies": {
26
- "@prairielearn/error": "^1.0.0",
27
- "@prairielearn/named-locks": "^1.2.0",
28
- "@prairielearn/postgres": "^1.2.0",
29
- "fs-extra": "^11.1.1"
26
+ "@prairielearn/error": "^1.0.1",
27
+ "@prairielearn/logger": "^1.0.0",
28
+ "@prairielearn/named-locks": "^1.3.0",
29
+ "@prairielearn/postgres": "^1.6.0",
30
+ "fs-extra": "^11.1.1",
31
+ "serialize-error": "^8.1.0",
32
+ "zod": "^3.21.4"
30
33
  }
31
34
  }
@@ -0,0 +1,49 @@
1
+ CREATE TYPE enum_batched_migration_status AS ENUM(
2
+ 'pending',
3
+ 'paused',
4
+ 'running',
5
+ 'finalizing',
6
+ 'failed',
7
+ 'succeeded'
8
+ );
9
+
10
+ CREATE TYPE enum_batched_migration_job_status AS ENUM('pending', 'failed', 'succeeded');
11
+
12
+ CREATE TABLE IF NOT EXISTS
13
+ batched_migrations (
14
+ id BIGSERIAL PRIMARY KEY,
15
+ project TEXT DEFAULT 'prairielearn' NOT NULL,
16
+ filename TEXT NOT NULL,
17
+ timestamp TEXT NOT NULL,
18
+ batch_size INTEGER NOT NULL,
19
+ min_value BIGINT NOT NULL,
20
+ max_value BIGINT NOT NULL,
21
+ status enum_batched_migration_status DEFAULT 'pending' NOT NULL,
22
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
23
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
24
+ started_at TIMESTAMP WITH TIME ZONE,
25
+ UNIQUE (project, timestamp),
26
+ CONSTRAINT batched_migrations_min_value_check CHECK (min_value > 0),
27
+ CONSTRAINT batched_migrations_max_value_check CHECK (max_value >= min_value)
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS
31
+ batched_migration_jobs (
32
+ id BIGSERIAL PRIMARY KEY,
33
+ batched_migration_id BIGINT NOT NULL REFERENCES batched_migrations (id) ON UPDATE CASCADE ON DELETE CASCADE,
34
+ min_value BIGINT NOT NULL,
35
+ max_value BIGINT NOT NULL,
36
+ status enum_batched_migration_job_status DEFAULT 'pending' NOT NULL,
37
+ attempts INTEGER DEFAULT 0 NOT NULL,
38
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
39
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
40
+ started_at TIMESTAMP WITH TIME ZONE,
41
+ finished_at TIMESTAMP WITH TIME ZONE,
42
+ data jsonb,
43
+ CONSTRAINT batched_migration_jobs_min_value_check CHECK (min_value > 0),
44
+ CONSTRAINT batched_migration_jobs_max_value_check CHECK (max_value >= min_value)
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS batched_migration_jobs_batched_migration_id_max_value_idx ON batched_migration_jobs (batched_migration_id, max_value);
48
+
49
+ CREATE INDEX IF NOT EXISTS batched_migration_jobs_batched_migration_id_status_idx ON batched_migration_jobs (batched_migration_id, status);
@@ -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,34 @@
1
+ import { z } from 'zod';
2
+ import { loadSqlEquiv, queryValidatedRows } from '@prairielearn/postgres';
3
+
4
+ const sql = loadSqlEquiv(__filename);
5
+
6
+ export const BatchedMigrationJobStatusSchema = z.enum(['pending', 'failed', 'succeeded']);
7
+ export type BatchedMigrationJobStatus = z.infer<typeof BatchedMigrationJobStatusSchema>;
8
+
9
+ export const BatchedMigrationJobRowSchema = z.object({
10
+ id: z.string(),
11
+ batched_migration_id: z.string(),
12
+ min_value: z.bigint({ coerce: true }),
13
+ max_value: z.bigint({ coerce: true }),
14
+ status: BatchedMigrationJobStatusSchema,
15
+ attempts: z.number(),
16
+ created_at: z.date(),
17
+ updated_at: z.date(),
18
+ started_at: z.date().nullable(),
19
+ finished_at: z.date().nullable(),
20
+ data: z.unknown(),
21
+ });
22
+ export type BatchedMigrationJobRow = z.infer<typeof BatchedMigrationJobRowSchema>;
23
+
24
+ export async function selectRecentJobsWithStatus(
25
+ batchedMigrationId: string,
26
+ status: BatchedMigrationJobStatus,
27
+ limit: number
28
+ ): Promise<BatchedMigrationJobRow[]> {
29
+ return queryValidatedRows(
30
+ sql.select_recent_jobs_with_status,
31
+ { batched_migration_id: batchedMigrationId, status, limit },
32
+ BatchedMigrationJobRowSchema
33
+ );
34
+ }
@@ -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;
@@ -0,0 +1,208 @@
1
+ import { assert } from 'chai';
2
+ import {
3
+ makePostgresTestUtils,
4
+ queryAsync,
5
+ queryValidatedOneRow,
6
+ queryValidatedRows,
7
+ } from '@prairielearn/postgres';
8
+ import * as namedLocks from '@prairielearn/named-locks';
9
+ import * as error from '@prairielearn/error';
10
+
11
+ import {
12
+ BatchedMigrationRowSchema,
13
+ insertBatchedMigration,
14
+ makeBatchedMigration,
15
+ updateBatchedMigrationStatus,
16
+ } from './batched-migration';
17
+ import { BatchedMigrationJobRowSchema } from './batched-migration-job';
18
+ import { BatchedMigrationRunner } from './batched-migration-runner';
19
+ import { SCHEMA_MIGRATIONS_PATH, init } from '../index';
20
+
21
+ const postgresTestUtils = makePostgresTestUtils({
22
+ database: 'prairielearn_migrations',
23
+ });
24
+
25
+ function makeTestBatchMigration() {
26
+ let executionCount = 0;
27
+ let failingIds: bigint[] = [];
28
+
29
+ return makeBatchedMigration({
30
+ async getParameters() {
31
+ return {
32
+ min: 1n,
33
+ max: 10000n,
34
+ batchSize: 1000,
35
+ };
36
+ },
37
+ async execute(start: bigint, end: bigint) {
38
+ executionCount += 1;
39
+ const shouldFail = failingIds.some((id) => id >= start && id <= end);
40
+ if (shouldFail) {
41
+ // Throw an error with some data to make sure it gets persisted. We
42
+ // specifically use BigInt values here to make sure that they are
43
+ // correctly serialized to strings.
44
+ throw error.makeWithData('Execution failure', { start, end });
45
+ }
46
+ },
47
+ setFailingIds(ids: bigint[]) {
48
+ failingIds = ids;
49
+ },
50
+ get executionCount() {
51
+ return executionCount;
52
+ },
53
+ });
54
+ }
55
+
56
+ async function getBatchedMigration(migrationId: string) {
57
+ return queryValidatedOneRow(
58
+ 'SELECT * FROM batched_migrations WHERE id = $id;',
59
+ { id: migrationId },
60
+ BatchedMigrationRowSchema
61
+ );
62
+ }
63
+
64
+ async function getBatchedMigrationJobs(migrationId: string) {
65
+ return queryValidatedRows(
66
+ 'SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;',
67
+ { batched_migration_id: migrationId },
68
+ BatchedMigrationJobRowSchema
69
+ );
70
+ }
71
+
72
+ async function resetFailedBatchedMigrationJobs(migrationId: string) {
73
+ await queryAsync(
74
+ "UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'",
75
+ {
76
+ batched_migration_id: migrationId,
77
+ }
78
+ );
79
+ }
80
+
81
+ async function insertTestBatchedMigration() {
82
+ const migrationImplementation = makeTestBatchMigration();
83
+ const parameters = await migrationImplementation.getParameters();
84
+ const migration = await insertBatchedMigration({
85
+ project: 'test',
86
+ filename: '20230406184103_test_batch_migration.js',
87
+ timestamp: '20230406184103',
88
+ batch_size: parameters.batchSize,
89
+ min_value: parameters.min,
90
+ max_value: parameters.max,
91
+ status: 'running',
92
+ });
93
+ if (!migration) throw new Error('Failed to insert batched migration');
94
+ return migration;
95
+ }
96
+
97
+ describe('BatchedMigrationExecutor', () => {
98
+ before(async () => {
99
+ await postgresTestUtils.createDatabase();
100
+ await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {
101
+ throw err;
102
+ });
103
+ await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');
104
+ });
105
+
106
+ beforeEach(async () => {
107
+ await postgresTestUtils.resetDatabase();
108
+ });
109
+
110
+ after(async () => {
111
+ await namedLocks.close();
112
+ await postgresTestUtils.dropDatabase();
113
+ });
114
+
115
+ it('runs one iteration of a batched migration', async () => {
116
+ const migration = await insertTestBatchedMigration();
117
+
118
+ const migrationImplementation = makeTestBatchMigration();
119
+ const executor = new BatchedMigrationRunner(migration, migrationImplementation);
120
+ await executor.run({ iterations: 1 });
121
+
122
+ const jobs = await getBatchedMigrationJobs(migration.id);
123
+ assert.lengthOf(jobs, 1);
124
+
125
+ const finalMigration = await getBatchedMigration(migration.id);
126
+ assert.equal(finalMigration.status, 'running');
127
+
128
+ assert.equal(migrationImplementation.executionCount, 1);
129
+ });
130
+
131
+ it('runs an entire batched migration', async () => {
132
+ const migration = await insertTestBatchedMigration();
133
+
134
+ const migrationImplementation = makeTestBatchMigration();
135
+ const runner = new BatchedMigrationRunner(migration, migrationImplementation);
136
+ await runner.run();
137
+
138
+ const jobs = await getBatchedMigrationJobs(migration.id);
139
+ assert.lengthOf(jobs, 10);
140
+ assert.equal(jobs[0].min_value, 1n);
141
+ assert.equal(jobs[0].max_value, 1000n);
142
+ assert.equal(jobs.at(-1)?.min_value, 9001n);
143
+ assert.equal(jobs.at(-1)?.max_value, 10000n);
144
+ assert.isTrue(jobs.every((job) => job.started_at !== null));
145
+ assert.isTrue(jobs.every((job) => job.finished_at !== null));
146
+ assert.isTrue(jobs.every((job) => job.status === 'succeeded'));
147
+ assert.isTrue(jobs.every((job) => job.attempts === 1));
148
+
149
+ const finalMigration = await getBatchedMigration(migration.id);
150
+ assert.equal(finalMigration.status, 'succeeded');
151
+ });
152
+
153
+ it('handles failing execution', async () => {
154
+ let migration = await insertTestBatchedMigration();
155
+
156
+ const migrationImplementation = makeTestBatchMigration();
157
+ migrationImplementation.setFailingIds([1n, 5010n]);
158
+ const runner = new BatchedMigrationRunner(migration, migrationImplementation);
159
+ await runner.run();
160
+
161
+ const jobs = await getBatchedMigrationJobs(migration.id);
162
+ const failedJobs = jobs.filter((job) => job.status === 'failed');
163
+ const successfulJobs = jobs.filter((job) => job.status === 'succeeded');
164
+ assert.lengthOf(jobs, 10);
165
+ assert.lengthOf(failedJobs, 2);
166
+ assert.lengthOf(successfulJobs, 8);
167
+ assert.equal(migrationImplementation.executionCount, 10);
168
+ assert.isTrue(jobs.every((job) => job.attempts === 1));
169
+ failedJobs.forEach((job) => {
170
+ const jobData = job.data as any;
171
+ assert.isObject(jobData);
172
+ assert.isObject(jobData.error);
173
+ assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data']);
174
+ assert.equal(jobData.error.name, 'Error');
175
+ assert.equal(jobData.error.message, 'Execution failure');
176
+ assert.equal(jobData.error.data.start, job.min_value.toString());
177
+ assert.equal(jobData.error.data.end, job.max_value.toString());
178
+ });
179
+
180
+ const failedMigration = await getBatchedMigration(migration.id);
181
+ assert.equal(failedMigration.status, 'failed');
182
+
183
+ // Retry the failed jobs; ensure they succeed this time.
184
+ await resetFailedBatchedMigrationJobs(migration.id);
185
+ migration = await updateBatchedMigrationStatus(migration.id, 'running');
186
+
187
+ migrationImplementation.setFailingIds([]);
188
+ const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);
189
+ await retryRunner.run();
190
+
191
+ const finalJobs = await getBatchedMigrationJobs(migration.id);
192
+ const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');
193
+ const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');
194
+ const retriedJobs = finalJobs.filter((job) => job.attempts === 2);
195
+ assert.lengthOf(finalJobs, 10);
196
+ assert.lengthOf(finalFailedJobs, 0);
197
+ assert.lengthOf(finalSuccessfulJobs, 10);
198
+ assert.lengthOf(retriedJobs, 2);
199
+ assert.isTrue(finalJobs.every((job) => job.data === null));
200
+
201
+ migration = await getBatchedMigration(migration.id);
202
+ assert.equal(migration.status, 'succeeded');
203
+
204
+ // The runner should have run only the previously failed jobs, which
205
+ // works out to 2 additional execution.
206
+ assert.equal(migrationImplementation.executionCount, 12);
207
+ });
208
+ });