@prairielearn/migrations 5.0.0 → 5.0.2
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 +26 -0
- package/dist/batched-migrations/batched-migration-runner.d.ts.map +1 -1
- package/dist/batched-migrations/batched-migration-runner.js +4 -4
- 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 +5 -3
- package/dist/batched-migrations/batched-migration-runner.test.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 +3 -2
- package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -1
- package/dist/migrations/migrations.d.ts.map +1 -1
- package/dist/migrations/migrations.js +12 -5
- package/dist/migrations/migrations.js.map +1 -1
- package/dist/migrations/migrations.test.d.ts.map +1 -1
- package/dist/migrations/migrations.test.js +4 -4
- package/dist/migrations/migrations.test.js.map +1 -1
- package/package.json +11 -11
- package/src/batched-migrations/batched-migration-runner.test.ts +5 -3
- package/src/batched-migrations/batched-migration-runner.ts +10 -4
- package/src/batched-migrations/batched-migrations-runner.test.ts +6 -3
- package/src/migrations/migrations.test.ts +4 -4
- package/src/migrations/migrations.ts +13 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @prairielearn/migrations
|
|
2
2
|
|
|
3
|
+
## 5.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3c4799a: Upgrade all JavaScript dependencies
|
|
8
|
+
- 2f9d39b: Update single-column Postgres query call sites to use `queryScalar` after the postgres package API change.
|
|
9
|
+
- b7885cd: Remove deprecated untyped SQL exports: `queryAsync`, `queryOneRowAsync`, `queryZeroOrOneRowAsync`, `callAsync`, `callOneRowAsync`, `callZeroOrOneRowAsync`
|
|
10
|
+
- Updated dependencies [3c4799a]
|
|
11
|
+
- Updated dependencies [b7885cd]
|
|
12
|
+
- Updated dependencies [2f9d39b]
|
|
13
|
+
- @prairielearn/named-locks@4.0.2
|
|
14
|
+
- @prairielearn/postgres@6.0.0
|
|
15
|
+
- @prairielearn/logger@3.1.1
|
|
16
|
+
- @prairielearn/error@3.0.3
|
|
17
|
+
|
|
18
|
+
## 5.0.1
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 8bdf6ea: Upgrade all JavaScript dependencies
|
|
23
|
+
- Updated dependencies [8bdf6ea]
|
|
24
|
+
- @prairielearn/named-locks@4.0.1
|
|
25
|
+
- @prairielearn/postgres@5.0.2
|
|
26
|
+
- @prairielearn/logger@3.0.1
|
|
27
|
+
- @prairielearn/error@3.0.2
|
|
28
|
+
|
|
3
29
|
## 5.0.0
|
|
4
30
|
|
|
5
31
|
### Major Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migration-runner.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"batched-migration-runner.d.ts","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.ts"],"names":[],"mappings":"AAiBA,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 {\n execute,\n loadSqlEquiv,\n queryOptionalRow,\n queryRow,\n queryScalar,\n} 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 queryScalar(\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 queryScalar(\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 queryScalar(\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,7 +1,7 @@
|
|
|
1
1
|
import { serializeError } from 'serialize-error';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { logger } from '@prairielearn/logger';
|
|
4
|
-
import { execute, loadSqlEquiv, queryOptionalRow, queryRow } from '@prairielearn/postgres';
|
|
4
|
+
import { execute, loadSqlEquiv, queryOptionalRow, queryRow, queryScalar, } from '@prairielearn/postgres';
|
|
5
5
|
import { BatchedMigrationJobRowSchema, } from './batched-migration-job.js';
|
|
6
6
|
import { BatchedMigrationStatusSchema, updateBatchedMigrationStatus, } from './batched-migration.js';
|
|
7
7
|
const sql = loadSqlEquiv(import.meta.filename);
|
|
@@ -22,13 +22,13 @@ export class BatchedMigrationRunner {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
async hasIncompleteJobs(migration) {
|
|
25
|
-
return await
|
|
25
|
+
return await queryScalar(sql.batched_migration_has_incomplete_jobs, { batched_migration_id: migration.id }, z.boolean());
|
|
26
26
|
}
|
|
27
27
|
async hasFailedJobs(migration) {
|
|
28
|
-
return await
|
|
28
|
+
return await queryScalar(sql.batched_migration_has_failed_jobs, { batched_migration_id: migration.id }, z.boolean());
|
|
29
29
|
}
|
|
30
30
|
async refreshMigrationStatus(migration) {
|
|
31
|
-
this.migrationStatus = await
|
|
31
|
+
this.migrationStatus = await queryScalar(sql.get_migration_status, { id: migration.id }, BatchedMigrationStatusSchema);
|
|
32
32
|
}
|
|
33
33
|
async finishRunningMigration(migration) {
|
|
34
34
|
// Safety check: if there are any pending jobs, don't mark this
|
|
@@ -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 {\n execute,\n loadSqlEquiv,\n queryOptionalRow,\n queryRow,\n queryScalar,\n} 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 queryScalar(\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 queryScalar(\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 queryScalar(\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":"","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
|
|
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 { withoutLogging } from '@prairielearn/logger';\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 withoutLogging(() => 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 // serialize-error converts BigInts to strings with 'n' suffix\n assert.equal(jobData.error.data.start, `${job.min_value}n`);\n assert.equal(jobData.error.data.end, `${job.max_value}n`);\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,5 +1,6 @@
|
|
|
1
1
|
import { afterAll, assert, beforeAll, beforeEach, describe, it } from 'vitest';
|
|
2
2
|
import * as error from '@prairielearn/error';
|
|
3
|
+
import { withoutLogging } from '@prairielearn/logger';
|
|
3
4
|
import * as namedLocks from '@prairielearn/named-locks';
|
|
4
5
|
import { execute, makePostgresTestUtils, queryRow, queryRows } from '@prairielearn/postgres';
|
|
5
6
|
import { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';
|
|
@@ -112,7 +113,7 @@ describe('BatchedMigrationExecutor', () => {
|
|
|
112
113
|
const migrationImplementation = makeTestBatchMigration();
|
|
113
114
|
migrationImplementation.setFailingIds([1n, 5010n]);
|
|
114
115
|
const runner = new BatchedMigrationRunner(migration, migrationImplementation);
|
|
115
|
-
await runner.run();
|
|
116
|
+
await withoutLogging(() => runner.run());
|
|
116
117
|
const jobs = await getBatchedMigrationJobs(migration.id);
|
|
117
118
|
const failedJobs = jobs.filter((job) => job.status === 'failed');
|
|
118
119
|
const successfulJobs = jobs.filter((job) => job.status === 'succeeded');
|
|
@@ -128,8 +129,9 @@ describe('BatchedMigrationExecutor', () => {
|
|
|
128
129
|
assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);
|
|
129
130
|
assert.equal(jobData.error.name, 'Error');
|
|
130
131
|
assert.equal(jobData.error.message, 'Execution failure');
|
|
131
|
-
|
|
132
|
-
assert.equal(jobData.error.data.
|
|
132
|
+
// serialize-error converts BigInts to strings with 'n' suffix
|
|
133
|
+
assert.equal(jobData.error.data.start, `${job.min_value}n`);
|
|
134
|
+
assert.equal(jobData.error.data.end, `${job.max_value}n`);
|
|
133
135
|
});
|
|
134
136
|
const failedMigration = await getBatchedMigration(migration.id);
|
|
135
137
|
assert.equal(failedMigration.status, 'failed');
|
|
@@ -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 { withoutLogging } from '@prairielearn/logger';\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 withoutLogging(() => 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 // serialize-error converts BigInts to strings with 'n' suffix\n assert.equal(jobData.error.data.start, `${job.min_value}n`);\n assert.equal(jobData.error.data.end, `${job.max_value}n`);\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-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
|
|
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 { withoutLogging } from '@prairielearn/logger';\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 withoutLogging(() =>\n runner.finalizeBatchedMigration('20230406184107_failing_migration', {\n logProgress: false,\n }),\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,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { afterAll, afterEach, assert, beforeAll, describe, expect, it } from 'vitest';
|
|
3
|
+
import { withoutLogging } from '@prairielearn/logger';
|
|
3
4
|
import * as namedLocks from '@prairielearn/named-locks';
|
|
4
5
|
import { makePostgresTestUtils } from '@prairielearn/postgres';
|
|
5
6
|
import { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';
|
|
@@ -74,9 +75,9 @@ describe('BatchedMigrationsRunner', () => {
|
|
|
74
75
|
directories: [path.join(import.meta.dirname, 'fixtures')],
|
|
75
76
|
});
|
|
76
77
|
await runner.enqueueBatchedMigration('20230406184107_failing_migration');
|
|
77
|
-
await expect(runner.finalizeBatchedMigration('20230406184107_failing_migration', {
|
|
78
|
+
await expect(withoutLogging(() => runner.finalizeBatchedMigration('20230406184107_failing_migration', {
|
|
78
79
|
logProgress: false,
|
|
79
|
-
})).rejects.toThrow("but it is 'failed'");
|
|
80
|
+
}))).rejects.toThrow("but it is 'failed'");
|
|
80
81
|
const migrations = await selectAllBatchedMigrations('test');
|
|
81
82
|
assert.lengthOf(migrations, 1);
|
|
82
83
|
assert.equal(migrations[0].timestamp, '20230406184107');
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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 { withoutLogging } from '@prairielearn/logger';\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 withoutLogging(() =>\n runner.finalizeBatchedMigration('20230406184107_failing_migration', {\n logProgress: false,\n }),\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":"migrations.d.ts","sourceRoot":"","sources":["../../src/migrations/migrations.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/migrations/migrations.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,uBAAuB,CAAC;AAW/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';\nimport { z } from 'zod';\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\nconst MigrationRowSchema = z.object({\n id: z.coerce.string(),\n filename: z.string(),\n index: z.number().nullable(),\n timestamp: z.string().nullable(),\n});\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.queryRows(sql.get_migrations, { project }, MigrationRowSchema);\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.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.queryRows(sql.get_migrations, { project }, MigrationRowSchema);\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,\n ...resolvedMigrationFilters,\n });\n for (const { directory, filename, timestamp } of migrationsToExecute) {\n if (allMigrations.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,11 +1,18 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import * as error from '@prairielearn/error';
|
|
4
5
|
import { logger } from '@prairielearn/logger';
|
|
5
6
|
import * as namedLocks from '@prairielearn/named-locks';
|
|
6
7
|
import * as sqldb from '@prairielearn/postgres';
|
|
7
8
|
import { parseAnnotations, readAndValidateMigrationsFromDirectories, sortMigrationFiles, } from '../load-migrations.js';
|
|
8
9
|
const sql = sqldb.loadSqlEquiv(import.meta.filename);
|
|
10
|
+
const MigrationRowSchema = z.object({
|
|
11
|
+
id: z.coerce.string(),
|
|
12
|
+
filename: z.string(),
|
|
13
|
+
index: z.number().nullable(),
|
|
14
|
+
timestamp: z.string().nullable(),
|
|
15
|
+
});
|
|
9
16
|
export async function init({ directories, project, migrationFilters = {} }) {
|
|
10
17
|
const migrationDirectories = Array.isArray(directories) ? directories : [directories];
|
|
11
18
|
const lockName = 'migrations';
|
|
@@ -89,7 +96,7 @@ export async function initWithLock({ directories, project, migrationFilters = {}
|
|
|
89
96
|
throw err;
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
|
-
let allMigrations = await sqldb.
|
|
99
|
+
let allMigrations = await sqldb.queryRows(sql.get_migrations, { project }, MigrationRowSchema);
|
|
93
100
|
const migrationFiles = await readAndValidateMigrationsFromDirectories(directories, [
|
|
94
101
|
'.sql',
|
|
95
102
|
'.js',
|
|
@@ -99,7 +106,7 @@ export async function initWithLock({ directories, project, migrationFilters = {}
|
|
|
99
106
|
// Validation: if we not all previously-executed migrations have timestamps,
|
|
100
107
|
// prompt the user to deploy an earlier version that includes both indexes
|
|
101
108
|
// and timestamps.
|
|
102
|
-
const migrationsMissingTimestamps = allMigrations.
|
|
109
|
+
const migrationsMissingTimestamps = allMigrations.filter((m) => !m.timestamp);
|
|
103
110
|
if (migrationsMissingTimestamps.length > 0) {
|
|
104
111
|
throw new Error([
|
|
105
112
|
'The following migrations are missing timestamps:',
|
|
@@ -110,16 +117,16 @@ export async function initWithLock({ directories, project, migrationFilters = {}
|
|
|
110
117
|
].join('\n'));
|
|
111
118
|
}
|
|
112
119
|
// Refetch the list of migrations from the database.
|
|
113
|
-
allMigrations = await sqldb.
|
|
120
|
+
allMigrations = await sqldb.queryRows(sql.get_migrations, { project }, MigrationRowSchema);
|
|
114
121
|
// Sort the migration files into execution order.
|
|
115
122
|
const sortedMigrationFiles = sortMigrationFiles(migrationFiles);
|
|
116
123
|
// Figure out which migrations have to be applied.
|
|
117
124
|
const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, {
|
|
118
|
-
excludeMigrations: allMigrations
|
|
125
|
+
excludeMigrations: allMigrations,
|
|
119
126
|
...resolvedMigrationFilters,
|
|
120
127
|
});
|
|
121
128
|
for (const { directory, filename, timestamp } of migrationsToExecute) {
|
|
122
|
-
if (allMigrations.
|
|
129
|
+
if (allMigrations.length === 0) {
|
|
123
130
|
// if we are running all the migrations then log at a lower level
|
|
124
131
|
logger.verbose(`Running migration ${filename}`);
|
|
125
132
|
}
|
|
@@ -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';\nimport { z } from 'zod';\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\nconst MigrationRowSchema = z.object({\n id: z.coerce.string(),\n filename: z.string(),\n index: z.number().nullable(),\n timestamp: z.string().nullable(),\n});\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.queryRows(sql.get_migrations, { project }, MigrationRowSchema);\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.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.queryRows(sql.get_migrations, { project }, MigrationRowSchema);\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,\n ...resolvedMigrationFilters,\n });\n for (const { directory, filename, timestamp } of migrationsToExecute) {\n if (allMigrations.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":"","sourcesContent":["import path from 'node:path';\n\nimport { afterAll, assert, beforeAll, describe, it } from 'vitest';\n\nimport { makePostgresTestUtils,
|
|
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';\nimport { z } from 'zod';\n\nimport { makePostgresTestUtils, queryRow } 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 queryRow('SELECT * FROM users', {}, z.object({ name: z.string() }));\n assert.equal(users.name, 'Test User');\n });\n });\n});\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { afterAll, assert, beforeAll, describe, it } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { makePostgresTestUtils, queryRow } from '@prairielearn/postgres';
|
|
4
5
|
import { getMigrationsToExecute, initWithLock } from './migrations.js';
|
|
5
6
|
describe('migrations', () => {
|
|
6
7
|
describe('getMigrationsToExecute', () => {
|
|
@@ -129,9 +130,8 @@ describe('migrations', () => {
|
|
|
129
130
|
await initWithLock({ directories: [migrationDir], project: 'prairielearn_migrations' });
|
|
130
131
|
// If both migrations ran successfully, there should be a single user
|
|
131
132
|
// in the database.
|
|
132
|
-
const users = await
|
|
133
|
-
assert.
|
|
134
|
-
assert.equal(users.rows[0].name, 'Test User');
|
|
133
|
+
const users = await queryRow('SELECT * FROM users', {}, z.object({ name: z.string() }));
|
|
134
|
+
assert.equal(users.name, 'Test User');
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
137
|
});
|
|
@@ -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;
|
|
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';\nimport { z } from 'zod';\n\nimport { makePostgresTestUtils, queryRow } 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 queryRow('SELECT * FROM users', {}, z.object({ name: z.string() }));\n assert.equal(users.name, 'Test User');\n });\n });\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/migrations",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,24 +17,24 @@
|
|
|
17
17
|
"test": "vitest run --coverage"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@prairielearn/error": "^3.0.
|
|
21
|
-
"@prairielearn/logger": "^3.
|
|
22
|
-
"@prairielearn/named-locks": "^4.0.
|
|
23
|
-
"@prairielearn/postgres": "^
|
|
20
|
+
"@prairielearn/error": "^3.0.3",
|
|
21
|
+
"@prairielearn/logger": "^3.1.1",
|
|
22
|
+
"@prairielearn/named-locks": "^4.0.2",
|
|
23
|
+
"@prairielearn/postgres": "^6.0.0",
|
|
24
24
|
"fs-extra": "^11.3.3",
|
|
25
|
-
"serialize-error": "^
|
|
25
|
+
"serialize-error": "^13.0.1",
|
|
26
26
|
"zod": "^3.25.76"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@prairielearn/tsconfig": "^
|
|
29
|
+
"@prairielearn/tsconfig": "^2.0.0",
|
|
30
30
|
"@types/fs-extra": "^11.0.4",
|
|
31
|
-
"@types/node": "^24.
|
|
32
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
33
|
-
"@vitest/coverage-v8": "^4.0.
|
|
31
|
+
"@types/node": "^24.11.0",
|
|
32
|
+
"@typescript/native-preview": "^7.0.0-dev.20260302.1",
|
|
33
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
34
34
|
"tmp-promise": "^3.0.3",
|
|
35
35
|
"tsx": "^4.21.0",
|
|
36
36
|
"typescript": "^5.9.3",
|
|
37
37
|
"typescript-cp": "^0.1.9",
|
|
38
|
-
"vitest": "^4.0.
|
|
38
|
+
"vitest": "^4.0.18"
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, assert, beforeAll, beforeEach, describe, it } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import * as error from '@prairielearn/error';
|
|
4
|
+
import { withoutLogging } from '@prairielearn/logger';
|
|
4
5
|
import * as namedLocks from '@prairielearn/named-locks';
|
|
5
6
|
import { execute, makePostgresTestUtils, queryRow, queryRows } from '@prairielearn/postgres';
|
|
6
7
|
|
|
@@ -151,7 +152,7 @@ describe('BatchedMigrationExecutor', () => {
|
|
|
151
152
|
const migrationImplementation = makeTestBatchMigration();
|
|
152
153
|
migrationImplementation.setFailingIds([1n, 5010n]);
|
|
153
154
|
const runner = new BatchedMigrationRunner(migration, migrationImplementation);
|
|
154
|
-
await runner.run();
|
|
155
|
+
await withoutLogging(() => runner.run());
|
|
155
156
|
|
|
156
157
|
const jobs = await getBatchedMigrationJobs(migration.id);
|
|
157
158
|
const failedJobs = jobs.filter((job) => job.status === 'failed');
|
|
@@ -168,8 +169,9 @@ describe('BatchedMigrationExecutor', () => {
|
|
|
168
169
|
assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);
|
|
169
170
|
assert.equal(jobData.error.name, 'Error');
|
|
170
171
|
assert.equal(jobData.error.message, 'Execution failure');
|
|
171
|
-
|
|
172
|
-
assert.equal(jobData.error.data.
|
|
172
|
+
// serialize-error converts BigInts to strings with 'n' suffix
|
|
173
|
+
assert.equal(jobData.error.data.start, `${job.min_value}n`);
|
|
174
|
+
assert.equal(jobData.error.data.end, `${job.max_value}n`);
|
|
173
175
|
});
|
|
174
176
|
|
|
175
177
|
const failedMigration = await getBatchedMigration(migration.id);
|
|
@@ -2,7 +2,13 @@ import { serializeError } from 'serialize-error';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
import { logger } from '@prairielearn/logger';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
execute,
|
|
7
|
+
loadSqlEquiv,
|
|
8
|
+
queryOptionalRow,
|
|
9
|
+
queryRow,
|
|
10
|
+
queryScalar,
|
|
11
|
+
} from '@prairielearn/postgres';
|
|
6
12
|
|
|
7
13
|
import {
|
|
8
14
|
type BatchedMigrationJobRow,
|
|
@@ -47,7 +53,7 @@ export class BatchedMigrationRunner {
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
private async hasIncompleteJobs(migration: BatchedMigrationRow): Promise<boolean> {
|
|
50
|
-
return await
|
|
56
|
+
return await queryScalar(
|
|
51
57
|
sql.batched_migration_has_incomplete_jobs,
|
|
52
58
|
{ batched_migration_id: migration.id },
|
|
53
59
|
z.boolean(),
|
|
@@ -55,7 +61,7 @@ export class BatchedMigrationRunner {
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
private async hasFailedJobs(migration: BatchedMigrationRow): Promise<boolean> {
|
|
58
|
-
return await
|
|
64
|
+
return await queryScalar(
|
|
59
65
|
sql.batched_migration_has_failed_jobs,
|
|
60
66
|
{ batched_migration_id: migration.id },
|
|
61
67
|
z.boolean(),
|
|
@@ -63,7 +69,7 @@ export class BatchedMigrationRunner {
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
private async refreshMigrationStatus(migration: BatchedMigrationRow) {
|
|
66
|
-
this.migrationStatus = await
|
|
72
|
+
this.migrationStatus = await queryScalar(
|
|
67
73
|
sql.get_migration_status,
|
|
68
74
|
{ id: migration.id },
|
|
69
75
|
BatchedMigrationStatusSchema,
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
|
|
3
3
|
import { afterAll, afterEach, assert, beforeAll, describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
|
+
import { withoutLogging } from '@prairielearn/logger';
|
|
5
6
|
import * as namedLocks from '@prairielearn/named-locks';
|
|
6
7
|
import { makePostgresTestUtils } from '@prairielearn/postgres';
|
|
7
8
|
|
|
@@ -97,9 +98,11 @@ describe('BatchedMigrationsRunner', () => {
|
|
|
97
98
|
await runner.enqueueBatchedMigration('20230406184107_failing_migration');
|
|
98
99
|
|
|
99
100
|
await expect(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
withoutLogging(() =>
|
|
102
|
+
runner.finalizeBatchedMigration('20230406184107_failing_migration', {
|
|
103
|
+
logProgress: false,
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
103
106
|
).rejects.toThrow("but it is 'failed'");
|
|
104
107
|
const migrations = await selectAllBatchedMigrations('test');
|
|
105
108
|
assert.lengthOf(migrations, 1);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
3
|
import { afterAll, assert, beforeAll, describe, it } from 'vitest';
|
|
4
|
+
import { z } from 'zod';
|
|
4
5
|
|
|
5
|
-
import { makePostgresTestUtils,
|
|
6
|
+
import { makePostgresTestUtils, queryRow } from '@prairielearn/postgres';
|
|
6
7
|
|
|
7
8
|
import { getMigrationsToExecute, initWithLock } from './migrations.js';
|
|
8
9
|
|
|
@@ -152,9 +153,8 @@ describe('migrations', () => {
|
|
|
152
153
|
|
|
153
154
|
// If both migrations ran successfully, there should be a single user
|
|
154
155
|
// in the database.
|
|
155
|
-
const users = await
|
|
156
|
-
assert.
|
|
157
|
-
assert.equal(users.rows[0].name, 'Test User');
|
|
156
|
+
const users = await queryRow('SELECT * FROM users', {}, z.object({ name: z.string() }));
|
|
157
|
+
assert.equal(users.name, 'Test User');
|
|
158
158
|
});
|
|
159
159
|
});
|
|
160
160
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
|
+
import { z } from 'zod';
|
|
4
5
|
|
|
5
6
|
import * as error from '@prairielearn/error';
|
|
6
7
|
import { logger } from '@prairielearn/logger';
|
|
@@ -16,6 +17,13 @@ import {
|
|
|
16
17
|
|
|
17
18
|
const sql = sqldb.loadSqlEquiv(import.meta.filename);
|
|
18
19
|
|
|
20
|
+
const MigrationRowSchema = z.object({
|
|
21
|
+
id: z.coerce.string(),
|
|
22
|
+
filename: z.string(),
|
|
23
|
+
index: z.number().nullable(),
|
|
24
|
+
timestamp: z.string().nullable(),
|
|
25
|
+
});
|
|
26
|
+
|
|
19
27
|
interface InitOptions {
|
|
20
28
|
directories: string[];
|
|
21
29
|
project: string;
|
|
@@ -130,7 +138,7 @@ export async function initWithLock({ directories, project, migrationFilters = {}
|
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
let allMigrations = await sqldb.
|
|
141
|
+
let allMigrations = await sqldb.queryRows(sql.get_migrations, { project }, MigrationRowSchema);
|
|
134
142
|
const migrationFiles = await readAndValidateMigrationsFromDirectories(directories, [
|
|
135
143
|
'.sql',
|
|
136
144
|
'.js',
|
|
@@ -141,7 +149,7 @@ export async function initWithLock({ directories, project, migrationFilters = {}
|
|
|
141
149
|
// Validation: if we not all previously-executed migrations have timestamps,
|
|
142
150
|
// prompt the user to deploy an earlier version that includes both indexes
|
|
143
151
|
// and timestamps.
|
|
144
|
-
const migrationsMissingTimestamps = allMigrations.
|
|
152
|
+
const migrationsMissingTimestamps = allMigrations.filter((m) => !m.timestamp);
|
|
145
153
|
if (migrationsMissingTimestamps.length > 0) {
|
|
146
154
|
throw new Error(
|
|
147
155
|
[
|
|
@@ -155,18 +163,18 @@ export async function initWithLock({ directories, project, migrationFilters = {}
|
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
// Refetch the list of migrations from the database.
|
|
158
|
-
allMigrations = await sqldb.
|
|
166
|
+
allMigrations = await sqldb.queryRows(sql.get_migrations, { project }, MigrationRowSchema);
|
|
159
167
|
|
|
160
168
|
// Sort the migration files into execution order.
|
|
161
169
|
const sortedMigrationFiles = sortMigrationFiles(migrationFiles);
|
|
162
170
|
|
|
163
171
|
// Figure out which migrations have to be applied.
|
|
164
172
|
const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, {
|
|
165
|
-
excludeMigrations: allMigrations
|
|
173
|
+
excludeMigrations: allMigrations,
|
|
166
174
|
...resolvedMigrationFilters,
|
|
167
175
|
});
|
|
168
176
|
for (const { directory, filename, timestamp } of migrationsToExecute) {
|
|
169
|
-
if (allMigrations.
|
|
177
|
+
if (allMigrations.length === 0) {
|
|
170
178
|
// if we are running all the migrations then log at a lower level
|
|
171
179
|
logger.verbose(`Running migration ${filename}`);
|
|
172
180
|
} else {
|