@prairielearn/migrations 4.0.4 → 5.0.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.
- package/CHANGELOG.md +25 -0
- package/dist/batched-migrations/batched-migration-job.d.ts +6 -6
- package/dist/batched-migrations/batched-migration-job.d.ts.map +1 -1
- package/dist/batched-migrations/batched-migration-job.js.map +1 -1
- package/dist/batched-migrations/batched-migration-runner.d.ts +1 -1
- package/dist/batched-migrations/batched-migration-runner.d.ts.map +1 -1
- package/dist/batched-migrations/batched-migration-runner.js.map +1 -1
- package/dist/batched-migrations/batched-migration-runner.test.d.ts.map +1 -1
- package/dist/batched-migrations/batched-migration-runner.test.js.map +1 -1
- package/dist/batched-migrations/batched-migration.d.ts +9 -9
- package/dist/batched-migrations/batched-migration.d.ts.map +1 -1
- package/dist/batched-migrations/batched-migration.js.map +1 -1
- package/dist/batched-migrations/batched-migrations-runner.d.ts +0 -6
- package/dist/batched-migrations/batched-migrations-runner.d.ts.map +1 -1
- package/dist/batched-migrations/batched-migrations-runner.js.map +1 -1
- package/dist/batched-migrations/batched-migrations-runner.test.d.ts.map +1 -1
- package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -1
- package/dist/batched-migrations/fixtures/20230406184103_successful_migration.d.ts.map +1 -1
- package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js.map +1 -1
- package/dist/batched-migrations/fixtures/20230406184107_failing_migration.d.ts.map +1 -1
- package/dist/batched-migrations/fixtures/20230406184107_failing_migration.js.map +1 -1
- package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.d.ts.map +1 -1
- package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js.map +1 -1
- package/dist/batched-migrations/index.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/load-migrations.d.ts.map +1 -1
- package/dist/load-migrations.js.map +1 -1
- package/dist/load-migrations.test.d.ts.map +1 -1
- package/dist/load-migrations.test.js.map +1 -1
- package/dist/migrations/fixtures/20230407210430_insert_user.d.ts.map +1 -1
- package/dist/migrations/fixtures/20230407210430_insert_user.js.map +1 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/migrations.d.ts +1 -1
- package/dist/migrations/migrations.d.ts.map +1 -1
- package/dist/migrations/migrations.js.map +1 -1
- package/dist/migrations/migrations.test.d.ts.map +1 -1
- package/dist/migrations/migrations.test.js.map +1 -1
- package/package.json +14 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @prairielearn/migrations
|
|
2
2
|
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 3914bb4: Upgrade to Node 24
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [3914bb4]
|
|
12
|
+
- @prairielearn/named-locks@4.0.0
|
|
13
|
+
- @prairielearn/postgres@5.0.0
|
|
14
|
+
- @prairielearn/logger@3.0.0
|
|
15
|
+
- @prairielearn/error@3.0.0
|
|
16
|
+
|
|
17
|
+
## 4.0.5
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 0900843: Switch to the `tsgo` compiler
|
|
22
|
+
- Updated dependencies [0900843]
|
|
23
|
+
- @prairielearn/named-locks@3.0.28
|
|
24
|
+
- @prairielearn/postgres@4.5.2
|
|
25
|
+
- @prairielearn/logger@2.0.25
|
|
26
|
+
- @prairielearn/error@2.0.24
|
|
27
|
+
|
|
3
28
|
## 4.0.4
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -14,27 +14,27 @@ export declare const BatchedMigrationJobRowSchema: z.ZodObject<{
|
|
|
14
14
|
finished_at: z.ZodNullable<z.ZodDate>;
|
|
15
15
|
data: z.ZodUnknown;
|
|
16
16
|
}, "strip", z.ZodTypeAny, {
|
|
17
|
-
status: "pending" | "failed" | "succeeded";
|
|
18
17
|
id: string;
|
|
18
|
+
batched_migration_id: string;
|
|
19
19
|
min_value: bigint;
|
|
20
20
|
max_value: bigint;
|
|
21
|
+
status: "failed" | "pending" | "succeeded";
|
|
22
|
+
attempts: number;
|
|
21
23
|
created_at: Date;
|
|
22
24
|
updated_at: Date;
|
|
23
25
|
started_at: Date | null;
|
|
24
|
-
batched_migration_id: string;
|
|
25
|
-
attempts: number;
|
|
26
26
|
finished_at: Date | null;
|
|
27
27
|
data?: unknown;
|
|
28
28
|
}, {
|
|
29
|
-
status: "pending" | "failed" | "succeeded";
|
|
30
29
|
id: string;
|
|
30
|
+
batched_migration_id: string;
|
|
31
31
|
min_value: bigint;
|
|
32
32
|
max_value: bigint;
|
|
33
|
+
status: "failed" | "pending" | "succeeded";
|
|
34
|
+
attempts: number;
|
|
33
35
|
created_at: Date;
|
|
34
36
|
updated_at: Date;
|
|
35
37
|
started_at: Date | null;
|
|
36
|
-
batched_migration_id: string;
|
|
37
|
-
attempts: number;
|
|
38
38
|
finished_at: Date | null;
|
|
39
39
|
data?: unknown;
|
|
40
40
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-job.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-job.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,+BAA+B,+CAA6C,CAAC;AAC1F,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAExF,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,wBAAsB,0BAA0B,CAC9C,kBAAkB,EAAE,MAAM,EAC1B,MAAM,EAAE,yBAAyB,EACjC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAMnC"}
|
|
1
|
+
{"version":3,"file":"batched-migration-job.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-job.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,+BAA+B,+CAA6C,CAAC;AAC1F,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAExF,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,wBAAsB,0BAA0B,CAC9C,kBAAkB,EAAE,MAAM,EAC1B,MAAM,EAAE,yBAAyB,EACjC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAMnC","sourcesContent":["import { z } from 'zod';\n\nimport { loadSqlEquiv, queryRows } from '@prairielearn/postgres';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\nexport const BatchedMigrationJobStatusSchema = z.enum(['pending', 'failed', 'succeeded']);\nexport type BatchedMigrationJobStatus = z.infer<typeof BatchedMigrationJobStatusSchema>;\n\nexport const BatchedMigrationJobRowSchema = z.object({\n id: z.string(),\n batched_migration_id: z.string(),\n min_value: z.bigint({ coerce: true }),\n max_value: z.bigint({ coerce: true }),\n status: BatchedMigrationJobStatusSchema,\n attempts: z.number(),\n created_at: z.date(),\n updated_at: z.date(),\n started_at: z.date().nullable(),\n finished_at: z.date().nullable(),\n data: z.unknown(),\n});\nexport type BatchedMigrationJobRow = z.infer<typeof BatchedMigrationJobRowSchema>;\n\nexport async function selectRecentJobsWithStatus(\n batchedMigrationId: string,\n status: BatchedMigrationJobStatus,\n limit: number,\n): Promise<BatchedMigrationJobRow[]> {\n return await queryRows(\n sql.select_recent_jobs_with_status,\n { batched_migration_id: batchedMigrationId, status, limit },\n BatchedMigrationJobRowSchema,\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-job.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-job.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEjE,MAAM,GAAG,GAAG,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"batched-migration-job.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-job.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEjE,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;AAE/C,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;AAG1F,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,oBAAoB,EAAE,CAAC,CAAC,MAAM,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,MAAM,EAAE,+BAA+B;IACvC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE;IACpB,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE;IACpB,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAC/B,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAChC,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;CAClB,CAAC,CAAC;AAGH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,kBAA0B,EAC1B,MAAiC,EACjC,KAAa,EACsB;IACnC,OAAO,MAAM,SAAS,CACpB,GAAG,CAAC,8BAA8B,EAClC,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAK,EAAE,EAC3D,4BAA4B,CAC7B,CAAC;AAAA,CACH","sourcesContent":["import { z } from 'zod';\n\nimport { loadSqlEquiv, queryRows } from '@prairielearn/postgres';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\nexport const BatchedMigrationJobStatusSchema = z.enum(['pending', 'failed', 'succeeded']);\nexport type BatchedMigrationJobStatus = z.infer<typeof BatchedMigrationJobStatusSchema>;\n\nexport const BatchedMigrationJobRowSchema = z.object({\n id: z.string(),\n batched_migration_id: z.string(),\n min_value: z.bigint({ coerce: true }),\n max_value: z.bigint({ coerce: true }),\n status: BatchedMigrationJobStatusSchema,\n attempts: z.number(),\n created_at: z.date(),\n updated_at: z.date(),\n started_at: z.date().nullable(),\n finished_at: z.date().nullable(),\n data: z.unknown(),\n});\nexport type BatchedMigrationJobRow = z.infer<typeof BatchedMigrationJobRowSchema>;\n\nexport async function selectRecentJobsWithStatus(\n batchedMigrationId: string,\n status: BatchedMigrationJobStatus,\n limit: number,\n): Promise<BatchedMigrationJobRow[]> {\n return await queryRows(\n sql.select_recent_jobs_with_status,\n { batched_migration_id: batchedMigrationId, status, limit },\n BatchedMigrationJobRowSchema,\n );\n}\n"]}
|
|
@@ -19,7 +19,7 @@ export declare class BatchedMigrationRunner {
|
|
|
19
19
|
private finishJob;
|
|
20
20
|
private getOrCreateNextMigrationJob;
|
|
21
21
|
private runMigrationJob;
|
|
22
|
-
run({ signal, iterations, durationMs
|
|
22
|
+
run({ signal, iterations, durationMs }?: {
|
|
23
23
|
signal?: AbortSignal;
|
|
24
24
|
iterations?: number;
|
|
25
25
|
durationMs?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-runner.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,KAAK,8BAA8B,EACnC,KAAK,mBAAmB,EAIzB,MAAM,wBAAwB,CAAC;AAIhC,UAAU,6BAA6B;IACrC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,sBAAsB;IACjC,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,uBAAuB,CAAiC;IAChE,OAAO,CAAC,eAAe,CAAyB;
|
|
1
|
+
{"version":3,"file":"batched-migration-runner.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,KAAK,8BAA8B,EACnC,KAAK,mBAAmB,EAIzB,MAAM,wBAAwB,CAAC;AAIhC,UAAU,6BAA6B;IACrC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,sBAAsB;IACjC,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,uBAAuB,CAAiC;IAChE,OAAO,CAAC,eAAe,CAAyB;IAEhD,YACE,SAAS,EAAE,mBAAmB,EAC9B,uBAAuB,EAAE,8BAA8B,EACvD,OAAO,GAAE,6BAAkC,EAM5C;IAED,OAAO,CAAC,GAAG;YAMG,iBAAiB;YAQjB,aAAa;YAQb,sBAAsB;YAQtB,sBAAsB;YActB,kBAAkB;YAkBlB,QAAQ;IAOtB,OAAO,CAAC,gBAAgB;YAUV,SAAS;YAaT,2BAA2B;YA0B3B,eAAe;IA+BvB,GAAG,CAAC,EACR,MAAM,EACN,UAAU,EACV,UAAU,EACX,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO,iBAezE;CACF","sourcesContent":["import { serializeError } from 'serialize-error';\nimport { z } from 'zod';\n\nimport { logger } from '@prairielearn/logger';\nimport { execute, loadSqlEquiv, queryOptionalRow, queryRow } from '@prairielearn/postgres';\n\nimport {\n type BatchedMigrationJobRow,\n BatchedMigrationJobRowSchema,\n type BatchedMigrationJobStatus,\n} from './batched-migration-job.js';\nimport {\n type BatchedMigrationImplementation,\n type BatchedMigrationRow,\n type BatchedMigrationStatus,\n BatchedMigrationStatusSchema,\n updateBatchedMigrationStatus,\n} from './batched-migration.js';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\ninterface BatchedMigrationRunnerOptions {\n logProgress?: boolean;\n}\n\nexport class BatchedMigrationRunner {\n private options: BatchedMigrationRunnerOptions;\n private migration: BatchedMigrationRow;\n private migrationImplementation: BatchedMigrationImplementation;\n private migrationStatus: BatchedMigrationStatus;\n\n constructor(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n options: BatchedMigrationRunnerOptions = {},\n ) {\n this.options = options;\n this.migration = migration;\n this.migrationImplementation = migrationImplementation;\n this.migrationStatus = migration.status;\n }\n\n private log(message: string, ...meta: any[]) {\n if (this.options.logProgress) {\n logger.info(`[${this.migration.filename}] ${message}`, ...meta);\n }\n }\n\n private async hasIncompleteJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_incomplete_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async hasFailedJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_failed_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async refreshMigrationStatus(migration: BatchedMigrationRow) {\n this.migrationStatus = await queryRow(\n sql.get_migration_status,\n { id: migration.id },\n BatchedMigrationStatusSchema,\n );\n }\n\n private async finishRunningMigration(migration: BatchedMigrationRow) {\n // Safety check: if there are any pending jobs, don't mark this\n // migration as finished.\n if (await this.hasIncompleteJobs(migration)) {\n this.log('Incomplete jobs found, not marking as finished');\n return;\n }\n\n const hasFailedJobs = await this.hasFailedJobs(migration);\n const finalStatus = hasFailedJobs ? 'failed' : 'succeeded';\n await updateBatchedMigrationStatus(migration.id, finalStatus);\n this.log(`Finished with status '${finalStatus}'`);\n }\n\n private async getNextBatchBounds(\n migration: BatchedMigrationRow,\n ): Promise<null | [bigint, bigint]> {\n const lastJob = await queryOptionalRow(\n sql.select_last_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n\n const nextMin = lastJob ? lastJob.max_value + 1n : migration.min_value;\n if (nextMin > migration.max_value) return null;\n\n let nextMax = nextMin + BigInt(migration.batch_size) - 1n;\n if (nextMax > migration.max_value) nextMax = migration.max_value;\n\n return [nextMin, nextMax];\n }\n\n private async startJob(job: BatchedMigrationJobRow) {\n await execute(sql.start_batched_migration_job, { id: job.id });\n const jobRange = `[${job.min_value}, ${job.max_value}]`;\n const migrationRange = `[${this.migration.min_value}, ${this.migration.max_value}]`;\n this.log(`Started job ${job.id} for range ${jobRange} in ${migrationRange}`);\n }\n\n private serializeJobData(data: unknown) {\n if (data == null) return null;\n\n // Return JSON-stringified data. Convert BigInts to strings.\n return JSON.stringify(data, (_key, value) => {\n if (typeof value === 'bigint') return value.toString();\n return value;\n });\n }\n\n private async finishJob(\n job: BatchedMigrationJobRow,\n status: Extract<BatchedMigrationJobStatus, 'failed' | 'succeeded'>,\n data?: unknown,\n ) {\n await execute(sql.finish_batched_migration_job, {\n id: job.id,\n status,\n data: this.serializeJobData(data),\n });\n this.log(`Job ${job.id} finished with status '${status}'`);\n }\n\n private async getOrCreateNextMigrationJob(\n migration: BatchedMigrationRow,\n ): Promise<BatchedMigrationJobRow | null> {\n const nextBatchBounds = await this.getNextBatchBounds(migration);\n if (nextBatchBounds) {\n return await queryRow(\n sql.insert_batched_migration_job,\n {\n batched_migration_id: migration.id,\n min_value: nextBatchBounds[0],\n max_value: nextBatchBounds[1],\n },\n BatchedMigrationJobRowSchema,\n );\n } else {\n // Pick up any old pending jobs from this migration. These will only exist if\n // an admin manually elected to retry all failed jobs; we'll never automatically\n // transition failed jobs back to pending.\n return await queryOptionalRow(\n sql.select_first_pending_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n }\n }\n\n private async runMigrationJob(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n ) {\n const nextJob = await this.getOrCreateNextMigrationJob(migration);\n if (nextJob) {\n await this.startJob(nextJob);\n\n let error = null;\n try {\n // We'll only handle errors thrown by the migration itself. If any of\n // our own execution machinery throws an error, we'll let it bubble up.\n await migrationImplementation.execute(nextJob.min_value, nextJob.max_value);\n } catch (err) {\n error = err;\n }\n\n if (error) {\n logger.error(\n `Error running job ${nextJob.id} for batched migration ${migration.filename}`,\n error,\n );\n await this.finishJob(nextJob, 'failed', { error: serializeError(error) });\n } else {\n await this.finishJob(nextJob, 'succeeded');\n }\n } else {\n await this.finishRunningMigration(migration);\n }\n }\n\n async run({\n signal,\n iterations,\n durationMs,\n }: { signal?: AbortSignal; iterations?: number; durationMs?: number } = {}) {\n let iterationCount = 0;\n const endTime = durationMs ? Date.now() + durationMs : null;\n while (\n !signal?.aborted &&\n (iterations ? iterationCount < iterations : true) &&\n (endTime ? Date.now() < endTime : true) &&\n (this.migrationStatus === 'running' || this.migrationStatus === 'finalizing')\n ) {\n await this.runMigrationJob(this.migration, this.migrationImplementation);\n iterationCount += 1;\n // Always refresh the status so we can detect if the migration was marked\n // as paused by another process.\n await this.refreshMigrationStatus(this.migration);\n }\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-runner.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { serializeError } from 'serialize-error';\nimport { z } from 'zod';\n\nimport { logger } from '@prairielearn/logger';\nimport { execute, loadSqlEquiv, queryOptionalRow, queryRow } from '@prairielearn/postgres';\n\nimport {\n type BatchedMigrationJobRow,\n BatchedMigrationJobRowSchema,\n type BatchedMigrationJobStatus,\n} from './batched-migration-job.js';\nimport {\n type BatchedMigrationImplementation,\n type BatchedMigrationRow,\n type BatchedMigrationStatus,\n BatchedMigrationStatusSchema,\n updateBatchedMigrationStatus,\n} from './batched-migration.js';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\ninterface BatchedMigrationRunnerOptions {\n logProgress?: boolean;\n}\n\nexport class BatchedMigrationRunner {\n private options: BatchedMigrationRunnerOptions;\n private migration: BatchedMigrationRow;\n private migrationImplementation: BatchedMigrationImplementation;\n private migrationStatus: BatchedMigrationStatus;\n\n constructor(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n options: BatchedMigrationRunnerOptions = {},\n ) {\n this.options = options;\n this.migration = migration;\n this.migrationImplementation = migrationImplementation;\n this.migrationStatus = migration.status;\n }\n\n private log(message: string, ...meta: any[]) {\n if (this.options.logProgress) {\n logger.info(`[${this.migration.filename}] ${message}`, ...meta);\n }\n }\n\n private async hasIncompleteJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_incomplete_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async hasFailedJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_failed_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async refreshMigrationStatus(migration: BatchedMigrationRow) {\n this.migrationStatus = await queryRow(\n sql.get_migration_status,\n { id: migration.id },\n BatchedMigrationStatusSchema,\n );\n }\n\n private async finishRunningMigration(migration: BatchedMigrationRow) {\n // Safety check: if there are any pending jobs, don't mark this\n // migration as finished.\n if (await this.hasIncompleteJobs(migration)) {\n this.log('Incomplete jobs found, not marking as finished');\n return;\n }\n\n const hasFailedJobs = await this.hasFailedJobs(migration);\n const finalStatus = hasFailedJobs ? 'failed' : 'succeeded';\n await updateBatchedMigrationStatus(migration.id, finalStatus);\n this.log(`Finished with status '${finalStatus}'`);\n }\n\n private async getNextBatchBounds(\n migration: BatchedMigrationRow,\n ): Promise<null | [bigint, bigint]> {\n const lastJob = await queryOptionalRow(\n sql.select_last_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n\n const nextMin = lastJob ? lastJob.max_value + 1n : migration.min_value;\n if (nextMin > migration.max_value) return null;\n\n let nextMax = nextMin + BigInt(migration.batch_size) - 1n;\n if (nextMax > migration.max_value) nextMax = migration.max_value;\n\n return [nextMin, nextMax];\n }\n\n private async startJob(job: BatchedMigrationJobRow) {\n await execute(sql.start_batched_migration_job, { id: job.id });\n const jobRange = `[${job.min_value}, ${job.max_value}]`;\n const migrationRange = `[${this.migration.min_value}, ${this.migration.max_value}]`;\n this.log(`Started job ${job.id} for range ${jobRange} in ${migrationRange}`);\n }\n\n private serializeJobData(data: unknown) {\n if (data == null) return null;\n\n // Return JSON-stringified data. Convert BigInts to strings.\n return JSON.stringify(data, (_key, value) => {\n if (typeof value === 'bigint') return value.toString();\n return value;\n });\n }\n\n private async finishJob(\n job: BatchedMigrationJobRow,\n status: Extract<BatchedMigrationJobStatus, 'failed' | 'succeeded'>,\n data?: unknown,\n ) {\n await execute(sql.finish_batched_migration_job, {\n id: job.id,\n status,\n data: this.serializeJobData(data),\n });\n this.log(`Job ${job.id} finished with status '${status}'`);\n }\n\n private async getOrCreateNextMigrationJob(\n migration: BatchedMigrationRow,\n ): Promise<BatchedMigrationJobRow | null> {\n const nextBatchBounds = await this.getNextBatchBounds(migration);\n if (nextBatchBounds) {\n return await queryRow(\n sql.insert_batched_migration_job,\n {\n batched_migration_id: migration.id,\n min_value: nextBatchBounds[0],\n max_value: nextBatchBounds[1],\n },\n BatchedMigrationJobRowSchema,\n );\n } else {\n // Pick up any old pending jobs from this migration. These will only exist if\n // an admin manually elected to retry all failed jobs; we'll never automatically\n // transition failed jobs back to pending.\n return await queryOptionalRow(\n sql.select_first_pending_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n }\n }\n\n private async runMigrationJob(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n ) {\n const nextJob = await this.getOrCreateNextMigrationJob(migration);\n if (nextJob) {\n await this.startJob(nextJob);\n\n let error = null;\n try {\n // We'll only handle errors thrown by the migration itself. If any of\n // our own execution machinery throws an error, we'll let it bubble up.\n await migrationImplementation.execute(nextJob.min_value, nextJob.max_value);\n } catch (err) {\n error = err;\n }\n\n if (error) {\n logger.error(\n `Error running job ${nextJob.id} for batched migration ${migration.filename}`,\n error,\n );\n await this.finishJob(nextJob, 'failed', { error: serializeError(error) });\n } else {\n await this.finishJob(nextJob, 'succeeded');\n }\n } else {\n await this.finishRunningMigration(migration);\n }\n }\n\n async run({\n signal,\n iterations,\n durationMs,\n }: { signal?: AbortSignal; iterations?: number; durationMs?: number } = {}) {\n let iterationCount = 0;\n const endTime = durationMs ? Date.now() + durationMs : null;\n while (\n !signal?.aborted &&\n (iterations ? iterationCount < iterations : true) &&\n (endTime ? Date.now() < endTime : true) &&\n (this.migrationStatus === 'running' || this.migrationStatus === 'finalizing')\n ) {\n await this.runMigrationJob(this.migration, this.migrationImplementation);\n iterationCount += 1;\n // Always refresh the status so we can detect if the migration was marked\n // as paused by another process.\n await this.refreshMigrationStatus(this.migration);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"batched-migration-runner.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { serializeError } from 'serialize-error';\nimport { z } from 'zod';\n\nimport { logger } from '@prairielearn/logger';\nimport { execute, loadSqlEquiv, queryOptionalRow, queryRow } from '@prairielearn/postgres';\n\nimport {\n type BatchedMigrationJobRow,\n BatchedMigrationJobRowSchema,\n type BatchedMigrationJobStatus,\n} from './batched-migration-job.js';\nimport {\n type BatchedMigrationImplementation,\n type BatchedMigrationRow,\n type BatchedMigrationStatus,\n BatchedMigrationStatusSchema,\n updateBatchedMigrationStatus,\n} from './batched-migration.js';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\ninterface BatchedMigrationRunnerOptions {\n logProgress?: boolean;\n}\n\nexport class BatchedMigrationRunner {\n private options: BatchedMigrationRunnerOptions;\n private migration: BatchedMigrationRow;\n private migrationImplementation: BatchedMigrationImplementation;\n private migrationStatus: BatchedMigrationStatus;\n\n constructor(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n options: BatchedMigrationRunnerOptions = {},\n ) {\n this.options = options;\n this.migration = migration;\n this.migrationImplementation = migrationImplementation;\n this.migrationStatus = migration.status;\n }\n\n private log(message: string, ...meta: any[]) {\n if (this.options.logProgress) {\n logger.info(`[${this.migration.filename}] ${message}`, ...meta);\n }\n }\n\n private async hasIncompleteJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_incomplete_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async hasFailedJobs(migration: BatchedMigrationRow): Promise<boolean> {\n return await queryRow(\n sql.batched_migration_has_failed_jobs,\n { batched_migration_id: migration.id },\n z.boolean(),\n );\n }\n\n private async refreshMigrationStatus(migration: BatchedMigrationRow) {\n this.migrationStatus = await queryRow(\n sql.get_migration_status,\n { id: migration.id },\n BatchedMigrationStatusSchema,\n );\n }\n\n private async finishRunningMigration(migration: BatchedMigrationRow) {\n // Safety check: if there are any pending jobs, don't mark this\n // migration as finished.\n if (await this.hasIncompleteJobs(migration)) {\n this.log('Incomplete jobs found, not marking as finished');\n return;\n }\n\n const hasFailedJobs = await this.hasFailedJobs(migration);\n const finalStatus = hasFailedJobs ? 'failed' : 'succeeded';\n await updateBatchedMigrationStatus(migration.id, finalStatus);\n this.log(`Finished with status '${finalStatus}'`);\n }\n\n private async getNextBatchBounds(\n migration: BatchedMigrationRow,\n ): Promise<null | [bigint, bigint]> {\n const lastJob = await queryOptionalRow(\n sql.select_last_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n\n const nextMin = lastJob ? lastJob.max_value + 1n : migration.min_value;\n if (nextMin > migration.max_value) return null;\n\n let nextMax = nextMin + BigInt(migration.batch_size) - 1n;\n if (nextMax > migration.max_value) nextMax = migration.max_value;\n\n return [nextMin, nextMax];\n }\n\n private async startJob(job: BatchedMigrationJobRow) {\n await execute(sql.start_batched_migration_job, { id: job.id });\n const jobRange = `[${job.min_value}, ${job.max_value}]`;\n const migrationRange = `[${this.migration.min_value}, ${this.migration.max_value}]`;\n this.log(`Started job ${job.id} for range ${jobRange} in ${migrationRange}`);\n }\n\n private serializeJobData(data: unknown) {\n if (data == null) return null;\n\n // Return JSON-stringified data. Convert BigInts to strings.\n return JSON.stringify(data, (_key, value) => {\n if (typeof value === 'bigint') return value.toString();\n return value;\n });\n }\n\n private async finishJob(\n job: BatchedMigrationJobRow,\n status: Extract<BatchedMigrationJobStatus, 'failed' | 'succeeded'>,\n data?: unknown,\n ) {\n await execute(sql.finish_batched_migration_job, {\n id: job.id,\n status,\n data: this.serializeJobData(data),\n });\n this.log(`Job ${job.id} finished with status '${status}'`);\n }\n\n private async getOrCreateNextMigrationJob(\n migration: BatchedMigrationRow,\n ): Promise<BatchedMigrationJobRow | null> {\n const nextBatchBounds = await this.getNextBatchBounds(migration);\n if (nextBatchBounds) {\n return await queryRow(\n sql.insert_batched_migration_job,\n {\n batched_migration_id: migration.id,\n min_value: nextBatchBounds[0],\n max_value: nextBatchBounds[1],\n },\n BatchedMigrationJobRowSchema,\n );\n } else {\n // Pick up any old pending jobs from this migration. These will only exist if\n // an admin manually elected to retry all failed jobs; we'll never automatically\n // transition failed jobs back to pending.\n return await queryOptionalRow(\n sql.select_first_pending_batched_migration_job,\n { batched_migration_id: migration.id },\n BatchedMigrationJobRowSchema,\n );\n }\n }\n\n private async runMigrationJob(\n migration: BatchedMigrationRow,\n migrationImplementation: BatchedMigrationImplementation,\n ) {\n const nextJob = await this.getOrCreateNextMigrationJob(migration);\n if (nextJob) {\n await this.startJob(nextJob);\n\n let error = null;\n try {\n // We'll only handle errors thrown by the migration itself. If any of\n // our own execution machinery throws an error, we'll let it bubble up.\n await migrationImplementation.execute(nextJob.min_value, nextJob.max_value);\n } catch (err) {\n error = err;\n }\n\n if (error) {\n logger.error(\n `Error running job ${nextJob.id} for batched migration ${migration.filename}`,\n error,\n );\n await this.finishJob(nextJob, 'failed', { error: serializeError(error) });\n } else {\n await this.finishJob(nextJob, 'succeeded');\n }\n } else {\n await this.finishRunningMigration(migration);\n }\n }\n\n async run({\n signal,\n iterations,\n durationMs,\n }: { signal?: AbortSignal; iterations?: number; durationMs?: number } = {}) {\n let iterationCount = 0;\n const endTime = durationMs ? Date.now() + durationMs : null;\n while (\n !signal?.aborted &&\n (iterations ? iterationCount < iterations : true) &&\n (endTime ? Date.now() < endTime : true) &&\n (this.migrationStatus === 'running' || this.migrationStatus === 'finalizing')\n ) {\n await this.runMigrationJob(this.migration, this.migrationImplementation);\n iterationCount += 1;\n // Always refresh the status so we can detect if the migration was marked\n // as paused by another process.\n await this.refreshMigrationStatus(this.migration);\n }\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-runner.test.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"batched-migration-runner.test.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.test.ts"],"names":[],"mappings":"","sourcesContent":["import { afterAll, assert, beforeAll, beforeEach, describe, it } from 'vitest';\n\nimport * as error from '@prairielearn/error';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport { execute, makePostgresTestUtils, queryRow, queryRows } from '@prairielearn/postgres';\n\nimport { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';\n\nimport { BatchedMigrationJobRowSchema } from './batched-migration-job.js';\nimport { BatchedMigrationRunner } from './batched-migration-runner.js';\nimport {\n BatchedMigrationRowSchema,\n insertBatchedMigration,\n makeBatchedMigration,\n updateBatchedMigrationStatus,\n} from './batched-migration.js';\n\nconst postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n});\n\nfunction makeTestBatchMigration() {\n let executionCount = 0;\n let failingIds: bigint[] = [];\n\n return makeBatchedMigration({\n async getParameters() {\n return {\n min: 1n,\n max: 10000n,\n batchSize: 1000,\n };\n },\n async execute(start: bigint, end: bigint) {\n executionCount += 1;\n const shouldFail = failingIds.some((id) => id >= start && id <= end);\n if (shouldFail) {\n // Throw an error with some data to make sure it gets persisted. We\n // specifically use BigInt values here to make sure that they are\n // correctly serialized to strings.\n throw new error.AugmentedError('Execution failure', { data: { start, end } });\n }\n },\n setFailingIds(ids: bigint[]) {\n failingIds = ids;\n },\n get executionCount() {\n return executionCount;\n },\n });\n}\n\nasync function getBatchedMigration(migrationId: string) {\n return await queryRow(\n 'SELECT * FROM batched_migrations WHERE id = $id;',\n { id: migrationId },\n BatchedMigrationRowSchema,\n );\n}\n\nasync function getBatchedMigrationJobs(migrationId: string) {\n return await queryRows(\n 'SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;',\n { batched_migration_id: migrationId },\n BatchedMigrationJobRowSchema,\n );\n}\n\nasync function resetFailedBatchedMigrationJobs(migrationId: string) {\n await execute(\n \"UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'\",\n { batched_migration_id: migrationId },\n );\n}\n\nasync function insertTestBatchedMigration() {\n const migrationImplementation = makeTestBatchMigration();\n const parameters = await migrationImplementation.getParameters();\n const migration = await insertBatchedMigration({\n project: 'test',\n filename: '20230406184103_test_batch_migration.js',\n timestamp: '20230406184103',\n batch_size: parameters.batchSize,\n min_value: parameters.min,\n max_value: parameters.max,\n status: 'running',\n });\n if (!migration) throw new Error('Failed to insert batched migration');\n return migration;\n}\n\ndescribe('BatchedMigrationExecutor', () => {\n beforeAll(async () => {\n const poolConfig = await postgresTestUtils.createDatabase();\n await namedLocks.init(poolConfig, (err) => {\n throw err;\n });\n await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });\n });\n\n beforeEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n afterAll(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs one iteration of a batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const executor = new BatchedMigrationRunner(migration, migrationImplementation);\n await executor.run({ iterations: 1 });\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 1);\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'running');\n\n assert.equal(migrationImplementation.executionCount, 1);\n });\n\n it('runs an entire batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 10);\n assert.equal(jobs[0].min_value, 1n);\n assert.equal(jobs[0].max_value, 1000n);\n assert.equal(jobs.at(-1)?.min_value, 9001n);\n assert.equal(jobs.at(-1)?.max_value, 10000n);\n assert.isTrue(jobs.every((job) => job.started_at !== null));\n assert.isTrue(jobs.every((job) => job.finished_at !== null));\n assert.isTrue(jobs.every((job) => job.status === 'succeeded'));\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'succeeded');\n });\n\n it('handles failing execution', async () => {\n let migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n migrationImplementation.setFailingIds([1n, 5010n]);\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n const failedJobs = jobs.filter((job) => job.status === 'failed');\n const successfulJobs = jobs.filter((job) => job.status === 'succeeded');\n assert.lengthOf(jobs, 10);\n assert.lengthOf(failedJobs, 2);\n assert.lengthOf(successfulJobs, 8);\n assert.equal(migrationImplementation.executionCount, 10);\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n failedJobs.forEach((job) => {\n const jobData = job.data as any;\n assert.isObject(jobData);\n assert.isObject(jobData.error);\n assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);\n assert.equal(jobData.error.name, 'Error');\n assert.equal(jobData.error.message, 'Execution failure');\n assert.equal(jobData.error.data.start, job.min_value.toString());\n assert.equal(jobData.error.data.end, job.max_value.toString());\n });\n\n const failedMigration = await getBatchedMigration(migration.id);\n assert.equal(failedMigration.status, 'failed');\n\n // Retry the failed jobs; ensure they succeed this time.\n await resetFailedBatchedMigrationJobs(migration.id);\n migration = await updateBatchedMigrationStatus(migration.id, 'running');\n\n migrationImplementation.setFailingIds([]);\n const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);\n await retryRunner.run();\n\n const finalJobs = await getBatchedMigrationJobs(migration.id);\n const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');\n const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');\n const retriedJobs = finalJobs.filter((job) => job.attempts === 2);\n assert.lengthOf(finalJobs, 10);\n assert.lengthOf(finalFailedJobs, 0);\n assert.lengthOf(finalSuccessfulJobs, 10);\n assert.lengthOf(retriedJobs, 2);\n assert.isTrue(finalJobs.every((job) => job.data === null));\n\n migration = await getBatchedMigration(migration.id);\n assert.equal(migration.status, 'succeeded');\n\n // The runner should have run only the previously failed jobs, which\n // works out to 2 additional execution.\n assert.equal(migrationImplementation.executionCount, 12);\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-runner.test.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { afterAll, assert, beforeAll, beforeEach, describe, it } from 'vitest';\n\nimport * as error from '@prairielearn/error';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport { execute, makePostgresTestUtils, queryRow, queryRows } from '@prairielearn/postgres';\n\nimport { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';\n\nimport { BatchedMigrationJobRowSchema } from './batched-migration-job.js';\nimport { BatchedMigrationRunner } from './batched-migration-runner.js';\nimport {\n BatchedMigrationRowSchema,\n insertBatchedMigration,\n makeBatchedMigration,\n updateBatchedMigrationStatus,\n} from './batched-migration.js';\n\nconst postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n});\n\nfunction makeTestBatchMigration() {\n let executionCount = 0;\n let failingIds: bigint[] = [];\n\n return makeBatchedMigration({\n async getParameters() {\n return {\n min: 1n,\n max: 10000n,\n batchSize: 1000,\n };\n },\n async execute(start: bigint, end: bigint) {\n executionCount += 1;\n const shouldFail = failingIds.some((id) => id >= start && id <= end);\n if (shouldFail) {\n // Throw an error with some data to make sure it gets persisted. We\n // specifically use BigInt values here to make sure that they are\n // correctly serialized to strings.\n throw new error.AugmentedError('Execution failure', { data: { start, end } });\n }\n },\n setFailingIds(ids: bigint[]) {\n failingIds = ids;\n },\n get executionCount() {\n return executionCount;\n },\n });\n}\n\nasync function getBatchedMigration(migrationId: string) {\n return await queryRow(\n 'SELECT * FROM batched_migrations WHERE id = $id;',\n { id: migrationId },\n BatchedMigrationRowSchema,\n );\n}\n\nasync function getBatchedMigrationJobs(migrationId: string) {\n return await queryRows(\n 'SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;',\n { batched_migration_id: migrationId },\n BatchedMigrationJobRowSchema,\n );\n}\n\nasync function resetFailedBatchedMigrationJobs(migrationId: string) {\n await execute(\n \"UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'\",\n { batched_migration_id: migrationId },\n );\n}\n\nasync function insertTestBatchedMigration() {\n const migrationImplementation = makeTestBatchMigration();\n const parameters = await migrationImplementation.getParameters();\n const migration = await insertBatchedMigration({\n project: 'test',\n filename: '20230406184103_test_batch_migration.js',\n timestamp: '20230406184103',\n batch_size: parameters.batchSize,\n min_value: parameters.min,\n max_value: parameters.max,\n status: 'running',\n });\n if (!migration) throw new Error('Failed to insert batched migration');\n return migration;\n}\n\ndescribe('BatchedMigrationExecutor', () => {\n beforeAll(async () => {\n const poolConfig = await postgresTestUtils.createDatabase();\n await namedLocks.init(poolConfig, (err) => {\n throw err;\n });\n await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });\n });\n\n beforeEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n afterAll(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs one iteration of a batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const executor = new BatchedMigrationRunner(migration, migrationImplementation);\n await executor.run({ iterations: 1 });\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 1);\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'running');\n\n assert.equal(migrationImplementation.executionCount, 1);\n });\n\n it('runs an entire batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 10);\n assert.equal(jobs[0].min_value, 1n);\n assert.equal(jobs[0].max_value, 1000n);\n assert.equal(jobs.at(-1)?.min_value, 9001n);\n assert.equal(jobs.at(-1)?.max_value, 10000n);\n assert.isTrue(jobs.every((job) => job.started_at !== null));\n assert.isTrue(jobs.every((job) => job.finished_at !== null));\n assert.isTrue(jobs.every((job) => job.status === 'succeeded'));\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'succeeded');\n });\n\n it('handles failing execution', async () => {\n let migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n migrationImplementation.setFailingIds([1n, 5010n]);\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n const failedJobs = jobs.filter((job) => job.status === 'failed');\n const successfulJobs = jobs.filter((job) => job.status === 'succeeded');\n assert.lengthOf(jobs, 10);\n assert.lengthOf(failedJobs, 2);\n assert.lengthOf(successfulJobs, 8);\n assert.equal(migrationImplementation.executionCount, 10);\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n failedJobs.forEach((job) => {\n const jobData = job.data as any;\n assert.isObject(jobData);\n assert.isObject(jobData.error);\n assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);\n assert.equal(jobData.error.name, 'Error');\n assert.equal(jobData.error.message, 'Execution failure');\n assert.equal(jobData.error.data.start, job.min_value.toString());\n assert.equal(jobData.error.data.end, job.max_value.toString());\n });\n\n const failedMigration = await getBatchedMigration(migration.id);\n assert.equal(failedMigration.status, 'failed');\n\n // Retry the failed jobs; ensure they succeed this time.\n await resetFailedBatchedMigrationJobs(migration.id);\n migration = await updateBatchedMigrationStatus(migration.id, 'running');\n\n migrationImplementation.setFailingIds([]);\n const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);\n await retryRunner.run();\n\n const finalJobs = await getBatchedMigrationJobs(migration.id);\n const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');\n const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');\n const retriedJobs = finalJobs.filter((job) => job.attempts === 2);\n assert.lengthOf(finalJobs, 10);\n assert.lengthOf(finalFailedJobs, 0);\n assert.lengthOf(finalSuccessfulJobs, 10);\n assert.lengthOf(retriedJobs, 2);\n assert.isTrue(finalJobs.every((job) => job.data === null));\n\n migration = await getBatchedMigration(migration.id);\n assert.equal(migration.status, 'succeeded');\n\n // The runner should have run only the previously failed jobs, which\n // works out to 2 additional execution.\n assert.equal(migrationImplementation.executionCount, 12);\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"batched-migration-runner.test.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { afterAll, assert, beforeAll, beforeEach, describe, it } from 'vitest';\n\nimport * as error from '@prairielearn/error';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport { execute, makePostgresTestUtils, queryRow, queryRows } from '@prairielearn/postgres';\n\nimport { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';\n\nimport { BatchedMigrationJobRowSchema } from './batched-migration-job.js';\nimport { BatchedMigrationRunner } from './batched-migration-runner.js';\nimport {\n BatchedMigrationRowSchema,\n insertBatchedMigration,\n makeBatchedMigration,\n updateBatchedMigrationStatus,\n} from './batched-migration.js';\n\nconst postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n});\n\nfunction makeTestBatchMigration() {\n let executionCount = 0;\n let failingIds: bigint[] = [];\n\n return makeBatchedMigration({\n async getParameters() {\n return {\n min: 1n,\n max: 10000n,\n batchSize: 1000,\n };\n },\n async execute(start: bigint, end: bigint) {\n executionCount += 1;\n const shouldFail = failingIds.some((id) => id >= start && id <= end);\n if (shouldFail) {\n // Throw an error with some data to make sure it gets persisted. We\n // specifically use BigInt values here to make sure that they are\n // correctly serialized to strings.\n throw new error.AugmentedError('Execution failure', { data: { start, end } });\n }\n },\n setFailingIds(ids: bigint[]) {\n failingIds = ids;\n },\n get executionCount() {\n return executionCount;\n },\n });\n}\n\nasync function getBatchedMigration(migrationId: string) {\n return await queryRow(\n 'SELECT * FROM batched_migrations WHERE id = $id;',\n { id: migrationId },\n BatchedMigrationRowSchema,\n );\n}\n\nasync function getBatchedMigrationJobs(migrationId: string) {\n return await queryRows(\n 'SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;',\n { batched_migration_id: migrationId },\n BatchedMigrationJobRowSchema,\n );\n}\n\nasync function resetFailedBatchedMigrationJobs(migrationId: string) {\n await execute(\n \"UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'\",\n { batched_migration_id: migrationId },\n );\n}\n\nasync function insertTestBatchedMigration() {\n const migrationImplementation = makeTestBatchMigration();\n const parameters = await migrationImplementation.getParameters();\n const migration = await insertBatchedMigration({\n project: 'test',\n filename: '20230406184103_test_batch_migration.js',\n timestamp: '20230406184103',\n batch_size: parameters.batchSize,\n min_value: parameters.min,\n max_value: parameters.max,\n status: 'running',\n });\n if (!migration) throw new Error('Failed to insert batched migration');\n return migration;\n}\n\ndescribe('BatchedMigrationExecutor', () => {\n beforeAll(async () => {\n const poolConfig = await postgresTestUtils.createDatabase();\n await namedLocks.init(poolConfig, (err) => {\n throw err;\n });\n await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });\n });\n\n beforeEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n afterAll(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs one iteration of a batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const executor = new BatchedMigrationRunner(migration, migrationImplementation);\n await executor.run({ iterations: 1 });\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 1);\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'running');\n\n assert.equal(migrationImplementation.executionCount, 1);\n });\n\n it('runs an entire batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 10);\n assert.equal(jobs[0].min_value, 1n);\n assert.equal(jobs[0].max_value, 1000n);\n assert.equal(jobs.at(-1)?.min_value, 9001n);\n assert.equal(jobs.at(-1)?.max_value, 10000n);\n assert.isTrue(jobs.every((job) => job.started_at !== null));\n assert.isTrue(jobs.every((job) => job.finished_at !== null));\n assert.isTrue(jobs.every((job) => job.status === 'succeeded'));\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'succeeded');\n });\n\n it('handles failing execution', async () => {\n let migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n migrationImplementation.setFailingIds([1n, 5010n]);\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n const failedJobs = jobs.filter((job) => job.status === 'failed');\n const successfulJobs = jobs.filter((job) => job.status === 'succeeded');\n assert.lengthOf(jobs, 10);\n assert.lengthOf(failedJobs, 2);\n assert.lengthOf(successfulJobs, 8);\n assert.equal(migrationImplementation.executionCount, 10);\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n failedJobs.forEach((job) => {\n const jobData = job.data as any;\n assert.isObject(jobData);\n assert.isObject(jobData.error);\n assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);\n assert.equal(jobData.error.name, 'Error');\n assert.equal(jobData.error.message, 'Execution failure');\n assert.equal(jobData.error.data.start, job.min_value.toString());\n assert.equal(jobData.error.data.end, job.max_value.toString());\n });\n\n const failedMigration = await getBatchedMigration(migration.id);\n assert.equal(failedMigration.status, 'failed');\n\n // Retry the failed jobs; ensure they succeed this time.\n await resetFailedBatchedMigrationJobs(migration.id);\n migration = await updateBatchedMigrationStatus(migration.id, 'running');\n\n migrationImplementation.setFailingIds([]);\n const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);\n await retryRunner.run();\n\n const finalJobs = await getBatchedMigrationJobs(migration.id);\n const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');\n const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');\n const retriedJobs = finalJobs.filter((job) => job.attempts === 2);\n assert.lengthOf(finalJobs, 10);\n assert.lengthOf(finalFailedJobs, 0);\n assert.lengthOf(finalSuccessfulJobs, 10);\n assert.lengthOf(retriedJobs, 2);\n assert.isTrue(finalJobs.every((job) => job.data === null));\n\n migration = await getBatchedMigration(migration.id);\n assert.equal(migration.status, 'succeeded');\n\n // The runner should have run only the previously failed jobs, which\n // works out to 2 additional execution.\n assert.equal(migrationImplementation.executionCount, 12);\n });\n});\n"]}
|
|
@@ -14,26 +14,26 @@ export declare const BatchedMigrationRowSchema: z.ZodObject<{
|
|
|
14
14
|
updated_at: z.ZodDate;
|
|
15
15
|
started_at: z.ZodNullable<z.ZodDate>;
|
|
16
16
|
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
id: string;
|
|
18
|
+
project: string;
|
|
17
19
|
filename: string;
|
|
18
20
|
timestamp: string;
|
|
19
|
-
project: string;
|
|
20
|
-
status: "pending" | "paused" | "running" | "finalizing" | "failed" | "succeeded";
|
|
21
|
-
id: string;
|
|
22
21
|
batch_size: number;
|
|
23
22
|
min_value: bigint;
|
|
24
23
|
max_value: bigint;
|
|
24
|
+
status: "failed" | "finalizing" | "paused" | "pending" | "running" | "succeeded";
|
|
25
25
|
created_at: Date;
|
|
26
26
|
updated_at: Date;
|
|
27
27
|
started_at: Date | null;
|
|
28
28
|
}, {
|
|
29
|
+
id: string;
|
|
30
|
+
project: string;
|
|
29
31
|
filename: string;
|
|
30
32
|
timestamp: string;
|
|
31
|
-
project: string;
|
|
32
|
-
status: "pending" | "paused" | "running" | "finalizing" | "failed" | "succeeded";
|
|
33
|
-
id: string;
|
|
34
33
|
batch_size: number;
|
|
35
34
|
min_value: bigint;
|
|
36
35
|
max_value: bigint;
|
|
36
|
+
status: "failed" | "finalizing" | "paused" | "pending" | "running" | "succeeded";
|
|
37
37
|
created_at: Date;
|
|
38
38
|
updated_at: Date;
|
|
39
39
|
started_at: Date | null;
|
|
@@ -60,14 +60,14 @@ type NewBatchedMigration = Pick<BatchedMigrationRow, 'project' | 'filename' | 't
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function insertBatchedMigration(migration: NewBatchedMigration): Promise<BatchedMigrationRow | null>;
|
|
62
62
|
export declare function selectAllBatchedMigrations(project: string): Promise<{
|
|
63
|
+
id: string;
|
|
64
|
+
project: string;
|
|
63
65
|
filename: string;
|
|
64
66
|
timestamp: string;
|
|
65
|
-
project: string;
|
|
66
|
-
status: "pending" | "paused" | "running" | "finalizing" | "failed" | "succeeded";
|
|
67
|
-
id: string;
|
|
68
67
|
batch_size: number;
|
|
69
68
|
min_value: bigint;
|
|
70
69
|
max_value: bigint;
|
|
70
|
+
status: "failed" | "finalizing" | "paused" | "pending" | "running" | "succeeded";
|
|
71
71
|
created_at: Date;
|
|
72
72
|
updated_at: Date;
|
|
73
73
|
started_at: Date | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,4BAA4B,kFAOvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC7B,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B;IAC7C,aAAa,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACrD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,8BAA8B,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAGxF;AAED,wBAAgB,sCAAsC,CACpD,GAAG,EAAE,8BAA8B,GAClC,OAAO,CAAC,GAAG,IAAI,8BAA8B,CAO/C;AAED,KAAK,mBAAmB,GAAG,IAAI,CAC7B,mBAAmB,EACnB,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,QAAQ,CAC3F,CAAC;AAEF;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAErC;AAED,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,MAAM;;;;;;;;;;;;KAE/D;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,mBAAmB,CAAC,CAE9B;AAED,wBAAsB,kCAAkC,CACtD,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,mBAAmB,CAAC,CAM9B;AAED,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,mBAAmB,CAAC,CAM9B;AAED,wBAAsB,+BAA+B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhG"}
|
|
1
|
+
{"version":3,"file":"batched-migration.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,4BAA4B,kFAOvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC7B,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B;IAC7C,aAAa,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACrD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,8BAA8B,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAGxF;AAED,wBAAgB,sCAAsC,CACpD,GAAG,EAAE,8BAA8B,GAClC,OAAO,CAAC,GAAG,IAAI,8BAA8B,CAO/C;AAED,KAAK,mBAAmB,GAAG,IAAI,CAC7B,mBAAmB,EACnB,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,QAAQ,CAC3F,CAAC;AAEF;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAErC;AAED,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,MAAM;;;;;;;;;;;;KAE/D;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,mBAAmB,CAAC,CAE9B;AAED,wBAAsB,kCAAkC,CACtD,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,mBAAmB,CAAC,CAM9B;AAED,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,mBAAmB,CAAC,CAM9B;AAED,wBAAsB,+BAA+B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhG","sourcesContent":["import { z } from 'zod';\n\nimport {\n execute,\n loadSqlEquiv,\n queryOptionalRow,\n queryRow,\n queryRows,\n} from '@prairielearn/postgres';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\nexport const BatchedMigrationStatusSchema = z.enum([\n 'pending',\n 'paused',\n 'running',\n 'finalizing',\n 'failed',\n 'succeeded',\n]);\nexport type BatchedMigrationStatus = z.infer<typeof BatchedMigrationStatusSchema>;\n\nexport const BatchedMigrationRowSchema = z.object({\n id: z.string(),\n project: z.string(),\n filename: z.string(),\n timestamp: z.string(),\n batch_size: z.number(),\n min_value: z.bigint({ coerce: true }),\n max_value: z.bigint({ coerce: true }),\n status: BatchedMigrationStatusSchema,\n created_at: z.date(),\n updated_at: z.date(),\n started_at: z.date().nullable(),\n});\nexport type BatchedMigrationRow = z.infer<typeof BatchedMigrationRowSchema>;\n\nexport interface BatchedMigrationParameters {\n min?: bigint | string | null;\n max: bigint | string | null;\n batchSize?: number;\n}\n\nexport interface BatchedMigrationImplementation {\n getParameters(): Promise<BatchedMigrationParameters>;\n execute(start: bigint, end: bigint): Promise<void>;\n}\n\n/**\n * Identity function that helps to write correct batched migrations.\n */\nexport function makeBatchedMigration<T extends BatchedMigrationImplementation>(fns: T): T {\n validateBatchedMigrationImplementation(fns);\n return fns;\n}\n\nexport function validateBatchedMigrationImplementation(\n fns: BatchedMigrationImplementation,\n): asserts fns is BatchedMigrationImplementation {\n if (typeof fns.getParameters !== 'function') {\n throw new Error('getParameters() must be a function');\n }\n if (typeof fns.execute !== 'function') {\n throw new Error('execute() must be a function');\n }\n}\n\ntype NewBatchedMigration = Pick<\n BatchedMigrationRow,\n 'project' | 'filename' | 'timestamp' | 'batch_size' | 'min_value' | 'max_value' | 'status'\n>;\n\n/**\n * Inserts a new batched migration. If one already exists for the given\n * project/timestamp pair, returns null, otherwise returns the inserted row.\n */\nexport async function insertBatchedMigration(\n migration: NewBatchedMigration,\n): Promise<BatchedMigrationRow | null> {\n return await queryOptionalRow(sql.insert_batched_migration, migration, BatchedMigrationRowSchema);\n}\n\nexport async function selectAllBatchedMigrations(project: string) {\n return await queryRows(sql.select_all_batched_migrations, { project }, BatchedMigrationRowSchema);\n}\n\nexport async function selectBatchedMigration(\n project: string,\n id: string,\n): Promise<BatchedMigrationRow> {\n return await queryRow(sql.select_batched_migration, { project, id }, BatchedMigrationRowSchema);\n}\n\nexport async function selectBatchedMigrationForTimestamp(\n project: string,\n timestamp: string,\n): Promise<BatchedMigrationRow> {\n return await queryRow(\n sql.select_batched_migration_for_timestamp,\n { project, timestamp },\n BatchedMigrationRowSchema,\n );\n}\n\nexport async function updateBatchedMigrationStatus(\n id: string,\n status: BatchedMigrationStatus,\n): Promise<BatchedMigrationRow> {\n return await queryRow(\n sql.update_batched_migration_status,\n { id, status },\n BatchedMigrationRowSchema,\n );\n}\n\nexport async function retryFailedBatchedMigrationJobs(project: string, id: string): Promise<void> {\n await execute(sql.retry_failed_jobs, { project, id });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,OAAO,EACP,YAAY,EACZ,gBAAgB,EAChB,QAAQ,EACR,SAAS,GACV,MAAM,wBAAwB,CAAC;AAEhC,MAAM,GAAG,GAAG,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"batched-migration.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\n\nimport {\n execute,\n loadSqlEquiv,\n queryOptionalRow,\n queryRow,\n queryRows,\n} from '@prairielearn/postgres';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\nexport const BatchedMigrationStatusSchema = z.enum([\n 'pending',\n 'paused',\n 'running',\n 'finalizing',\n 'failed',\n 'succeeded',\n]);\nexport type BatchedMigrationStatus = z.infer<typeof BatchedMigrationStatusSchema>;\n\nexport const BatchedMigrationRowSchema = z.object({\n id: z.string(),\n project: z.string(),\n filename: z.string(),\n timestamp: z.string(),\n batch_size: z.number(),\n min_value: z.bigint({ coerce: true }),\n max_value: z.bigint({ coerce: true }),\n status: BatchedMigrationStatusSchema,\n created_at: z.date(),\n updated_at: z.date(),\n started_at: z.date().nullable(),\n});\nexport type BatchedMigrationRow = z.infer<typeof BatchedMigrationRowSchema>;\n\nexport interface BatchedMigrationParameters {\n min?: bigint | string | null;\n max: bigint | string | null;\n batchSize?: number;\n}\n\nexport interface BatchedMigrationImplementation {\n getParameters(): Promise<BatchedMigrationParameters>;\n execute(start: bigint, end: bigint): Promise<void>;\n}\n\n/**\n * Identity function that helps to write correct batched migrations.\n */\nexport function makeBatchedMigration<T extends BatchedMigrationImplementation>(fns: T): T {\n validateBatchedMigrationImplementation(fns);\n return fns;\n}\n\nexport function validateBatchedMigrationImplementation(\n fns: BatchedMigrationImplementation,\n): asserts fns is BatchedMigrationImplementation {\n if (typeof fns.getParameters !== 'function') {\n throw new Error('getParameters() must be a function');\n }\n if (typeof fns.execute !== 'function') {\n throw new Error('execute() must be a function');\n }\n}\n\ntype NewBatchedMigration = Pick<\n BatchedMigrationRow,\n 'project' | 'filename' | 'timestamp' | 'batch_size' | 'min_value' | 'max_value' | 'status'\n>;\n\n/**\n * Inserts a new batched migration. If one already exists for the given\n * project/timestamp pair, returns null, otherwise returns the inserted row.\n */\nexport async function insertBatchedMigration(\n migration: NewBatchedMigration,\n): Promise<BatchedMigrationRow | null> {\n return await queryOptionalRow(sql.insert_batched_migration, migration, BatchedMigrationRowSchema);\n}\n\nexport async function selectAllBatchedMigrations(project: string) {\n return await queryRows(sql.select_all_batched_migrations, { project }, BatchedMigrationRowSchema);\n}\n\nexport async function selectBatchedMigration(\n project: string,\n id: string,\n): Promise<BatchedMigrationRow> {\n return await queryRow(sql.select_batched_migration, { project, id }, BatchedMigrationRowSchema);\n}\n\nexport async function selectBatchedMigrationForTimestamp(\n project: string,\n timestamp: string,\n): Promise<BatchedMigrationRow> {\n return await queryRow(\n sql.select_batched_migration_for_timestamp,\n { project, timestamp },\n BatchedMigrationRowSchema,\n );\n}\n\nexport async function updateBatchedMigrationStatus(\n id: string,\n status: BatchedMigrationStatus,\n): Promise<BatchedMigrationRow> {\n return await queryRow(\n sql.update_batched_migration_status,\n { id, status },\n BatchedMigrationRowSchema,\n );\n}\n\nexport async function retryFailedBatchedMigrationJobs(project: string, id: string): Promise<void> {\n await execute(sql.retry_failed_jobs, { project, id });\n}\n"]}
|
|
@@ -20,12 +20,6 @@ export declare class BatchedMigrationsRunner extends EventEmitter {
|
|
|
20
20
|
private lockNameForTimestamp;
|
|
21
21
|
private getMigrationFiles;
|
|
22
22
|
private getMigrationForIdentifier;
|
|
23
|
-
/**
|
|
24
|
-
* Loads the implementation for the migration with the given identifier. The identifier
|
|
25
|
-
* must start with a 14-character timestamp. It may optionally be followed by
|
|
26
|
-
* an underscore with additional characters, which are ignored. These should
|
|
27
|
-
* typically be used to provide a human-readable name for the migration.
|
|
28
|
-
*/
|
|
29
23
|
private loadMigrationImplementation;
|
|
30
24
|
enqueueBatchedMigration(identifier: string): Promise<void>;
|
|
31
25
|
finalizeBatchedMigration(identifier: string, options?: BatchedMigrationFinalizeOptions): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migrations-runner.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgC3C,UAAU,6BAA6B;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,UAAU,4BAA4B;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,+BAA+B;IACvC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,uBAAwB,SAAQ,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,eAAe,CAAyB;gBAEpC,OAAO,EAAE,6BAA6B;IAMlD,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,iBAAiB,CAQvB;YAEY,yBAAyB;IAQvC;;;;;OAKG;YACW,2BAA2B;IAUnC,uBAAuB,CAAC,UAAU,EAAE,MAAM;IA6B1C,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,+BAA+B;IAsC5F,KAAK,CAAC,OAAO,GAAE,4BAAiC;IAQ1C,IAAI,CAAC,EAAE,cAAc,EAAE,eAAe,EAAE,EAAE,4BAA4B;YAsC9D,mBAAmB;IA2B3B,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAuCtD,IAAI;CAQX;AAUD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,6BAA6B,2BAI3E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,4BAAiC,2BAIhF;AAED,wBAAsB,qBAAqB,kBAI1C;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAAC,UAAU,EAAE,MAAM,iBAG/D;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,+BAA+B,iBAI1C"}
|
|
1
|
+
{"version":3,"file":"batched-migrations-runner.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgC3C,UAAU,6BAA6B;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,UAAU,4BAA4B;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,+BAA+B;IACvC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,uBAAwB,SAAQ,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,eAAe,CAAyB;IAEhD,YAAY,OAAO,EAAE,6BAA6B,EAIjD;IAED,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,iBAAiB,CAQvB;YAEY,yBAAyB;YAczB,2BAA2B;IAUnC,uBAAuB,CAAC,UAAU,EAAE,MAAM,iBA2B/C;IAEK,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,+BAA+B,iBAoC3F;IAED,KAAK,CAAC,OAAO,GAAE,4BAAiC,QAM/C;IAEK,IAAI,CAAC,EAAE,cAAc,EAAE,eAAe,EAAE,EAAE,4BAA4B,iBAoC3E;YAEa,mBAAmB;IA2B3B,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAqC3D;IAEK,IAAI,kBAOT;CACF;AAUD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,6BAA6B,2BAI3E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,4BAAiC,2BAIhF;AAED,wBAAsB,qBAAqB,kBAI1C;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAAC,UAAU,EAAE,MAAM,iBAG/D;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,+BAA+B,iBAI1C","sourcesContent":["import { EventEmitter } from 'node:events';\nimport path from 'node:path';\nimport { setTimeout as sleep } from 'node:timers/promises';\n\nimport { doWithLock } from '@prairielearn/named-locks';\nimport { loadSqlEquiv, queryOptionalRow } from '@prairielearn/postgres';\n\nimport {\n type MigrationFile,\n readAndValidateMigrationsFromDirectories,\n} from '../load-migrations.js';\n\nimport { BatchedMigrationRunner } from './batched-migration-runner.js';\nimport {\n type BatchedMigrationImplementation,\n type BatchedMigrationRow,\n BatchedMigrationRowSchema,\n type BatchedMigrationStatus,\n insertBatchedMigration,\n selectBatchedMigrationForTimestamp,\n updateBatchedMigrationStatus,\n validateBatchedMigrationImplementation,\n} from './batched-migration.js';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\nconst DEFAULT_MIN_VALUE = 1n;\nconst DEFAULT_BATCH_SIZE = 1_000;\nconst DEFAULT_WORK_DURATION_MS = 60_000;\nconst DEFAULT_SLEEP_DURATION_MS = 30_000;\nconst EXTENSIONS = ['.js', '.ts', '.mjs', '.mts'];\n\ninterface BatchedMigrationRunnerOptions {\n project: string;\n directories: string[];\n}\n\ninterface BatchedMigrationStartOptions {\n workDurationMs?: number;\n sleepDurationMs?: number;\n}\n\ninterface BatchedMigrationFinalizeOptions {\n logProgress?: boolean;\n}\n\nexport class BatchedMigrationsRunner extends EventEmitter {\n private readonly options: BatchedMigrationRunnerOptions;\n private readonly lockName: string;\n private running = false;\n private migrationFiles: MigrationFile[] | null = null;\n private abortController = new AbortController();\n\n constructor(options: BatchedMigrationRunnerOptions) {\n super();\n this.options = options;\n this.lockName = `batched-migrations:${this.options.project}`;\n }\n\n private lockNameForTimestamp(timestamp: string) {\n return `${this.lockName}:${timestamp}`;\n }\n\n private getMigrationFiles = async () => {\n if (!this.migrationFiles) {\n this.migrationFiles = await readAndValidateMigrationsFromDirectories(\n this.options.directories,\n EXTENSIONS,\n );\n }\n return this.migrationFiles;\n };\n\n private async getMigrationForIdentifier(identifier: string): Promise<MigrationFile | null> {\n const timestamp = identifier.split('_')[0];\n\n const migrationFiles = await this.getMigrationFiles();\n const migrationFile = migrationFiles.find((m) => m.timestamp === timestamp);\n return migrationFile ?? null;\n }\n\n /**\n * Loads the implementation for the migration with the given identifier. The identifier\n * must start with a 14-character timestamp. It may optionally be followed by\n * an underscore with additional characters, which are ignored. These should\n * typically be used to provide a human-readable name for the migration.\n */\n private async loadMigrationImplementation(migrationFile: MigrationFile) {\n // We use dynamic imports to handle both CJS and ESM modules.\n const migrationModulePath = path.join(migrationFile.directory, migrationFile.filename);\n const migrationModule = await import(migrationModulePath);\n\n const migrationImplementation = migrationModule.default as BatchedMigrationImplementation;\n validateBatchedMigrationImplementation(migrationImplementation);\n return migrationImplementation;\n }\n\n async enqueueBatchedMigration(identifier: string) {\n const migrationFile = await this.getMigrationForIdentifier(identifier);\n if (!migrationFile) {\n throw new Error(`No migration found for identifier ${identifier}`);\n }\n\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n const migrationParameters = await migrationImplementation.getParameters();\n\n // If `max` is null, that implies that there are no rows to process, so\n // we can immediately mark the migration as finished.\n const status: BatchedMigrationStatus =\n migrationParameters.max === null ? 'succeeded' : 'pending';\n\n const minValue = BigInt(migrationParameters.min ?? DEFAULT_MIN_VALUE);\n const maxValue = BigInt(migrationParameters.max ?? minValue);\n const batchSize = migrationParameters.batchSize ?? DEFAULT_BATCH_SIZE;\n\n await insertBatchedMigration({\n project: this.options.project,\n filename: migrationFile.filename,\n timestamp: migrationFile.timestamp,\n batch_size: batchSize,\n min_value: minValue,\n max_value: maxValue,\n status,\n });\n }\n\n async finalizeBatchedMigration(identifier: string, options?: BatchedMigrationFinalizeOptions) {\n const timestamp = identifier.split('_')[0];\n\n let migration = await selectBatchedMigrationForTimestamp(this.options.project, timestamp);\n\n if (migration.status === 'succeeded') return;\n\n // If the migration isn't already in the finalizing state, mark it as such.\n if (migration.status !== 'finalizing') {\n migration = await updateBatchedMigrationStatus(migration.id, 'finalizing');\n }\n\n await doWithLock(this.lockNameForTimestamp(timestamp), { autoRenew: true }, async () => {\n const migrationFile = await this.getMigrationForIdentifier(identifier);\n if (!migrationFile) {\n throw new Error(`No migration found for identifier ${identifier}`);\n }\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n\n const runner = new BatchedMigrationRunner(migration, migrationImplementation, {\n // Always log progress unless explicitly disabled.\n logProgress: options?.logProgress ?? true,\n });\n\n // Because we don't give any arguments to `run()`, it will run until it\n // has attempted every job.\n await runner.run();\n });\n\n migration = await selectBatchedMigrationForTimestamp(this.options.project, timestamp);\n\n if (migration.status === 'succeeded') return;\n\n throw new Error(\n `Expected batched migration with identifier ${identifier} to be marked as 'succeeded', but it is '${migration.status}'.`,\n );\n }\n\n start(options: BatchedMigrationStartOptions = {}) {\n if (this.running) {\n throw new Error('BatchedMigrationsRunner is already running');\n }\n\n this.loop(options);\n }\n\n async loop({ workDurationMs, sleepDurationMs }: BatchedMigrationStartOptions) {\n workDurationMs ??= DEFAULT_WORK_DURATION_MS;\n sleepDurationMs ??= DEFAULT_SLEEP_DURATION_MS;\n\n this.running = true;\n while (this.running) {\n if (this.abortController.signal.aborted) {\n // We assign this here so that `stop()` can tell when this loop is done\n // processing jobs.\n this.running = false;\n return;\n }\n\n let didWork = false;\n try {\n didWork = await this.maybePerformWork(workDurationMs);\n } catch (err) {\n this.emit('error', err);\n }\n\n // If we did work, we'll immediately try again since there's probably more\n // work to be done. If not, we'll sleep for a while - maybe some more work\n // will become available!\n if (!didWork) {\n // We provide the signal here so that we can more quickly stop things\n // when we're shutting down.\n try {\n await sleep(sleepDurationMs, null, { ref: false, signal: this.abortController.signal });\n } catch {\n // We don't care about errors here, they should only ever occur when\n // the AbortController is aborted. Continue to the next iteration of\n // the loop so we can shut down.\n continue;\n }\n }\n }\n }\n\n private async getOrStartMigration(): Promise<BatchedMigrationRow | null> {\n return doWithLock(\n this.lockName,\n {\n // Don't fail if the lock couldn't be acquired immediately.\n onNotAcquired: () => null,\n },\n async () => {\n let migration = await queryOptionalRow(\n sql.select_running_migration,\n { project: this.options.project },\n BatchedMigrationRowSchema,\n );\n\n if (!migration) {\n migration = await queryOptionalRow(\n sql.start_next_pending_migration,\n { project: this.options.project },\n BatchedMigrationRowSchema,\n );\n }\n\n return migration;\n },\n );\n }\n\n async maybePerformWork(durationMs: number): Promise<boolean> {\n const migration = await this.getOrStartMigration();\n if (!migration) {\n // No work to do. Handle this case.\n return false;\n }\n\n // This server may not yet know about the current running migration. If\n // that's the case, we'll just skip it for now.\n const migrationFile = await this.getMigrationForIdentifier(migration.timestamp);\n if (!migrationFile) {\n return false;\n }\n\n let didWork = false;\n await doWithLock(\n this.lockNameForTimestamp(migrationFile.timestamp),\n {\n autoRenew: true,\n // Do nothing if the lock could not immediately be acquired.\n onNotAcquired: () => null,\n },\n async () => {\n didWork = true;\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n\n try {\n await runner.run({ signal: this.abortController.signal, durationMs });\n } catch (err) {\n this.emit('error', err);\n }\n },\n );\n\n return didWork;\n }\n\n async stop() {\n this.abortController.abort();\n\n // Spin until we're no longer running.\n while (this.running) {\n await sleep(1000);\n }\n }\n}\n\nlet runner: BatchedMigrationsRunner | null = null;\n\nfunction assertRunner(\n runner: BatchedMigrationsRunner | null,\n): asserts runner is BatchedMigrationsRunner {\n if (!runner) throw new Error('Batched migrations not initialized');\n}\n\nexport function initBatchedMigrations(options: BatchedMigrationRunnerOptions) {\n if (runner) throw new Error('Batched migrations already initialized');\n runner = new BatchedMigrationsRunner(options);\n return runner;\n}\n\nexport function startBatchedMigrations(options: BatchedMigrationStartOptions = {}) {\n assertRunner(runner);\n runner.start(options);\n return runner;\n}\n\nexport async function stopBatchedMigrations() {\n assertRunner(runner);\n await runner.stop();\n runner = null;\n}\n\n/**\n * Given a batched migration identifier like `20230406184103_migration`,\n * enqueues it for execution by creating a row in the `batched_migrations`\n * table.\n *\n * Despite taking a full identifier, only the timestamp is used to uniquely\n * identify the batched migration. The remaining part is just used to make\n * calls more human-readable.\n *\n * @param identifier The identifier of the batched migration to enqueue.\n */\nexport async function enqueueBatchedMigration(identifier: string) {\n assertRunner(runner);\n await runner.enqueueBatchedMigration(identifier);\n}\n\n/**\n * Given a batched migration identifier like `20230406184103_migration`,\n * synchronously runs it to completion. An error will be thrown if the final\n * status of the migration is not `succeeded`.\n *\n * @param identifier The identifier of the batched migration to finalize.\n * @param options Options for finalizing the batched migration.\n */\nexport async function finalizeBatchedMigration(\n identifier: string,\n options?: BatchedMigrationFinalizeOptions,\n) {\n assertRunner(runner);\n await runner.finalizeBatchedMigration(identifier, options);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migrations-runner.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from 'node:events';\nimport path from 'node:path';\nimport { setTimeout as sleep } from 'node:timers/promises';\n\nimport { doWithLock } from '@prairielearn/named-locks';\nimport { loadSqlEquiv, queryOptionalRow } from '@prairielearn/postgres';\n\nimport {\n type MigrationFile,\n readAndValidateMigrationsFromDirectories,\n} from '../load-migrations.js';\n\nimport { BatchedMigrationRunner } from './batched-migration-runner.js';\nimport {\n type BatchedMigrationImplementation,\n type BatchedMigrationRow,\n BatchedMigrationRowSchema,\n type BatchedMigrationStatus,\n insertBatchedMigration,\n selectBatchedMigrationForTimestamp,\n updateBatchedMigrationStatus,\n validateBatchedMigrationImplementation,\n} from './batched-migration.js';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\nconst DEFAULT_MIN_VALUE = 1n;\nconst DEFAULT_BATCH_SIZE = 1_000;\nconst DEFAULT_WORK_DURATION_MS = 60_000;\nconst DEFAULT_SLEEP_DURATION_MS = 30_000;\nconst EXTENSIONS = ['.js', '.ts', '.mjs', '.mts'];\n\ninterface BatchedMigrationRunnerOptions {\n project: string;\n directories: string[];\n}\n\ninterface BatchedMigrationStartOptions {\n workDurationMs?: number;\n sleepDurationMs?: number;\n}\n\ninterface BatchedMigrationFinalizeOptions {\n logProgress?: boolean;\n}\n\nexport class BatchedMigrationsRunner extends EventEmitter {\n private readonly options: BatchedMigrationRunnerOptions;\n private readonly lockName: string;\n private running = false;\n private migrationFiles: MigrationFile[] | null = null;\n private abortController = new AbortController();\n\n constructor(options: BatchedMigrationRunnerOptions) {\n super();\n this.options = options;\n this.lockName = `batched-migrations:${this.options.project}`;\n }\n\n private lockNameForTimestamp(timestamp: string) {\n return `${this.lockName}:${timestamp}`;\n }\n\n private getMigrationFiles = async () => {\n if (!this.migrationFiles) {\n this.migrationFiles = await readAndValidateMigrationsFromDirectories(\n this.options.directories,\n EXTENSIONS,\n );\n }\n return this.migrationFiles;\n };\n\n private async getMigrationForIdentifier(identifier: string): Promise<MigrationFile | null> {\n const timestamp = identifier.split('_')[0];\n\n const migrationFiles = await this.getMigrationFiles();\n const migrationFile = migrationFiles.find((m) => m.timestamp === timestamp);\n return migrationFile ?? null;\n }\n\n /**\n * Loads the implementation for the migration with the given identifier. The identifier\n * must start with a 14-character timestamp. It may optionally be followed by\n * an underscore with additional characters, which are ignored. These should\n * typically be used to provide a human-readable name for the migration.\n */\n private async loadMigrationImplementation(migrationFile: MigrationFile) {\n // We use dynamic imports to handle both CJS and ESM modules.\n const migrationModulePath = path.join(migrationFile.directory, migrationFile.filename);\n const migrationModule = await import(migrationModulePath);\n\n const migrationImplementation = migrationModule.default as BatchedMigrationImplementation;\n validateBatchedMigrationImplementation(migrationImplementation);\n return migrationImplementation;\n }\n\n async enqueueBatchedMigration(identifier: string) {\n const migrationFile = await this.getMigrationForIdentifier(identifier);\n if (!migrationFile) {\n throw new Error(`No migration found for identifier ${identifier}`);\n }\n\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n const migrationParameters = await migrationImplementation.getParameters();\n\n // If `max` is null, that implies that there are no rows to process, so\n // we can immediately mark the migration as finished.\n const status: BatchedMigrationStatus =\n migrationParameters.max === null ? 'succeeded' : 'pending';\n\n const minValue = BigInt(migrationParameters.min ?? DEFAULT_MIN_VALUE);\n const maxValue = BigInt(migrationParameters.max ?? minValue);\n const batchSize = migrationParameters.batchSize ?? DEFAULT_BATCH_SIZE;\n\n await insertBatchedMigration({\n project: this.options.project,\n filename: migrationFile.filename,\n timestamp: migrationFile.timestamp,\n batch_size: batchSize,\n min_value: minValue,\n max_value: maxValue,\n status,\n });\n }\n\n async finalizeBatchedMigration(identifier: string, options?: BatchedMigrationFinalizeOptions) {\n const timestamp = identifier.split('_')[0];\n\n let migration = await selectBatchedMigrationForTimestamp(this.options.project, timestamp);\n\n if (migration.status === 'succeeded') return;\n\n // If the migration isn't already in the finalizing state, mark it as such.\n if (migration.status !== 'finalizing') {\n migration = await updateBatchedMigrationStatus(migration.id, 'finalizing');\n }\n\n await doWithLock(this.lockNameForTimestamp(timestamp), { autoRenew: true }, async () => {\n const migrationFile = await this.getMigrationForIdentifier(identifier);\n if (!migrationFile) {\n throw new Error(`No migration found for identifier ${identifier}`);\n }\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n\n const runner = new BatchedMigrationRunner(migration, migrationImplementation, {\n // Always log progress unless explicitly disabled.\n logProgress: options?.logProgress ?? true,\n });\n\n // Because we don't give any arguments to `run()`, it will run until it\n // has attempted every job.\n await runner.run();\n });\n\n migration = await selectBatchedMigrationForTimestamp(this.options.project, timestamp);\n\n if (migration.status === 'succeeded') return;\n\n throw new Error(\n `Expected batched migration with identifier ${identifier} to be marked as 'succeeded', but it is '${migration.status}'.`,\n );\n }\n\n start(options: BatchedMigrationStartOptions = {}) {\n if (this.running) {\n throw new Error('BatchedMigrationsRunner is already running');\n }\n\n this.loop(options);\n }\n\n async loop({ workDurationMs, sleepDurationMs }: BatchedMigrationStartOptions) {\n workDurationMs ??= DEFAULT_WORK_DURATION_MS;\n sleepDurationMs ??= DEFAULT_SLEEP_DURATION_MS;\n\n this.running = true;\n while (this.running) {\n if (this.abortController.signal.aborted) {\n // We assign this here so that `stop()` can tell when this loop is done\n // processing jobs.\n this.running = false;\n return;\n }\n\n let didWork = false;\n try {\n didWork = await this.maybePerformWork(workDurationMs);\n } catch (err) {\n this.emit('error', err);\n }\n\n // If we did work, we'll immediately try again since there's probably more\n // work to be done. If not, we'll sleep for a while - maybe some more work\n // will become available!\n if (!didWork) {\n // We provide the signal here so that we can more quickly stop things\n // when we're shutting down.\n try {\n await sleep(sleepDurationMs, null, { ref: false, signal: this.abortController.signal });\n } catch {\n // We don't care about errors here, they should only ever occur when\n // the AbortController is aborted. Continue to the next iteration of\n // the loop so we can shut down.\n continue;\n }\n }\n }\n }\n\n private async getOrStartMigration(): Promise<BatchedMigrationRow | null> {\n return doWithLock(\n this.lockName,\n {\n // Don't fail if the lock couldn't be acquired immediately.\n onNotAcquired: () => null,\n },\n async () => {\n let migration = await queryOptionalRow(\n sql.select_running_migration,\n { project: this.options.project },\n BatchedMigrationRowSchema,\n );\n\n if (!migration) {\n migration = await queryOptionalRow(\n sql.start_next_pending_migration,\n { project: this.options.project },\n BatchedMigrationRowSchema,\n );\n }\n\n return migration;\n },\n );\n }\n\n async maybePerformWork(durationMs: number): Promise<boolean> {\n const migration = await this.getOrStartMigration();\n if (!migration) {\n // No work to do. Handle this case.\n return false;\n }\n\n // This server may not yet know about the current running migration. If\n // that's the case, we'll just skip it for now.\n const migrationFile = await this.getMigrationForIdentifier(migration.timestamp);\n if (!migrationFile) {\n return false;\n }\n\n let didWork = false;\n await doWithLock(\n this.lockNameForTimestamp(migrationFile.timestamp),\n {\n autoRenew: true,\n // Do nothing if the lock could not immediately be acquired.\n onNotAcquired: () => null,\n },\n async () => {\n didWork = true;\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n\n try {\n await runner.run({ signal: this.abortController.signal, durationMs });\n } catch (err) {\n this.emit('error', err);\n }\n },\n );\n\n return didWork;\n }\n\n async stop() {\n this.abortController.abort();\n\n // Spin until we're no longer running.\n while (this.running) {\n await sleep(1000);\n }\n }\n}\n\nlet runner: BatchedMigrationsRunner | null = null;\n\nfunction assertRunner(\n runner: BatchedMigrationsRunner | null,\n): asserts runner is BatchedMigrationsRunner {\n if (!runner) throw new Error('Batched migrations not initialized');\n}\n\nexport function initBatchedMigrations(options: BatchedMigrationRunnerOptions) {\n if (runner) throw new Error('Batched migrations already initialized');\n runner = new BatchedMigrationsRunner(options);\n return runner;\n}\n\nexport function startBatchedMigrations(options: BatchedMigrationStartOptions = {}) {\n assertRunner(runner);\n runner.start(options);\n return runner;\n}\n\nexport async function stopBatchedMigrations() {\n assertRunner(runner);\n await runner.stop();\n runner = null;\n}\n\n/**\n * Given a batched migration identifier like `20230406184103_migration`,\n * enqueues it for execution by creating a row in the `batched_migrations`\n * table.\n *\n * Despite taking a full identifier, only the timestamp is used to uniquely\n * identify the batched migration. The remaining part is just used to make\n * calls more human-readable.\n *\n * @param identifier The identifier of the batched migration to enqueue.\n */\nexport async function enqueueBatchedMigration(identifier: string) {\n assertRunner(runner);\n await runner.enqueueBatchedMigration(identifier);\n}\n\n/**\n * Given a batched migration identifier like `20230406184103_migration`,\n * synchronously runs it to completion. An error will be thrown if the final\n * status of the migration is not `succeeded`.\n *\n * @param identifier The identifier of the batched migration to finalize.\n * @param options Options for finalizing the batched migration.\n */\nexport async function finalizeBatchedMigration(\n identifier: string,\n options?: BatchedMigrationFinalizeOptions,\n) {\n assertRunner(runner);\n await runner.finalizeBatchedMigration(identifier, options);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"batched-migrations-runner.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from 'node:events';\nimport path from 'node:path';\nimport { setTimeout as sleep } from 'node:timers/promises';\n\nimport { doWithLock } from '@prairielearn/named-locks';\nimport { loadSqlEquiv, queryOptionalRow } from '@prairielearn/postgres';\n\nimport {\n type MigrationFile,\n readAndValidateMigrationsFromDirectories,\n} from '../load-migrations.js';\n\nimport { BatchedMigrationRunner } from './batched-migration-runner.js';\nimport {\n type BatchedMigrationImplementation,\n type BatchedMigrationRow,\n BatchedMigrationRowSchema,\n type BatchedMigrationStatus,\n insertBatchedMigration,\n selectBatchedMigrationForTimestamp,\n updateBatchedMigrationStatus,\n validateBatchedMigrationImplementation,\n} from './batched-migration.js';\n\nconst sql = loadSqlEquiv(import.meta.filename);\n\nconst DEFAULT_MIN_VALUE = 1n;\nconst DEFAULT_BATCH_SIZE = 1_000;\nconst DEFAULT_WORK_DURATION_MS = 60_000;\nconst DEFAULT_SLEEP_DURATION_MS = 30_000;\nconst EXTENSIONS = ['.js', '.ts', '.mjs', '.mts'];\n\ninterface BatchedMigrationRunnerOptions {\n project: string;\n directories: string[];\n}\n\ninterface BatchedMigrationStartOptions {\n workDurationMs?: number;\n sleepDurationMs?: number;\n}\n\ninterface BatchedMigrationFinalizeOptions {\n logProgress?: boolean;\n}\n\nexport class BatchedMigrationsRunner extends EventEmitter {\n private readonly options: BatchedMigrationRunnerOptions;\n private readonly lockName: string;\n private running = false;\n private migrationFiles: MigrationFile[] | null = null;\n private abortController = new AbortController();\n\n constructor(options: BatchedMigrationRunnerOptions) {\n super();\n this.options = options;\n this.lockName = `batched-migrations:${this.options.project}`;\n }\n\n private lockNameForTimestamp(timestamp: string) {\n return `${this.lockName}:${timestamp}`;\n }\n\n private getMigrationFiles = async () => {\n if (!this.migrationFiles) {\n this.migrationFiles = await readAndValidateMigrationsFromDirectories(\n this.options.directories,\n EXTENSIONS,\n );\n }\n return this.migrationFiles;\n };\n\n private async getMigrationForIdentifier(identifier: string): Promise<MigrationFile | null> {\n const timestamp = identifier.split('_')[0];\n\n const migrationFiles = await this.getMigrationFiles();\n const migrationFile = migrationFiles.find((m) => m.timestamp === timestamp);\n return migrationFile ?? null;\n }\n\n /**\n * Loads the implementation for the migration with the given identifier. The identifier\n * must start with a 14-character timestamp. It may optionally be followed by\n * an underscore with additional characters, which are ignored. These should\n * typically be used to provide a human-readable name for the migration.\n */\n private async loadMigrationImplementation(migrationFile: MigrationFile) {\n // We use dynamic imports to handle both CJS and ESM modules.\n const migrationModulePath = path.join(migrationFile.directory, migrationFile.filename);\n const migrationModule = await import(migrationModulePath);\n\n const migrationImplementation = migrationModule.default as BatchedMigrationImplementation;\n validateBatchedMigrationImplementation(migrationImplementation);\n return migrationImplementation;\n }\n\n async enqueueBatchedMigration(identifier: string) {\n const migrationFile = await this.getMigrationForIdentifier(identifier);\n if (!migrationFile) {\n throw new Error(`No migration found for identifier ${identifier}`);\n }\n\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n const migrationParameters = await migrationImplementation.getParameters();\n\n // If `max` is null, that implies that there are no rows to process, so\n // we can immediately mark the migration as finished.\n const status: BatchedMigrationStatus =\n migrationParameters.max === null ? 'succeeded' : 'pending';\n\n const minValue = BigInt(migrationParameters.min ?? DEFAULT_MIN_VALUE);\n const maxValue = BigInt(migrationParameters.max ?? minValue);\n const batchSize = migrationParameters.batchSize ?? DEFAULT_BATCH_SIZE;\n\n await insertBatchedMigration({\n project: this.options.project,\n filename: migrationFile.filename,\n timestamp: migrationFile.timestamp,\n batch_size: batchSize,\n min_value: minValue,\n max_value: maxValue,\n status,\n });\n }\n\n async finalizeBatchedMigration(identifier: string, options?: BatchedMigrationFinalizeOptions) {\n const timestamp = identifier.split('_')[0];\n\n let migration = await selectBatchedMigrationForTimestamp(this.options.project, timestamp);\n\n if (migration.status === 'succeeded') return;\n\n // If the migration isn't already in the finalizing state, mark it as such.\n if (migration.status !== 'finalizing') {\n migration = await updateBatchedMigrationStatus(migration.id, 'finalizing');\n }\n\n await doWithLock(this.lockNameForTimestamp(timestamp), { autoRenew: true }, async () => {\n const migrationFile = await this.getMigrationForIdentifier(identifier);\n if (!migrationFile) {\n throw new Error(`No migration found for identifier ${identifier}`);\n }\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n\n const runner = new BatchedMigrationRunner(migration, migrationImplementation, {\n // Always log progress unless explicitly disabled.\n logProgress: options?.logProgress ?? true,\n });\n\n // Because we don't give any arguments to `run()`, it will run until it\n // has attempted every job.\n await runner.run();\n });\n\n migration = await selectBatchedMigrationForTimestamp(this.options.project, timestamp);\n\n if (migration.status === 'succeeded') return;\n\n throw new Error(\n `Expected batched migration with identifier ${identifier} to be marked as 'succeeded', but it is '${migration.status}'.`,\n );\n }\n\n start(options: BatchedMigrationStartOptions = {}) {\n if (this.running) {\n throw new Error('BatchedMigrationsRunner is already running');\n }\n\n this.loop(options);\n }\n\n async loop({ workDurationMs, sleepDurationMs }: BatchedMigrationStartOptions) {\n workDurationMs ??= DEFAULT_WORK_DURATION_MS;\n sleepDurationMs ??= DEFAULT_SLEEP_DURATION_MS;\n\n this.running = true;\n while (this.running) {\n if (this.abortController.signal.aborted) {\n // We assign this here so that `stop()` can tell when this loop is done\n // processing jobs.\n this.running = false;\n return;\n }\n\n let didWork = false;\n try {\n didWork = await this.maybePerformWork(workDurationMs);\n } catch (err) {\n this.emit('error', err);\n }\n\n // If we did work, we'll immediately try again since there's probably more\n // work to be done. If not, we'll sleep for a while - maybe some more work\n // will become available!\n if (!didWork) {\n // We provide the signal here so that we can more quickly stop things\n // when we're shutting down.\n try {\n await sleep(sleepDurationMs, null, { ref: false, signal: this.abortController.signal });\n } catch {\n // We don't care about errors here, they should only ever occur when\n // the AbortController is aborted. Continue to the next iteration of\n // the loop so we can shut down.\n continue;\n }\n }\n }\n }\n\n private async getOrStartMigration(): Promise<BatchedMigrationRow | null> {\n return doWithLock(\n this.lockName,\n {\n // Don't fail if the lock couldn't be acquired immediately.\n onNotAcquired: () => null,\n },\n async () => {\n let migration = await queryOptionalRow(\n sql.select_running_migration,\n { project: this.options.project },\n BatchedMigrationRowSchema,\n );\n\n if (!migration) {\n migration = await queryOptionalRow(\n sql.start_next_pending_migration,\n { project: this.options.project },\n BatchedMigrationRowSchema,\n );\n }\n\n return migration;\n },\n );\n }\n\n async maybePerformWork(durationMs: number): Promise<boolean> {\n const migration = await this.getOrStartMigration();\n if (!migration) {\n // No work to do. Handle this case.\n return false;\n }\n\n // This server may not yet know about the current running migration. If\n // that's the case, we'll just skip it for now.\n const migrationFile = await this.getMigrationForIdentifier(migration.timestamp);\n if (!migrationFile) {\n return false;\n }\n\n let didWork = false;\n await doWithLock(\n this.lockNameForTimestamp(migrationFile.timestamp),\n {\n autoRenew: true,\n // Do nothing if the lock could not immediately be acquired.\n onNotAcquired: () => null,\n },\n async () => {\n didWork = true;\n const migrationImplementation = await this.loadMigrationImplementation(migrationFile);\n\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n\n try {\n await runner.run({ signal: this.abortController.signal, durationMs });\n } catch (err) {\n this.emit('error', err);\n }\n },\n );\n\n return didWork;\n }\n\n async stop() {\n this.abortController.abort();\n\n // Spin until we're no longer running.\n while (this.running) {\n await sleep(1000);\n }\n }\n}\n\nlet runner: BatchedMigrationsRunner | null = null;\n\nfunction assertRunner(\n runner: BatchedMigrationsRunner | null,\n): asserts runner is BatchedMigrationsRunner {\n if (!runner) throw new Error('Batched migrations not initialized');\n}\n\nexport function initBatchedMigrations(options: BatchedMigrationRunnerOptions) {\n if (runner) throw new Error('Batched migrations already initialized');\n runner = new BatchedMigrationsRunner(options);\n return runner;\n}\n\nexport function startBatchedMigrations(options: BatchedMigrationStartOptions = {}) {\n assertRunner(runner);\n runner.start(options);\n return runner;\n}\n\nexport async function stopBatchedMigrations() {\n assertRunner(runner);\n await runner.stop();\n runner = null;\n}\n\n/**\n * Given a batched migration identifier like `20230406184103_migration`,\n * enqueues it for execution by creating a row in the `batched_migrations`\n * table.\n *\n * Despite taking a full identifier, only the timestamp is used to uniquely\n * identify the batched migration. The remaining part is just used to make\n * calls more human-readable.\n *\n * @param identifier The identifier of the batched migration to enqueue.\n */\nexport async function enqueueBatchedMigration(identifier: string) {\n assertRunner(runner);\n await runner.enqueueBatchedMigration(identifier);\n}\n\n/**\n * Given a batched migration identifier like `20230406184103_migration`,\n * synchronously runs it to completion. An error will be thrown if the final\n * status of the migration is not `succeeded`.\n *\n * @param identifier The identifier of the batched migration to finalize.\n * @param options Options for finalizing the batched migration.\n */\nexport async function finalizeBatchedMigration(\n identifier: string,\n options?: BatchedMigrationFinalizeOptions,\n) {\n assertRunner(runner);\n await runner.finalizeBatchedMigration(identifier, options);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migrations-runner.test.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"batched-migrations-runner.test.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.test.ts"],"names":[],"mappings":"","sourcesContent":["import path from 'node:path';\n\nimport { afterAll, afterEach, assert, beforeAll, describe, expect, it } from 'vitest';\n\nimport * as namedLocks from '@prairielearn/named-locks';\nimport { makePostgresTestUtils } from '@prairielearn/postgres';\n\nimport { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';\n\nimport { selectAllBatchedMigrations } from './batched-migration.js';\nimport { BatchedMigrationsRunner } from './batched-migrations-runner.js';\n\nconst postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n});\n\ndescribe('BatchedMigrationsRunner', () => {\n beforeAll(async () => {\n const poolConfig = await postgresTestUtils.createDatabase();\n await namedLocks.init(poolConfig, (err) => {\n throw err;\n });\n await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });\n });\n\n afterEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n afterAll(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('enqueues migrations', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184107_failing_migration');\n await runner.enqueueBatchedMigration('20230407230446_no_rows_migration');\n\n const migrations = await selectAllBatchedMigrations('test');\n\n assert.lengthOf(migrations, 3);\n assert.equal(migrations[0].timestamp, '20230406184103');\n assert.equal(migrations[0].filename, '20230406184103_successful_migration.ts');\n assert.equal(migrations[0].status, 'pending');\n assert.equal(migrations[1].timestamp, '20230406184107');\n assert.equal(migrations[1].filename, '20230406184107_failing_migration.ts');\n assert.equal(migrations[1].status, 'pending');\n assert.equal(migrations[2].timestamp, '20230407230446');\n assert.equal(migrations[2].filename, '20230407230446_no_rows_migration.ts');\n assert.equal(migrations[2].status, 'succeeded');\n });\n\n it('safely enqueues migrations multiple times', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n\n const migrations = await selectAllBatchedMigrations('test');\n\n assert.lengthOf(migrations, 1);\n });\n\n it('finalizes a successful migration', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.finalizeBatchedMigration('20230406184103_successful_migration', {\n logProgress: false,\n });\n\n const migrations = await selectAllBatchedMigrations('test');\n assert.lengthOf(migrations, 1);\n assert.equal(migrations[0].timestamp, '20230406184103');\n assert.equal(migrations[0].status, 'succeeded');\n });\n\n it('finalizes a failing migration', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184107_failing_migration');\n\n await expect(\n runner.finalizeBatchedMigration('20230406184107_failing_migration', {\n logProgress: false,\n }),\n ).rejects.toThrow(\"but it is 'failed'\");\n const migrations = await selectAllBatchedMigrations('test');\n assert.lengthOf(migrations, 1);\n assert.equal(migrations[0].timestamp, '20230406184107');\n assert.equal(migrations[0].status, 'failed');\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migrations-runner.test.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEtF,OAAO,KAAK,UAAU,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAEzE,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;IAC9C,QAAQ,EAAE,yBAAyB;CACpC,CAAC,CAAC;AAEH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;
|
|
1
|
+
{"version":3,"file":"batched-migrations-runner.test.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.test.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\n\nimport { afterAll, afterEach, assert, beforeAll, describe, expect, it } from 'vitest';\n\nimport * as namedLocks from '@prairielearn/named-locks';\nimport { makePostgresTestUtils } from '@prairielearn/postgres';\n\nimport { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';\n\nimport { selectAllBatchedMigrations } from './batched-migration.js';\nimport { BatchedMigrationsRunner } from './batched-migrations-runner.js';\n\nconst postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n});\n\ndescribe('BatchedMigrationsRunner', () => {\n beforeAll(async () => {\n const poolConfig = await postgresTestUtils.createDatabase();\n await namedLocks.init(poolConfig, (err) => {\n throw err;\n });\n await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });\n });\n\n afterEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n afterAll(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('enqueues migrations', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184107_failing_migration');\n await runner.enqueueBatchedMigration('20230407230446_no_rows_migration');\n\n const migrations = await selectAllBatchedMigrations('test');\n\n assert.lengthOf(migrations, 3);\n assert.equal(migrations[0].timestamp, '20230406184103');\n assert.equal(migrations[0].filename, '20230406184103_successful_migration.ts');\n assert.equal(migrations[0].status, 'pending');\n assert.equal(migrations[1].timestamp, '20230406184107');\n assert.equal(migrations[1].filename, '20230406184107_failing_migration.ts');\n assert.equal(migrations[1].status, 'pending');\n assert.equal(migrations[2].timestamp, '20230407230446');\n assert.equal(migrations[2].filename, '20230407230446_no_rows_migration.ts');\n assert.equal(migrations[2].status, 'succeeded');\n });\n\n it('safely enqueues migrations multiple times', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n\n const migrations = await selectAllBatchedMigrations('test');\n\n assert.lengthOf(migrations, 1);\n });\n\n it('finalizes a successful migration', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.finalizeBatchedMigration('20230406184103_successful_migration', {\n logProgress: false,\n });\n\n const migrations = await selectAllBatchedMigrations('test');\n assert.lengthOf(migrations, 1);\n assert.equal(migrations[0].timestamp, '20230406184103');\n assert.equal(migrations[0].status, 'succeeded');\n });\n\n it('finalizes a failing migration', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184107_failing_migration');\n\n await expect(\n runner.finalizeBatchedMigration('20230406184107_failing_migration', {\n logProgress: false,\n }),\n ).rejects.toThrow(\"but it is 'failed'\");\n const migrations = await selectAllBatchedMigrations('test');\n assert.lengthOf(migrations, 1);\n assert.equal(migrations[0].timestamp, '20230406184107');\n assert.equal(migrations[0].status, 'failed');\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"20230406184103_successful_migration.d.ts","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230406184103_successful_migration.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"20230406184103_successful_migration.d.ts","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230406184103_successful_migration.ts"],"names":[],"mappings":";;;;;;;;AAEA,wBAUG","sourcesContent":["import { makeBatchedMigration } from '../batched-migration.js';\n\nexport default makeBatchedMigration({\n async getParameters() {\n return {\n min: 1n,\n max: 100n,\n batchSize: 10,\n };\n },\n\n async execute(_min: bigint, _max: bigint) {},\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"20230406184103_successful_migration.js","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230406184103_successful_migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,eAAe,oBAAoB,CAAC;IAClC,KAAK,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"20230406184103_successful_migration.js","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230406184103_successful_migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,eAAe,oBAAoB,CAAC;IAClC,KAAK,CAAC,aAAa,GAAG;QACpB,OAAO;YACL,GAAG,EAAE,EAAE;YACP,GAAG,EAAE,IAAI;YACT,SAAS,EAAE,EAAE;SACd,CAAC;IAAA,CACH;IAED,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAAY,EAAE,EAAC,CAAC;CAC7C,CAAC,CAAC","sourcesContent":["import { makeBatchedMigration } from '../batched-migration.js';\n\nexport default makeBatchedMigration({\n async getParameters() {\n return {\n min: 1n,\n max: 100n,\n batchSize: 10,\n };\n },\n\n async execute(_min: bigint, _max: bigint) {},\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"20230406184107_failing_migration.d.ts","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230406184107_failing_migration.ts"],"names":[],"mappings":";;;;;;;;AAEA,wBAYG"}
|
|
1
|
+
{"version":3,"file":"20230406184107_failing_migration.d.ts","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230406184107_failing_migration.ts"],"names":[],"mappings":";;;;;;;;AAEA,wBAYG","sourcesContent":["import { makeBatchedMigration } from '../batched-migration.js';\n\nexport default makeBatchedMigration({\n async getParameters() {\n return {\n min: 2n,\n max: 200n,\n batchSize: 20,\n };\n },\n\n async execute(_min, _max) {\n throw new Error('Testing failure');\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"20230406184107_failing_migration.js","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230406184107_failing_migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,eAAe,oBAAoB,CAAC;IAClC,KAAK,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"20230406184107_failing_migration.js","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230406184107_failing_migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,eAAe,oBAAoB,CAAC;IAClC,KAAK,CAAC,aAAa,GAAG;QACpB,OAAO;YACL,GAAG,EAAE,EAAE;YACP,GAAG,EAAE,IAAI;YACT,SAAS,EAAE,EAAE;SACd,CAAC;IAAA,CACH;IAED,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE;QACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAAA,CACpC;CACF,CAAC,CAAC","sourcesContent":["import { makeBatchedMigration } from '../batched-migration.js';\n\nexport default makeBatchedMigration({\n async getParameters() {\n return {\n min: 2n,\n max: 200n,\n batchSize: 20,\n };\n },\n\n async execute(_min, _max) {\n throw new Error('Testing failure');\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"20230407230446_no_rows_migration.d.ts","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"20230407230446_no_rows_migration.d.ts","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts"],"names":[],"mappings":";;;;;;;AAEA,wBAYG","sourcesContent":["import { makeBatchedMigration } from '../batched-migration.js';\n\nexport default makeBatchedMigration({\n async getParameters() {\n return {\n // Simulates the case where there are no rows to process. A null\n // max value is what we would get for some query like\n // `SELECT MAX(id) FROM table;`.\n max: null,\n batchSize: 10,\n };\n },\n\n async execute(_min: bigint, _max: bigint) {},\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"20230407230446_no_rows_migration.js","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,eAAe,oBAAoB,CAAC;IAClC,KAAK,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"20230407230446_no_rows_migration.js","sourceRoot":"","sources":["../../../src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,eAAe,oBAAoB,CAAC;IAClC,KAAK,CAAC,aAAa,GAAG;QACpB,OAAO;YACL,gEAAgE;YAChE,qDAAqD;YACrD,gCAAgC;YAChC,GAAG,EAAE,IAAI;YACT,SAAS,EAAE,EAAE;SACd,CAAC;IAAA,CACH;IAED,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAAY,EAAE,EAAC,CAAC;CAC7C,CAAC,CAAC","sourcesContent":["import { makeBatchedMigration } from '../batched-migration.js';\n\nexport default makeBatchedMigration({\n async getParameters() {\n return {\n // Simulates the case where there are no rows to process. A null\n // max value is what we would get for some query like\n // `SELECT MAX(id) FROM table;`.\n max: null,\n batchSize: 10,\n };\n },\n\n async execute(_min: bigint, _max: bigint) {},\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,sBAAsB,EACtB,kCAAkC,EAClC,+BAA+B,GAChC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,0BAA0B,GAC3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,gCAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,sBAAsB,EACtB,kCAAkC,EAClC,+BAA+B,GAChC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,0BAA0B,GAC3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,gCAAgC,CAAC","sourcesContent":["export {\n type BatchedMigrationRow,\n type BatchedMigrationStatus,\n makeBatchedMigration,\n selectAllBatchedMigrations,\n selectBatchedMigration,\n selectBatchedMigrationForTimestamp,\n retryFailedBatchedMigrationJobs,\n} from './batched-migration.js';\nexport {\n type BatchedMigrationJobRow,\n type BatchedMigrationJobStatus,\n selectRecentJobsWithStatus,\n} from './batched-migration-job.js';\nexport {\n initBatchedMigrations,\n startBatchedMigrations,\n stopBatchedMigrations,\n enqueueBatchedMigration,\n finalizeBatchedMigration,\n} from './batched-migrations-runner.js';\n"]}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,kCAAkC,EAClC,0BAA0B,EAC1B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACpE,eAAO,MAAM,sBAAsB,QAA+D,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,kCAAkC,EAClC,0BAA0B,EAC1B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACpE,eAAO,MAAM,sBAAsB,QAA+D,CAAC","sourcesContent":["import path from 'path';\n\nexport { init } from './migrations/index.js';\n\nexport {\n type BatchedMigrationRow,\n type BatchedMigrationStatus,\n type BatchedMigrationJobRow,\n type BatchedMigrationJobStatus,\n makeBatchedMigration,\n initBatchedMigrations,\n startBatchedMigrations,\n stopBatchedMigrations,\n enqueueBatchedMigration,\n finalizeBatchedMigration,\n selectAllBatchedMigrations,\n selectBatchedMigration,\n selectBatchedMigrationForTimestamp,\n selectRecentJobsWithStatus,\n retryFailedBatchedMigrationJobs,\n} from './batched-migrations/index.js';\n\nexport { extractTimestampFromFilename } from './load-migrations.js';\nexport const SCHEMA_MIGRATIONS_PATH = path.resolve(import.meta.dirname, '..', 'schema-migrations');\n"]}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EAKL,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,kCAAkC,EAClC,0BAA0B,EAC1B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EAKL,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,kCAAkC,EAClC,0BAA0B,EAC1B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,mBAAmB,CAAC,CAAC","sourcesContent":["import path from 'path';\n\nexport { init } from './migrations/index.js';\n\nexport {\n type BatchedMigrationRow,\n type BatchedMigrationStatus,\n type BatchedMigrationJobRow,\n type BatchedMigrationJobStatus,\n makeBatchedMigration,\n initBatchedMigrations,\n startBatchedMigrations,\n stopBatchedMigrations,\n enqueueBatchedMigration,\n finalizeBatchedMigration,\n selectAllBatchedMigrations,\n selectBatchedMigration,\n selectBatchedMigrationForTimestamp,\n selectRecentJobsWithStatus,\n retryFailedBatchedMigrationJobs,\n} from './batched-migrations/index.js';\n\nexport { extractTimestampFromFilename } from './load-migrations.js';\nexport const SCHEMA_MIGRATIONS_PATH = path.resolve(import.meta.dirname, '..', 'schema-migrations');\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-migrations.d.ts","sourceRoot":"","sources":["../src/load-migrations.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUrE;AAED,wBAAsB,sCAAsC,CAC1D,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,aAAa,EAAE,CAAC,CAgC1B;AAED,wBAAsB,wCAAwC,CAC5D,WAAW,EAAE,MAAM,EAAE,EACrB,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,aAAa,EAAE,CAAC,CAO1B;AAED,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAInF;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAe9D"}
|
|
1
|
+
{"version":3,"file":"load-migrations.d.ts","sourceRoot":"","sources":["../src/load-migrations.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUrE;AAED,wBAAsB,sCAAsC,CAC1D,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,aAAa,EAAE,CAAC,CAgC1B;AAED,wBAAsB,wCAAwC,CAC5D,WAAW,EAAE,MAAM,EAAE,EACrB,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,aAAa,EAAE,CAAC,CAO1B;AAED,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAInF;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAe9D","sourcesContent":["import fs from 'fs-extra';\n\n/**\n * Timestamp prefixes will be of the form `YYYYMMDDHHMMSS`, which will have 14 digits.\n * If this code is still around in the year 10000... good luck.\n */\nconst MIGRATION_FILENAME_REGEX = /^([0-9]{14})_.+/;\n\n/**\n * Annotations are expressed via the following:\n *\n * -- prairielearn:migrations NO TRANSACTION\n *\n * Currently, `NO TRANSACTION` is the only supported annotation. This will run\n * the migration without a transaction. This is useful for migrations that use\n * features that can't be run in transactions, such as `CREATE INDEX CONCURRENTLY`.\n */\nconst ANNOTATION_PREFIX = '-- prairielearn:migrations';\nconst ALLOWED_ANNOTATIONS = new Set(['NO TRANSACTION']);\n\nexport interface MigrationFile {\n directory: string;\n filename: string;\n timestamp: string;\n}\n\nexport function extractTimestampFromFilename(filename: string): string {\n const match = filename.match(MIGRATION_FILENAME_REGEX);\n if (!match) {\n throw new Error(`Invalid migration filename: ${filename}`);\n }\n const timestamp = match.at(1) ?? null;\n if (timestamp === null) {\n throw new Error(`Migration ${filename} does not have a timestamp`);\n }\n return timestamp;\n}\n\nexport async function readAndValidateMigrationsFromDirectory(\n dir: string,\n extensions: string[],\n): Promise<MigrationFile[]> {\n const migrationFiles = (await fs.readdir(dir)).filter((m) => {\n // Get the full extension of the file (e.g. for `foo.test.ts`, return `.test.ts`).\n const [_name, ...extensionParts] = m.split('.');\n return extensions.includes('.' + extensionParts.join('.'));\n });\n\n const migrations = migrationFiles.map((mf) => {\n const timestamp = extractTimestampFromFilename(mf);\n\n return {\n directory: dir,\n filename: mf,\n timestamp,\n };\n });\n\n // First pass: validate that all migrations have a unique timestamp prefix.\n // This will avoid data loss and conflicts in unexpected scenarios.\n const seenTimestamps = new Set();\n for (const migration of migrations) {\n const { filename, timestamp } = migration;\n\n if (timestamp !== null) {\n if (seenTimestamps.has(timestamp)) {\n throw new Error(`Duplicate migration timestamp: ${timestamp} (${filename})`);\n }\n seenTimestamps.add(timestamp);\n }\n }\n\n return migrations;\n}\n\nexport async function readAndValidateMigrationsFromDirectories(\n directories: string[],\n extensions: string[],\n): Promise<MigrationFile[]> {\n const allMigrations: MigrationFile[] = [];\n for (const directory of directories) {\n const migrations = await readAndValidateMigrationsFromDirectory(directory, extensions);\n allMigrations.push(...migrations);\n }\n return allMigrations;\n}\n\nexport function sortMigrationFiles(migrationFiles: MigrationFile[]): MigrationFile[] {\n return migrationFiles.sort((a, b) => {\n return a.timestamp.localeCompare(b.timestamp);\n });\n}\n\nexport function parseAnnotations(contents: string): Set<string> {\n const lines = contents.split('\\n');\n const annotations = new Set<string>();\n\n lines.forEach((line) => {\n if (line.startsWith(ANNOTATION_PREFIX)) {\n const annotation = line.slice(ANNOTATION_PREFIX.length).trim();\n if (!ALLOWED_ANNOTATIONS.has(annotation)) {\n throw new Error(`Invalid annotation: ${annotation}`);\n }\n annotations.add(annotation);\n }\n });\n\n return annotations;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-migrations.js","sourceRoot":"","sources":["../src/load-migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAE1B;;;GAGG;AACH,MAAM,wBAAwB,GAAG,iBAAiB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,iBAAiB,GAAG,4BAA4B,CAAC;AACvD,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC;AAQxD,MAAM,UAAU,4BAA4B,CAAC,QAAgB;
|
|
1
|
+
{"version":3,"file":"load-migrations.js","sourceRoot":"","sources":["../src/load-migrations.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs-extra';\n\n/**\n * Timestamp prefixes will be of the form `YYYYMMDDHHMMSS`, which will have 14 digits.\n * If this code is still around in the year 10000... good luck.\n */\nconst MIGRATION_FILENAME_REGEX = /^([0-9]{14})_.+/;\n\n/**\n * Annotations are expressed via the following:\n *\n * -- prairielearn:migrations NO TRANSACTION\n *\n * Currently, `NO TRANSACTION` is the only supported annotation. This will run\n * the migration without a transaction. This is useful for migrations that use\n * features that can't be run in transactions, such as `CREATE INDEX CONCURRENTLY`.\n */\nconst ANNOTATION_PREFIX = '-- prairielearn:migrations';\nconst ALLOWED_ANNOTATIONS = new Set(['NO TRANSACTION']);\n\nexport interface MigrationFile {\n directory: string;\n filename: string;\n timestamp: string;\n}\n\nexport function extractTimestampFromFilename(filename: string): string {\n const match = filename.match(MIGRATION_FILENAME_REGEX);\n if (!match) {\n throw new Error(`Invalid migration filename: ${filename}`);\n }\n const timestamp = match.at(1) ?? null;\n if (timestamp === null) {\n throw new Error(`Migration ${filename} does not have a timestamp`);\n }\n return timestamp;\n}\n\nexport async function readAndValidateMigrationsFromDirectory(\n dir: string,\n extensions: string[],\n): Promise<MigrationFile[]> {\n const migrationFiles = (await fs.readdir(dir)).filter((m) => {\n // Get the full extension of the file (e.g. for `foo.test.ts`, return `.test.ts`).\n const [_name, ...extensionParts] = m.split('.');\n return extensions.includes('.' + extensionParts.join('.'));\n });\n\n const migrations = migrationFiles.map((mf) => {\n const timestamp = extractTimestampFromFilename(mf);\n\n return {\n directory: dir,\n filename: mf,\n timestamp,\n };\n });\n\n // First pass: validate that all migrations have a unique timestamp prefix.\n // This will avoid data loss and conflicts in unexpected scenarios.\n const seenTimestamps = new Set();\n for (const migration of migrations) {\n const { filename, timestamp } = migration;\n\n if (timestamp !== null) {\n if (seenTimestamps.has(timestamp)) {\n throw new Error(`Duplicate migration timestamp: ${timestamp} (${filename})`);\n }\n seenTimestamps.add(timestamp);\n }\n }\n\n return migrations;\n}\n\nexport async function readAndValidateMigrationsFromDirectories(\n directories: string[],\n extensions: string[],\n): Promise<MigrationFile[]> {\n const allMigrations: MigrationFile[] = [];\n for (const directory of directories) {\n const migrations = await readAndValidateMigrationsFromDirectory(directory, extensions);\n allMigrations.push(...migrations);\n }\n return allMigrations;\n}\n\nexport function sortMigrationFiles(migrationFiles: MigrationFile[]): MigrationFile[] {\n return migrationFiles.sort((a, b) => {\n return a.timestamp.localeCompare(b.timestamp);\n });\n}\n\nexport function parseAnnotations(contents: string): Set<string> {\n const lines = contents.split('\\n');\n const annotations = new Set<string>();\n\n lines.forEach((line) => {\n if (line.startsWith(ANNOTATION_PREFIX)) {\n const annotation = line.slice(ANNOTATION_PREFIX.length).trim();\n if (!ALLOWED_ANNOTATIONS.has(annotation)) {\n throw new Error(`Invalid annotation: ${annotation}`);\n }\n annotations.add(annotation);\n }\n });\n\n return annotations;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-migrations.test.d.ts","sourceRoot":"","sources":["../src/load-migrations.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"load-migrations.test.d.ts","sourceRoot":"","sources":["../src/load-migrations.test.ts"],"names":[],"mappings":"","sourcesContent":["import path from 'path';\n\nimport fs from 'fs-extra';\nimport tmp from 'tmp-promise';\nimport { assert, describe, expect, it } from 'vitest';\n\nimport {\n parseAnnotations,\n readAndValidateMigrationsFromDirectory,\n sortMigrationFiles,\n} from './load-migrations.js';\n\nasync function withMigrationFiles(files: string[], fn: (tmpDir: string) => Promise<void>) {\n await tmp.withDir(\n async function (tmpDir) {\n for (const file of files) {\n await fs.writeFile(path.join(tmpDir.path, file), '');\n }\n await fn(tmpDir.path);\n },\n { unsafeCleanup: true },\n );\n}\n\ndescribe('load-migrations', () => {\n describe('readAndValidateMigrationsFromDirectory', () => {\n it('handles migrations without a timestamp', async () => {\n await withMigrationFiles(['001_testing.sql'], async (tmpDir) => {\n const promise = readAndValidateMigrationsFromDirectory(tmpDir, ['.sql']);\n await expect(promise).rejects.toThrow('Invalid migration filename: 001_testing.sql');\n });\n });\n\n it('handles duplicate timestamps', async () => {\n await withMigrationFiles(\n ['20220101010101_testing.sql', '20220101010101_testing_again.sql'],\n async (tmpDir) => {\n const promise = readAndValidateMigrationsFromDirectory(tmpDir, ['.sql']);\n await expect(promise).rejects.toThrow('Duplicate migration timestamp');\n },\n );\n });\n });\n\n describe('sortMigrationFiles', () => {\n it('sorts by timestamp', () => {\n assert.deepEqual(\n sortMigrationFiles([\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n ]),\n [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ],\n );\n });\n });\n\n describe('parseAnnotations', () => {\n it('parses a NO TRANSACTION annotation', () => {\n const annotations = parseAnnotations('-- prairielearn:migrations NO TRANSACTION');\n assert.deepEqual(annotations, new Set(['NO TRANSACTION']));\n });\n\n it('throws an error for an invalid annotation', () => {\n assert.throws(() => {\n parseAnnotations('-- prairielearn:migrations INVALID');\n });\n });\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-migrations.test.js","sourceRoot":"","sources":["../src/load-migrations.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,GAAG,MAAM,aAAa,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEtD,OAAO,EACL,gBAAgB,EAChB,sCAAsC,EACtC,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAE9B,KAAK,UAAU,kBAAkB,CAAC,KAAe,EAAE,EAAqC;
|
|
1
|
+
{"version":3,"file":"load-migrations.test.js","sourceRoot":"","sources":["../src/load-migrations.test.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'path';\n\nimport fs from 'fs-extra';\nimport tmp from 'tmp-promise';\nimport { assert, describe, expect, it } from 'vitest';\n\nimport {\n parseAnnotations,\n readAndValidateMigrationsFromDirectory,\n sortMigrationFiles,\n} from './load-migrations.js';\n\nasync function withMigrationFiles(files: string[], fn: (tmpDir: string) => Promise<void>) {\n await tmp.withDir(\n async function (tmpDir) {\n for (const file of files) {\n await fs.writeFile(path.join(tmpDir.path, file), '');\n }\n await fn(tmpDir.path);\n },\n { unsafeCleanup: true },\n );\n}\n\ndescribe('load-migrations', () => {\n describe('readAndValidateMigrationsFromDirectory', () => {\n it('handles migrations without a timestamp', async () => {\n await withMigrationFiles(['001_testing.sql'], async (tmpDir) => {\n const promise = readAndValidateMigrationsFromDirectory(tmpDir, ['.sql']);\n await expect(promise).rejects.toThrow('Invalid migration filename: 001_testing.sql');\n });\n });\n\n it('handles duplicate timestamps', async () => {\n await withMigrationFiles(\n ['20220101010101_testing.sql', '20220101010101_testing_again.sql'],\n async (tmpDir) => {\n const promise = readAndValidateMigrationsFromDirectory(tmpDir, ['.sql']);\n await expect(promise).rejects.toThrow('Duplicate migration timestamp');\n },\n );\n });\n });\n\n describe('sortMigrationFiles', () => {\n it('sorts by timestamp', () => {\n assert.deepEqual(\n sortMigrationFiles([\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n ]),\n [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ],\n );\n });\n });\n\n describe('parseAnnotations', () => {\n it('parses a NO TRANSACTION annotation', () => {\n const annotations = parseAnnotations('-- prairielearn:migrations NO TRANSACTION');\n assert.deepEqual(annotations, new Set(['NO TRANSACTION']));\n });\n\n it('throws an error for an invalid annotation', () => {\n assert.throws(() => {\n parseAnnotations('-- prairielearn:migrations INVALID');\n });\n });\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"20230407210430_insert_user.d.ts","sourceRoot":"","sources":["../../../src/migrations/fixtures/20230407210430_insert_user.ts"],"names":[],"mappings":"AAEA,wBAA8B,OAAO,kBAEpC"}
|
|
1
|
+
{"version":3,"file":"20230407210430_insert_user.d.ts","sourceRoot":"","sources":["../../../src/migrations/fixtures/20230407210430_insert_user.ts"],"names":[],"mappings":"AAEA,wBAA8B,OAAO,kBAEpC","sourcesContent":["import { execute } from '@prairielearn/postgres';\n\nexport default async function migrate() {\n await execute(\"INSERT INTO users (name) VALUES ('Test User')\");\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"20230407210430_insert_user.js","sourceRoot":"","sources":["../../../src/migrations/fixtures/20230407210430_insert_user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,OAAO;
|
|
1
|
+
{"version":3,"file":"20230407210430_insert_user.js","sourceRoot":"","sources":["../../../src/migrations/fixtures/20230407210430_insert_user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,OAAO,GAAG;IACtC,MAAM,OAAO,CAAC,+CAA+C,CAAC,CAAC;AAAA,CAChE","sourcesContent":["import { execute } from '@prairielearn/postgres';\n\nexport default async function migrate() {\n await execute(\"INSERT INTO users (name) VALUES ('Test User')\");\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["export { init } from './migrations.js';\n"]}
|
|
@@ -17,7 +17,7 @@ export declare function init({ directories, project, migrationFilters }: InitOpt
|
|
|
17
17
|
* @param options.beforeTimestamp All migrations with timestamps before this timestamp will be excluded.
|
|
18
18
|
* @param options.inclusiveBefore Whether to include the migration with the timestamp equal to the beforeTimestamp.
|
|
19
19
|
*/
|
|
20
|
-
export declare function getMigrationsToExecute(migrationFiles: MigrationFile[], { excludeMigrations, beforeTimestamp, inclusiveBefore
|
|
20
|
+
export declare function getMigrationsToExecute(migrationFiles: MigrationFile[], { excludeMigrations, beforeTimestamp, inclusiveBefore }: {
|
|
21
21
|
excludeMigrations?: {
|
|
22
22
|
timestamp: string | null;
|
|
23
23
|
}[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/migrations/migrations.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,uBAAuB,CAAC;AAI/B,UAAU,WAAW;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE;QACjB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B,CAAC;CACH;AAED,wBAAsB,IAAI,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAqB,EAAE,EAAE,WAAW,iBAsBtF;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,cAAc,EAAE,aAAa,EAAE,EAC/B,EACE,iBAAsB,EACtB,eAAsB,EACtB,eAAuB,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/migrations/migrations.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,uBAAuB,CAAC;AAI/B,UAAU,WAAW;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE;QACjB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B,CAAC;CACH;AAED,wBAAsB,IAAI,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAqB,EAAE,EAAE,WAAW,iBAsBtF;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,cAAc,EAAE,aAAa,EAAE,EAC/B,EACE,iBAAsB,EACtB,eAAsB,EACtB,eAAuB,EACxB,EAAE;IACD,iBAAiB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC;IACnD,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GACA,aAAa,EAAE,CAgBjB;AAED,wBAAsB,YAAY,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,gBAAqB,EAAE,EAAE,WAAW,iBA0H9F","sourcesContent":["import path from 'path';\n\nimport fs from 'fs-extra';\n\nimport * as error from '@prairielearn/error';\nimport { logger } from '@prairielearn/logger';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport * as sqldb from '@prairielearn/postgres';\n\nimport {\n type MigrationFile,\n parseAnnotations,\n readAndValidateMigrationsFromDirectories,\n sortMigrationFiles,\n} from '../load-migrations.js';\n\nconst sql = sqldb.loadSqlEquiv(import.meta.filename);\n\ninterface InitOptions {\n directories: string[];\n project: string;\n migrationFilters?: {\n beforeTimestamp?: string | null;\n inclusiveBefore?: boolean;\n };\n}\n\nexport async function init({ directories, project, migrationFilters = {} }: InitOptions) {\n const migrationDirectories = Array.isArray(directories) ? directories : [directories];\n const lockName = 'migrations';\n logger.verbose(`Waiting for lock ${lockName}`);\n await namedLocks.doWithLock(\n lockName,\n {\n // Migrations *might* take a long time to run, so we'll enable automatic\n // lock renewal so that our lock doesn't get killed by the Postgres\n // idle session timeout.\n //\n // That said, we should generally try to keep migrations executing as\n // quickly as possible. A long-running migration likely means that\n // Postgres is locking a whole table, which is unacceptable in production.\n autoRenew: true,\n },\n async () => {\n logger.verbose(`Acquired lock ${lockName}`);\n await initWithLock({ directories: migrationDirectories, project, migrationFilters });\n },\n );\n logger.verbose(`Released lock ${lockName}`);\n}\n\n/**\n * Get the migrations to execute.\n *\n * @param migrationFiles The full list of migration files.\n * @param options The options for the migration execution.\n * @param options.excludeMigrations The list of migrations to exclude.\n * @param options.beforeTimestamp All migrations with timestamps before this timestamp will be excluded.\n * @param options.inclusiveBefore Whether to include the migration with the timestamp equal to the beforeTimestamp.\n */\nexport function getMigrationsToExecute(\n migrationFiles: MigrationFile[],\n {\n excludeMigrations = [],\n beforeTimestamp = null,\n inclusiveBefore = false,\n }: {\n excludeMigrations?: { timestamp: string | null }[];\n beforeTimestamp?: string | null;\n inclusiveBefore?: boolean;\n },\n): MigrationFile[] {\n // If no migrations have ever been run, run them all.\n if (excludeMigrations.length === 0 && beforeTimestamp === null) {\n return migrationFiles;\n }\n\n const excludedMigrationTimestamps = new Set(excludeMigrations.map((m) => m.timestamp));\n const remainingMigrationFiles = migrationFiles.filter(\n (m) => !excludedMigrationTimestamps.has(m.timestamp),\n );\n if (beforeTimestamp === null) {\n return remainingMigrationFiles;\n }\n return remainingMigrationFiles.filter((m) =>\n inclusiveBefore ? m.timestamp <= beforeTimestamp : m.timestamp < beforeTimestamp,\n );\n}\n\nexport async function initWithLock({ directories, project, migrationFilters = {} }: InitOptions) {\n const resolvedMigrationFilters = {\n beforeTimestamp: null,\n inclusiveBefore: false,\n ...migrationFilters,\n };\n\n logger.verbose('Starting DB schema migration');\n\n const oldSchema = sqldb.defaultPool.getSearchSchema();\n // Each postgres pool uses a unique schema every time the server starts up.\n // After that code runs, the default schema is set to that schema instead of public, which\n // causes the 'create_migrations_table' query to fail.\n //\n // We'll set the default schema to public before running the migrations, and then restore it\n // after the migrations are run.\n await sqldb.defaultPool.setSearchSchema('public');\n try {\n // Create the migrations table if needed\n await sqldb.execute(sql.create_migrations_table);\n\n // Apply necessary changes to the migrations table as needed.\n try {\n await sqldb.execute('SELECT project FROM migrations;');\n } catch (err: any) {\n if (err.routine === 'errorMissingColumn') {\n logger.info('Altering migrations table');\n await sqldb.execute(sql.add_projects_column);\n } else {\n throw err;\n }\n }\n try {\n await sqldb.execute('SELECT timestamp FROM migrations;');\n } catch (err: any) {\n if (err.routine === 'errorMissingColumn') {\n logger.info('Altering migrations table again');\n await sqldb.execute(sql.add_timestamp_column);\n } else {\n throw err;\n }\n }\n\n let allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });\n const migrationFiles = await readAndValidateMigrationsFromDirectories(directories, [\n '.sql',\n '.js',\n '.ts',\n '.mjs',\n ]);\n\n // Validation: if we not all previously-executed migrations have timestamps,\n // prompt the user to deploy an earlier version that includes both indexes\n // and timestamps.\n const migrationsMissingTimestamps = allMigrations.rows.filter((m) => !m.timestamp);\n if (migrationsMissingTimestamps.length > 0) {\n throw new Error(\n [\n 'The following migrations are missing timestamps:',\n migrationsMissingTimestamps.map((m) => ` ${m.filename}`),\n // This revision was the most recent commit to `master` before the\n // code handling indexes was removed.\n 'You must deploy revision 1aa43c7348fa24cf636413d720d06a2fa9e38ef2 first.',\n ].join('\\n'),\n );\n }\n\n // Refetch the list of migrations from the database.\n allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });\n\n // Sort the migration files into execution order.\n const sortedMigrationFiles = sortMigrationFiles(migrationFiles);\n\n // Figure out which migrations have to be applied.\n const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, {\n excludeMigrations: allMigrations.rows,\n ...resolvedMigrationFilters,\n });\n for (const { directory, filename, timestamp } of migrationsToExecute) {\n if (allMigrations.rows.length === 0) {\n // if we are running all the migrations then log at a lower level\n logger.verbose(`Running migration ${filename}`);\n } else {\n logger.info(`Running migration ${filename}`);\n }\n\n const migrationPath = path.join(directory, filename);\n if (filename.endsWith('.sql')) {\n const migrationSql = await fs.readFile(migrationPath, 'utf8');\n const annotations = parseAnnotations(migrationSql);\n try {\n if (annotations.has('NO TRANSACTION')) {\n await sqldb.execute(migrationSql);\n } else {\n await sqldb.runInTransactionAsync(async () => {\n await sqldb.execute(migrationSql);\n });\n }\n } catch (err) {\n error.addData(err, { sqlFile: filename });\n throw err;\n }\n } else {\n const migrationModule = await import(/* @vite-ignore */ migrationPath);\n const implementation = migrationModule.default;\n if (typeof implementation !== 'function') {\n throw new Error(`Migration ${filename} does not export a default function`);\n }\n await implementation();\n }\n\n // Record the migration.\n await sqldb.execute(sql.insert_migration, {\n filename,\n timestamp,\n project,\n });\n }\n } finally {\n // Restore the search schema\n await sqldb.defaultPool.setSearchSchema(oldSchema);\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/migrations/migrations.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'path';\n\nimport fs from 'fs-extra';\n\nimport * as error from '@prairielearn/error';\nimport { logger } from '@prairielearn/logger';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport * as sqldb from '@prairielearn/postgres';\n\nimport {\n type MigrationFile,\n parseAnnotations,\n readAndValidateMigrationsFromDirectories,\n sortMigrationFiles,\n} from '../load-migrations.js';\n\nconst sql = sqldb.loadSqlEquiv(import.meta.filename);\n\ninterface InitOptions {\n directories: string[];\n project: string;\n migrationFilters?: {\n beforeTimestamp?: string | null;\n inclusiveBefore?: boolean;\n };\n}\n\nexport async function init({ directories, project, migrationFilters = {} }: InitOptions) {\n const migrationDirectories = Array.isArray(directories) ? directories : [directories];\n const lockName = 'migrations';\n logger.verbose(`Waiting for lock ${lockName}`);\n await namedLocks.doWithLock(\n lockName,\n {\n // Migrations *might* take a long time to run, so we'll enable automatic\n // lock renewal so that our lock doesn't get killed by the Postgres\n // idle session timeout.\n //\n // That said, we should generally try to keep migrations executing as\n // quickly as possible. A long-running migration likely means that\n // Postgres is locking a whole table, which is unacceptable in production.\n autoRenew: true,\n },\n async () => {\n logger.verbose(`Acquired lock ${lockName}`);\n await initWithLock({ directories: migrationDirectories, project, migrationFilters });\n },\n );\n logger.verbose(`Released lock ${lockName}`);\n}\n\n/**\n * Get the migrations to execute.\n *\n * @param migrationFiles The full list of migration files.\n * @param options The options for the migration execution.\n * @param options.excludeMigrations The list of migrations to exclude.\n * @param options.beforeTimestamp All migrations with timestamps before this timestamp will be excluded.\n * @param options.inclusiveBefore Whether to include the migration with the timestamp equal to the beforeTimestamp.\n */\nexport function getMigrationsToExecute(\n migrationFiles: MigrationFile[],\n {\n excludeMigrations = [],\n beforeTimestamp = null,\n inclusiveBefore = false,\n }: {\n excludeMigrations?: { timestamp: string | null }[];\n beforeTimestamp?: string | null;\n inclusiveBefore?: boolean;\n },\n): MigrationFile[] {\n // If no migrations have ever been run, run them all.\n if (excludeMigrations.length === 0 && beforeTimestamp === null) {\n return migrationFiles;\n }\n\n const excludedMigrationTimestamps = new Set(excludeMigrations.map((m) => m.timestamp));\n const remainingMigrationFiles = migrationFiles.filter(\n (m) => !excludedMigrationTimestamps.has(m.timestamp),\n );\n if (beforeTimestamp === null) {\n return remainingMigrationFiles;\n }\n return remainingMigrationFiles.filter((m) =>\n inclusiveBefore ? m.timestamp <= beforeTimestamp : m.timestamp < beforeTimestamp,\n );\n}\n\nexport async function initWithLock({ directories, project, migrationFilters = {} }: InitOptions) {\n const resolvedMigrationFilters = {\n beforeTimestamp: null,\n inclusiveBefore: false,\n ...migrationFilters,\n };\n\n logger.verbose('Starting DB schema migration');\n\n const oldSchema = sqldb.defaultPool.getSearchSchema();\n // Each postgres pool uses a unique schema every time the server starts up.\n // After that code runs, the default schema is set to that schema instead of public, which\n // causes the 'create_migrations_table' query to fail.\n //\n // We'll set the default schema to public before running the migrations, and then restore it\n // after the migrations are run.\n await sqldb.defaultPool.setSearchSchema('public');\n try {\n // Create the migrations table if needed\n await sqldb.execute(sql.create_migrations_table);\n\n // Apply necessary changes to the migrations table as needed.\n try {\n await sqldb.execute('SELECT project FROM migrations;');\n } catch (err: any) {\n if (err.routine === 'errorMissingColumn') {\n logger.info('Altering migrations table');\n await sqldb.execute(sql.add_projects_column);\n } else {\n throw err;\n }\n }\n try {\n await sqldb.execute('SELECT timestamp FROM migrations;');\n } catch (err: any) {\n if (err.routine === 'errorMissingColumn') {\n logger.info('Altering migrations table again');\n await sqldb.execute(sql.add_timestamp_column);\n } else {\n throw err;\n }\n }\n\n let allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });\n const migrationFiles = await readAndValidateMigrationsFromDirectories(directories, [\n '.sql',\n '.js',\n '.ts',\n '.mjs',\n ]);\n\n // Validation: if we not all previously-executed migrations have timestamps,\n // prompt the user to deploy an earlier version that includes both indexes\n // and timestamps.\n const migrationsMissingTimestamps = allMigrations.rows.filter((m) => !m.timestamp);\n if (migrationsMissingTimestamps.length > 0) {\n throw new Error(\n [\n 'The following migrations are missing timestamps:',\n migrationsMissingTimestamps.map((m) => ` ${m.filename}`),\n // This revision was the most recent commit to `master` before the\n // code handling indexes was removed.\n 'You must deploy revision 1aa43c7348fa24cf636413d720d06a2fa9e38ef2 first.',\n ].join('\\n'),\n );\n }\n\n // Refetch the list of migrations from the database.\n allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });\n\n // Sort the migration files into execution order.\n const sortedMigrationFiles = sortMigrationFiles(migrationFiles);\n\n // Figure out which migrations have to be applied.\n const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, {\n excludeMigrations: allMigrations.rows,\n ...resolvedMigrationFilters,\n });\n for (const { directory, filename, timestamp } of migrationsToExecute) {\n if (allMigrations.rows.length === 0) {\n // if we are running all the migrations then log at a lower level\n logger.verbose(`Running migration ${filename}`);\n } else {\n logger.info(`Running migration ${filename}`);\n }\n\n const migrationPath = path.join(directory, filename);\n if (filename.endsWith('.sql')) {\n const migrationSql = await fs.readFile(migrationPath, 'utf8');\n const annotations = parseAnnotations(migrationSql);\n try {\n if (annotations.has('NO TRANSACTION')) {\n await sqldb.execute(migrationSql);\n } else {\n await sqldb.runInTransactionAsync(async () => {\n await sqldb.execute(migrationSql);\n });\n }\n } catch (err) {\n error.addData(err, { sqlFile: filename });\n throw err;\n }\n } else {\n const migrationModule = await import(/* @vite-ignore */ migrationPath);\n const implementation = migrationModule.default;\n if (typeof implementation !== 'function') {\n throw new Error(`Migration ${filename} does not export a default function`);\n }\n await implementation();\n }\n\n // Record the migration.\n await sqldb.execute(sql.insert_migration, {\n filename,\n timestamp,\n project,\n });\n }\n } finally {\n // Restore the search schema\n await sqldb.defaultPool.setSearchSchema(oldSchema);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/migrations/migrations.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'path';\n\nimport fs from 'fs-extra';\n\nimport * as error from '@prairielearn/error';\nimport { logger } from '@prairielearn/logger';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport * as sqldb from '@prairielearn/postgres';\n\nimport {\n type MigrationFile,\n parseAnnotations,\n readAndValidateMigrationsFromDirectories,\n sortMigrationFiles,\n} from '../load-migrations.js';\n\nconst sql = sqldb.loadSqlEquiv(import.meta.filename);\n\ninterface InitOptions {\n directories: string[];\n project: string;\n migrationFilters?: {\n beforeTimestamp?: string | null;\n inclusiveBefore?: boolean;\n };\n}\n\nexport async function init({ directories, project, migrationFilters = {} }: InitOptions) {\n const migrationDirectories = Array.isArray(directories) ? directories : [directories];\n const lockName = 'migrations';\n logger.verbose(`Waiting for lock ${lockName}`);\n await namedLocks.doWithLock(\n lockName,\n {\n // Migrations *might* take a long time to run, so we'll enable automatic\n // lock renewal so that our lock doesn't get killed by the Postgres\n // idle session timeout.\n //\n // That said, we should generally try to keep migrations executing as\n // quickly as possible. A long-running migration likely means that\n // Postgres is locking a whole table, which is unacceptable in production.\n autoRenew: true,\n },\n async () => {\n logger.verbose(`Acquired lock ${lockName}`);\n await initWithLock({ directories: migrationDirectories, project, migrationFilters });\n },\n );\n logger.verbose(`Released lock ${lockName}`);\n}\n\n/**\n * Get the migrations to execute.\n *\n * @param migrationFiles The full list of migration files.\n * @param options The options for the migration execution.\n * @param options.excludeMigrations The list of migrations to exclude.\n * @param options.beforeTimestamp All migrations with timestamps before this timestamp will be excluded.\n * @param options.inclusiveBefore Whether to include the migration with the timestamp equal to the beforeTimestamp.\n */\nexport function getMigrationsToExecute(\n migrationFiles: MigrationFile[],\n {\n excludeMigrations = [],\n beforeTimestamp = null,\n inclusiveBefore = false,\n }: {\n excludeMigrations?: { timestamp: string | null }[];\n beforeTimestamp?: string | null;\n inclusiveBefore?: boolean;\n },\n): MigrationFile[] {\n // If no migrations have ever been run, run them all.\n if (excludeMigrations.length === 0 && beforeTimestamp === null) {\n return migrationFiles;\n }\n\n const excludedMigrationTimestamps = new Set(excludeMigrations.map((m) => m.timestamp));\n const remainingMigrationFiles = migrationFiles.filter(\n (m) => !excludedMigrationTimestamps.has(m.timestamp),\n );\n if (beforeTimestamp === null) {\n return remainingMigrationFiles;\n }\n return remainingMigrationFiles.filter((m) =>\n inclusiveBefore ? m.timestamp <= beforeTimestamp : m.timestamp < beforeTimestamp,\n );\n}\n\nexport async function initWithLock({ directories, project, migrationFilters = {} }: InitOptions) {\n const resolvedMigrationFilters = {\n beforeTimestamp: null,\n inclusiveBefore: false,\n ...migrationFilters,\n };\n\n logger.verbose('Starting DB schema migration');\n\n const oldSchema = sqldb.defaultPool.getSearchSchema();\n // Each postgres pool uses a unique schema every time the server starts up.\n // After that code runs, the default schema is set to that schema instead of public, which\n // causes the 'create_migrations_table' query to fail.\n //\n // We'll set the default schema to public before running the migrations, and then restore it\n // after the migrations are run.\n await sqldb.defaultPool.setSearchSchema('public');\n try {\n // Create the migrations table if needed\n await sqldb.execute(sql.create_migrations_table);\n\n // Apply necessary changes to the migrations table as needed.\n try {\n await sqldb.execute('SELECT project FROM migrations;');\n } catch (err: any) {\n if (err.routine === 'errorMissingColumn') {\n logger.info('Altering migrations table');\n await sqldb.execute(sql.add_projects_column);\n } else {\n throw err;\n }\n }\n try {\n await sqldb.execute('SELECT timestamp FROM migrations;');\n } catch (err: any) {\n if (err.routine === 'errorMissingColumn') {\n logger.info('Altering migrations table again');\n await sqldb.execute(sql.add_timestamp_column);\n } else {\n throw err;\n }\n }\n\n let allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });\n const migrationFiles = await readAndValidateMigrationsFromDirectories(directories, [\n '.sql',\n '.js',\n '.ts',\n '.mjs',\n ]);\n\n // Validation: if we not all previously-executed migrations have timestamps,\n // prompt the user to deploy an earlier version that includes both indexes\n // and timestamps.\n const migrationsMissingTimestamps = allMigrations.rows.filter((m) => !m.timestamp);\n if (migrationsMissingTimestamps.length > 0) {\n throw new Error(\n [\n 'The following migrations are missing timestamps:',\n migrationsMissingTimestamps.map((m) => ` ${m.filename}`),\n // This revision was the most recent commit to `master` before the\n // code handling indexes was removed.\n 'You must deploy revision 1aa43c7348fa24cf636413d720d06a2fa9e38ef2 first.',\n ].join('\\n'),\n );\n }\n\n // Refetch the list of migrations from the database.\n allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });\n\n // Sort the migration files into execution order.\n const sortedMigrationFiles = sortMigrationFiles(migrationFiles);\n\n // Figure out which migrations have to be applied.\n const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, {\n excludeMigrations: allMigrations.rows,\n ...resolvedMigrationFilters,\n });\n for (const { directory, filename, timestamp } of migrationsToExecute) {\n if (allMigrations.rows.length === 0) {\n // if we are running all the migrations then log at a lower level\n logger.verbose(`Running migration ${filename}`);\n } else {\n logger.info(`Running migration ${filename}`);\n }\n\n const migrationPath = path.join(directory, filename);\n if (filename.endsWith('.sql')) {\n const migrationSql = await fs.readFile(migrationPath, 'utf8');\n const annotations = parseAnnotations(migrationSql);\n try {\n if (annotations.has('NO TRANSACTION')) {\n await sqldb.execute(migrationSql);\n } else {\n await sqldb.runInTransactionAsync(async () => {\n await sqldb.execute(migrationSql);\n });\n }\n } catch (err) {\n error.addData(err, { sqlFile: filename });\n throw err;\n }\n } else {\n const migrationModule = await import(/* @vite-ignore */ migrationPath);\n const implementation = migrationModule.default;\n if (typeof implementation !== 'function') {\n throw new Error(`Migration ${filename} does not export a default function`);\n }\n await implementation();\n }\n\n // Record the migration.\n await sqldb.execute(sql.insert_migration, {\n filename,\n timestamp,\n project,\n });\n }\n } finally {\n // Restore the search schema\n await sqldb.defaultPool.setSearchSchema(oldSchema);\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.test.d.ts","sourceRoot":"","sources":["../../src/migrations/migrations.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"migrations.test.d.ts","sourceRoot":"","sources":["../../src/migrations/migrations.test.ts"],"names":[],"mappings":"","sourcesContent":["import path from 'node:path';\n\nimport { afterAll, assert, beforeAll, describe, it } from 'vitest';\n\nimport { makePostgresTestUtils, queryAsync } from '@prairielearn/postgres';\n\nimport { getMigrationsToExecute, initWithLock } from './migrations.js';\n\ndescribe('migrations', () => {\n describe('getMigrationsToExecute', () => {\n it('handles the case of no executed migrations', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '001_testing.sql',\n timestamp: '20220101010101',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, { excludeMigrations: [] }),\n migrationFiles,\n );\n assert.deepEqual(getMigrationsToExecute(migrationFiles, {}), migrationFiles);\n });\n\n it('handles case where subset of migrations have been executed', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n const executedMigrations = [\n {\n timestamp: '20220101010101',\n },\n {\n timestamp: '20220101010102',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, { excludeMigrations: executedMigrations }),\n [\n {\n directory: 'migrations',\n timestamp: '20220101010103',\n filename: '20220101010103_testing_3.sql',\n },\n ],\n );\n });\n });\n\n it('handles case where beforeTimestamp is specified', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, {\n excludeMigrations: [],\n beforeTimestamp: '20220101010102',\n }),\n [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n ],\n );\n });\n it('handles case where inclusiveBefore is specified', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, {\n excludeMigrations: [],\n beforeTimestamp: '20220101010102',\n inclusiveBefore: true,\n }),\n [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n ],\n );\n });\n\n describe('initWithLock', () => {\n const postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n });\n\n beforeAll(async () => {\n await postgresTestUtils.createDatabase();\n });\n\n afterAll(async () => {\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs both SQL and JavaScript migrations', async () => {\n const migrationDir = path.join(import.meta.dirname, 'fixtures');\n await initWithLock({ directories: [migrationDir], project: 'prairielearn_migrations' });\n\n // If both migrations ran successfully, there should be a single user\n // in the database.\n const users = await queryAsync('SELECT * FROM users', {});\n assert.lengthOf(users.rows, 1);\n assert.equal(users.rows[0].name, 'Test User');\n });\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.test.js","sourceRoot":"","sources":["../../src/migrations/migrations.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEnE,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAE3E,OAAO,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEvE,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;
|
|
1
|
+
{"version":3,"file":"migrations.test.js","sourceRoot":"","sources":["../../src/migrations/migrations.test.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\n\nimport { afterAll, assert, beforeAll, describe, it } from 'vitest';\n\nimport { makePostgresTestUtils, queryAsync } from '@prairielearn/postgres';\n\nimport { getMigrationsToExecute, initWithLock } from './migrations.js';\n\ndescribe('migrations', () => {\n describe('getMigrationsToExecute', () => {\n it('handles the case of no executed migrations', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '001_testing.sql',\n timestamp: '20220101010101',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, { excludeMigrations: [] }),\n migrationFiles,\n );\n assert.deepEqual(getMigrationsToExecute(migrationFiles, {}), migrationFiles);\n });\n\n it('handles case where subset of migrations have been executed', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n const executedMigrations = [\n {\n timestamp: '20220101010101',\n },\n {\n timestamp: '20220101010102',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, { excludeMigrations: executedMigrations }),\n [\n {\n directory: 'migrations',\n timestamp: '20220101010103',\n filename: '20220101010103_testing_3.sql',\n },\n ],\n );\n });\n });\n\n it('handles case where beforeTimestamp is specified', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, {\n excludeMigrations: [],\n beforeTimestamp: '20220101010102',\n }),\n [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n ],\n );\n });\n it('handles case where inclusiveBefore is specified', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, {\n excludeMigrations: [],\n beforeTimestamp: '20220101010102',\n inclusiveBefore: true,\n }),\n [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n ],\n );\n });\n\n describe('initWithLock', () => {\n const postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n });\n\n beforeAll(async () => {\n await postgresTestUtils.createDatabase();\n });\n\n afterAll(async () => {\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs both SQL and JavaScript migrations', async () => {\n const migrationDir = path.join(import.meta.dirname, 'fixtures');\n await initWithLock({ directories: [migrationDir], project: 'prairielearn_migrations' });\n\n // If both migrations ran successfully, there should be a single user\n // in the database.\n const users = await queryAsync('SELECT * FROM users', {});\n assert.lengthOf(users.rows, 1);\n assert.equal(users.rows[0].name, 'Test User');\n });\n });\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/migrations",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/PrairieLearn/PrairieLearn.git",
|
|
8
8
|
"directory": "packages/migrations"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=24.0.0"
|
|
12
|
+
},
|
|
10
13
|
"main": "./dist/index.js",
|
|
11
14
|
"scripts": {
|
|
12
|
-
"build": "
|
|
13
|
-
"dev": "
|
|
15
|
+
"build": "tsgo && tscp",
|
|
16
|
+
"dev": "tsgo --watch --preserveWatchOutput & tscp --watch",
|
|
14
17
|
"test": "vitest run --coverage"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@prairielearn/error": "^
|
|
18
|
-
"@prairielearn/logger": "^
|
|
19
|
-
"@prairielearn/named-locks": "^
|
|
20
|
-
"@prairielearn/postgres": "^
|
|
20
|
+
"@prairielearn/error": "^3.0.0",
|
|
21
|
+
"@prairielearn/logger": "^3.0.0",
|
|
22
|
+
"@prairielearn/named-locks": "^4.0.0",
|
|
23
|
+
"@prairielearn/postgres": "^5.0.0",
|
|
21
24
|
"fs-extra": "^11.3.3",
|
|
22
25
|
"serialize-error": "^12.0.0",
|
|
23
26
|
"zod": "^3.25.76"
|
|
@@ -25,12 +28,13 @@
|
|
|
25
28
|
"devDependencies": {
|
|
26
29
|
"@prairielearn/tsconfig": "^0.0.0",
|
|
27
30
|
"@types/fs-extra": "^11.0.4",
|
|
28
|
-
"@types/node": "^
|
|
29
|
-
"@
|
|
31
|
+
"@types/node": "^24.10.9",
|
|
32
|
+
"@typescript/native-preview": "^7.0.0-dev.20260106.1",
|
|
33
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
30
34
|
"tmp-promise": "^3.0.3",
|
|
31
35
|
"tsx": "^4.21.0",
|
|
32
36
|
"typescript": "^5.9.3",
|
|
33
37
|
"typescript-cp": "^0.1.9",
|
|
34
|
-
"vitest": "^4.0.
|
|
38
|
+
"vitest": "^4.0.17"
|
|
35
39
|
}
|
|
36
40
|
}
|