@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.
- package/.turbo/turbo-build.log +2 -0
- package/CHANGELOG.md +19 -0
- package/README.md +145 -0
- package/dist/batched-migrations/batched-migration-job.d.ts +42 -0
- package/dist/batched-migrations/batched-migration-job.js +25 -0
- package/dist/batched-migrations/batched-migration-job.js.map +1 -0
- package/dist/batched-migrations/batched-migration-job.sql +12 -0
- package/dist/batched-migrations/batched-migration-runner.d.ts +29 -0
- package/dist/batched-migrations/batched-migration-runner.js +136 -0
- package/dist/batched-migrations/batched-migration-runner.js.map +1 -0
- package/dist/batched-migrations/batched-migration-runner.sql +93 -0
- package/dist/batched-migrations/batched-migration-runner.test.js +185 -0
- package/dist/batched-migrations/batched-migration-runner.test.js.map +1 -0
- package/dist/batched-migrations/batched-migration.d.ts +79 -0
- package/dist/batched-migrations/batched-migration.js +73 -0
- package/dist/batched-migrations/batched-migration.js.map +1 -0
- package/dist/batched-migrations/batched-migration.sql +95 -0
- package/dist/batched-migrations/batched-migrations-runner.d.ts +63 -0
- package/dist/batched-migrations/batched-migrations-runner.js +272 -0
- package/dist/batched-migrations/batched-migrations-runner.js.map +1 -0
- package/dist/batched-migrations/batched-migrations-runner.sql +35 -0
- package/dist/batched-migrations/batched-migrations-runner.test.js +116 -0
- package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -0
- package/dist/batched-migrations/fixtures/20230406184103_successful_migration.d.ts +9 -0
- package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js +14 -0
- package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js.map +1 -0
- package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.d.ts +8 -0
- package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js +16 -0
- package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js.map +1 -0
- package/dist/batched-migrations/index.d.ts +3 -0
- package/dist/batched-migrations/index.js +18 -0
- package/dist/batched-migrations/index.js.map +1 -0
- package/dist/index.d.ts +3 -12
- package/dist/index.js +15 -192
- package/dist/index.js.map +1 -1
- package/dist/load-migrations.d.ts +8 -0
- package/dist/load-migrations.js +60 -0
- package/dist/load-migrations.js.map +1 -0
- package/dist/load-migrations.test.d.ts +1 -0
- package/dist/{index.test.js → load-migrations.test.js} +12 -65
- package/dist/load-migrations.test.js.map +1 -0
- package/dist/migrations/fixtures/20230407210430_insert_user.d.ts +1 -0
- package/dist/migrations/fixtures/20230407210430_insert_user.js.map +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.js +6 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/migrations.d.ts +6 -0
- package/dist/migrations/migrations.js +158 -0
- package/dist/migrations/migrations.js.map +1 -0
- package/dist/migrations/migrations.test.d.ts +1 -0
- package/dist/migrations/migrations.test.js +78 -0
- package/dist/migrations/migrations.test.js.map +1 -0
- package/package.json +15 -12
- package/schema-migrations/20230303193423_batched_migrations__create.sql +49 -0
- package/src/batched-migrations/batched-migration-job.sql +12 -0
- package/src/batched-migrations/batched-migration-job.ts +34 -0
- package/src/batched-migrations/batched-migration-runner.sql +93 -0
- package/src/batched-migrations/batched-migration-runner.test.ts +208 -0
- package/src/batched-migrations/batched-migration-runner.ts +215 -0
- package/src/batched-migrations/batched-migration.sql +95 -0
- package/src/batched-migrations/batched-migration.ts +129 -0
- package/src/batched-migrations/batched-migrations-runner.sql +35 -0
- package/src/batched-migrations/batched-migrations-runner.test.ts +111 -0
- package/src/batched-migrations/batched-migrations-runner.ts +327 -0
- package/src/batched-migrations/fixtures/20230406184103_successful_migration.ts +13 -0
- package/src/batched-migrations/fixtures/20230406184107_failing_migration.js +16 -0
- package/src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts +15 -0
- package/src/batched-migrations/index.ts +21 -0
- package/src/index.ts +20 -201
- package/src/{index.test.ts → load-migrations.test.ts} +8 -73
- package/src/load-migrations.ts +76 -0
- package/src/migrations/index.ts +1 -0
- package/src/migrations/migrations.test.ts +80 -0
- package/src/migrations/migrations.ts +149 -0
- package/tsconfig.json +1 -1
- package/dist/fixtures/20230407210430_insert_user.js.map +0 -1
- package/dist/index.test.js.map +0 -1
- /package/dist/{fixtures/20230407210430_insert_user.d.ts → batched-migrations/batched-migration-runner.test.d.ts} +0 -0
- /package/dist/{index.test.d.ts → batched-migrations/batched-migrations-runner.test.d.ts} +0 -0
- /package/dist/{fixtures → migrations/fixtures}/20230407210409_create_users.sql +0 -0
- /package/dist/{fixtures → migrations/fixtures}/20230407210430_insert_user.js +0 -0
- /package/dist/{index.sql → migrations/migrations.sql} +0 -0
- /package/src/{fixtures → migrations/fixtures}/20230407210409_create_users.sql +0 -0
- /package/src/{fixtures → migrations/fixtures}/20230407210430_insert_user.ts +0 -0
- /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
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,24 +8,27 @@
|
|
|
8
8
|
"directory": "packages/migrations"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc &&
|
|
12
|
-
"dev": "tsc --watch --preserveWatchOutput",
|
|
13
|
-
"test": "mocha --no-config --require ts-node/register src
|
|
11
|
+
"build": "tsc && tscp",
|
|
12
|
+
"dev": "tsc --watch --preserveWatchOutput & tscp --watch",
|
|
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
|
-
"@types/node": "^18.
|
|
20
|
-
"copyfiles": "^2.4.1",
|
|
19
|
+
"@types/node": "^18.16.3",
|
|
21
20
|
"mocha": "^10.2.0",
|
|
22
21
|
"ts-node": "^10.9.1",
|
|
23
|
-
"typescript": "^
|
|
22
|
+
"typescript": "^5.0.4",
|
|
23
|
+
"typescript-cp": "^0.1.7"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@prairielearn/error": "^1.0.
|
|
27
|
-
"@prairielearn/
|
|
28
|
-
"@prairielearn/
|
|
29
|
-
"
|
|
26
|
+
"@prairielearn/error": "^1.0.1",
|
|
27
|
+
"@prairielearn/logger": "^1.0.0",
|
|
28
|
+
"@prairielearn/named-locks": "^1.3.1",
|
|
29
|
+
"@prairielearn/postgres": "^1.6.1",
|
|
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,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
|
+
});
|