@prairielearn/migrations 3.0.24 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +9 -4
- package/dist/batched-migrations/batched-migration-runner.test.js +1 -1
- package/dist/batched-migrations/batched-migration-runner.test.js.map +1 -1
- package/dist/batched-migrations/batched-migrations-runner.test.js +1 -1
- package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/load-migrations.d.ts +1 -0
- package/dist/load-migrations.js +18 -10
- package/dist/load-migrations.js.map +1 -1
- package/dist/migrations/migrations.d.ts +27 -5
- package/dist/migrations/migrations.js +29 -8
- package/dist/migrations/migrations.js.map +1 -1
- package/dist/migrations/migrations.test.js +68 -3
- package/dist/migrations/migrations.test.js.map +1 -1
- package/package.json +2 -2
- package/src/batched-migrations/batched-migration-runner.test.ts +1 -1
- package/src/batched-migrations/batched-migrations-runner.test.ts +1 -1
- package/src/index.ts +1 -0
- package/src/load-migrations.ts +19 -15
- package/src/migrations/migrations.test.ts +85 -7
- package/src/migrations/migrations.ts +51 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @prairielearn/migrations
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- d4d5ce6: - `init(...)` now accepts an options object instead of positional arguments.
|
|
8
|
+
- `init(...)` now supports a `migrationFilters` option to control which migrations to run.
|
|
9
|
+
|
|
10
|
+
## 3.0.25
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- d6f506e: Update README for migrations to use modern function calls
|
|
15
|
+
- Updated dependencies [c6f661c]
|
|
16
|
+
- @prairielearn/postgres@4.2.0
|
|
17
|
+
|
|
3
18
|
## 3.0.24
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -60,19 +60,24 @@ A `makeBatchedMigration()` function is available to help ensure you're writing a
|
|
|
60
60
|
```ts
|
|
61
61
|
// batched-migrations/20230411002409_example_migration.ts
|
|
62
62
|
import { makeBatchedMigration } from '@prairielearn/migrations';
|
|
63
|
-
import {
|
|
63
|
+
import { queryRow, execute } from '@prairielearn/postgres';
|
|
64
64
|
|
|
65
65
|
export default makeBatchedMigration({
|
|
66
66
|
async getParameters() {
|
|
67
|
-
const
|
|
67
|
+
const max = await queryRow(
|
|
68
|
+
'SELECT MAX(id) as max from examples;',
|
|
69
|
+
z.bigint({ coerce: true }).nullable(),
|
|
70
|
+
);
|
|
71
|
+
|
|
68
72
|
return {
|
|
69
|
-
|
|
73
|
+
min: 1n,
|
|
74
|
+
max,
|
|
70
75
|
batchSize: 1000,
|
|
71
76
|
};
|
|
72
77
|
},
|
|
73
78
|
|
|
74
79
|
async execute(min: bigint, max: bigint) {
|
|
75
|
-
await
|
|
80
|
+
await execute('UPDATE examples SET text = TRIM(text) WHERE id >= $min AND id <= $max', {
|
|
76
81
|
min,
|
|
77
82
|
max,
|
|
78
83
|
});
|
|
@@ -69,7 +69,7 @@ describe('BatchedMigrationExecutor', () => {
|
|
|
69
69
|
await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {
|
|
70
70
|
throw err;
|
|
71
71
|
});
|
|
72
|
-
await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');
|
|
72
|
+
await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });
|
|
73
73
|
});
|
|
74
74
|
beforeEach(async () => {
|
|
75
75
|
await postgresTestUtils.resetDatabase();
|
|
@@ -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 await postgresTestUtils.createDatabase();\n await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {\n throw err;\n });\n await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');\n });\n\n beforeEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n afterAll(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs one iteration of a batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const executor = new BatchedMigrationRunner(migration, migrationImplementation);\n await executor.run({ iterations: 1 });\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 1);\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'running');\n\n assert.equal(migrationImplementation.executionCount, 1);\n });\n\n it('runs an entire batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 10);\n assert.equal(jobs[0].min_value, 1n);\n assert.equal(jobs[0].max_value, 1000n);\n assert.equal(jobs.at(-1)?.min_value, 9001n);\n assert.equal(jobs.at(-1)?.max_value, 10000n);\n assert.isTrue(jobs.every((job) => job.started_at !== null));\n assert.isTrue(jobs.every((job) => job.finished_at !== null));\n assert.isTrue(jobs.every((job) => job.status === 'succeeded'));\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'succeeded');\n });\n\n it('handles failing execution', async () => {\n let migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n migrationImplementation.setFailingIds([1n, 5010n]);\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n const failedJobs = jobs.filter((job) => job.status === 'failed');\n const successfulJobs = jobs.filter((job) => job.status === 'succeeded');\n assert.lengthOf(jobs, 10);\n assert.lengthOf(failedJobs, 2);\n assert.lengthOf(successfulJobs, 8);\n assert.equal(migrationImplementation.executionCount, 10);\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n failedJobs.forEach((job) => {\n const jobData = job.data as any;\n assert.isObject(jobData);\n assert.isObject(jobData.error);\n assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);\n assert.equal(jobData.error.name, 'Error');\n assert.equal(jobData.error.message, 'Execution failure');\n assert.equal(jobData.error.data.start, job.min_value.toString());\n assert.equal(jobData.error.data.end, job.max_value.toString());\n });\n\n const failedMigration = await getBatchedMigration(migration.id);\n assert.equal(failedMigration.status, 'failed');\n\n // Retry the failed jobs; ensure they succeed this time.\n await resetFailedBatchedMigrationJobs(migration.id);\n migration = await updateBatchedMigrationStatus(migration.id, 'running');\n\n migrationImplementation.setFailingIds([]);\n const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);\n await retryRunner.run();\n\n const finalJobs = await getBatchedMigrationJobs(migration.id);\n const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');\n const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');\n const retriedJobs = finalJobs.filter((job) => job.attempts === 2);\n assert.lengthOf(finalJobs, 10);\n assert.lengthOf(finalFailedJobs, 0);\n assert.lengthOf(finalSuccessfulJobs, 10);\n assert.lengthOf(retriedJobs, 2);\n assert.isTrue(finalJobs.every((job) => job.data === null));\n\n migration = await getBatchedMigration(migration.id);\n assert.equal(migration.status, 'succeeded');\n\n // The runner should have run only the previously failed jobs, which\n // works out to 2 additional execution.\n assert.equal(migrationImplementation.executionCount, 12);\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"batched-migration-runner.test.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migration-runner.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { afterAll, assert, beforeAll, beforeEach, describe, it } from 'vitest';\n\nimport * as error from '@prairielearn/error';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport { execute, makePostgresTestUtils, queryRow, queryRows } from '@prairielearn/postgres';\n\nimport { SCHEMA_MIGRATIONS_PATH, init } from '../index.js';\n\nimport { BatchedMigrationJobRowSchema } from './batched-migration-job.js';\nimport { BatchedMigrationRunner } from './batched-migration-runner.js';\nimport {\n BatchedMigrationRowSchema,\n insertBatchedMigration,\n makeBatchedMigration,\n updateBatchedMigrationStatus,\n} from './batched-migration.js';\n\nconst postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n});\n\nfunction makeTestBatchMigration() {\n let executionCount = 0;\n let failingIds: bigint[] = [];\n\n return makeBatchedMigration({\n async getParameters() {\n return {\n min: 1n,\n max: 10000n,\n batchSize: 1000,\n };\n },\n async execute(start: bigint, end: bigint) {\n executionCount += 1;\n const shouldFail = failingIds.some((id) => id >= start && id <= end);\n if (shouldFail) {\n // Throw an error with some data to make sure it gets persisted. We\n // specifically use BigInt values here to make sure that they are\n // correctly serialized to strings.\n throw new error.AugmentedError('Execution failure', { data: { start, end } });\n }\n },\n setFailingIds(ids: bigint[]) {\n failingIds = ids;\n },\n get executionCount() {\n return executionCount;\n },\n });\n}\n\nasync function getBatchedMigration(migrationId: string) {\n return await queryRow(\n 'SELECT * FROM batched_migrations WHERE id = $id;',\n { id: migrationId },\n BatchedMigrationRowSchema,\n );\n}\n\nasync function getBatchedMigrationJobs(migrationId: string) {\n return await queryRows(\n 'SELECT * FROM batched_migration_jobs WHERE batched_migration_id = $batched_migration_id ORDER BY id ASC;',\n { batched_migration_id: migrationId },\n BatchedMigrationJobRowSchema,\n );\n}\n\nasync function resetFailedBatchedMigrationJobs(migrationId: string) {\n await execute(\n \"UPDATE batched_migration_jobs SET status = 'pending', updated_at = CURRENT_TIMESTAMP WHERE batched_migration_id = $batched_migration_id AND status = 'failed'\",\n { batched_migration_id: migrationId },\n );\n}\n\nasync function insertTestBatchedMigration() {\n const migrationImplementation = makeTestBatchMigration();\n const parameters = await migrationImplementation.getParameters();\n const migration = await insertBatchedMigration({\n project: 'test',\n filename: '20230406184103_test_batch_migration.js',\n timestamp: '20230406184103',\n batch_size: parameters.batchSize,\n min_value: parameters.min,\n max_value: parameters.max,\n status: 'running',\n });\n if (!migration) throw new Error('Failed to insert batched migration');\n return migration;\n}\n\ndescribe('BatchedMigrationExecutor', () => {\n beforeAll(async () => {\n await postgresTestUtils.createDatabase();\n await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {\n throw err;\n });\n await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });\n });\n\n beforeEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n afterAll(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs one iteration of a batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const executor = new BatchedMigrationRunner(migration, migrationImplementation);\n await executor.run({ iterations: 1 });\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 1);\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'running');\n\n assert.equal(migrationImplementation.executionCount, 1);\n });\n\n it('runs an entire batched migration', async () => {\n const migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n assert.lengthOf(jobs, 10);\n assert.equal(jobs[0].min_value, 1n);\n assert.equal(jobs[0].max_value, 1000n);\n assert.equal(jobs.at(-1)?.min_value, 9001n);\n assert.equal(jobs.at(-1)?.max_value, 10000n);\n assert.isTrue(jobs.every((job) => job.started_at !== null));\n assert.isTrue(jobs.every((job) => job.finished_at !== null));\n assert.isTrue(jobs.every((job) => job.status === 'succeeded'));\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n\n const finalMigration = await getBatchedMigration(migration.id);\n assert.equal(finalMigration.status, 'succeeded');\n });\n\n it('handles failing execution', async () => {\n let migration = await insertTestBatchedMigration();\n\n const migrationImplementation = makeTestBatchMigration();\n migrationImplementation.setFailingIds([1n, 5010n]);\n const runner = new BatchedMigrationRunner(migration, migrationImplementation);\n await runner.run();\n\n const jobs = await getBatchedMigrationJobs(migration.id);\n const failedJobs = jobs.filter((job) => job.status === 'failed');\n const successfulJobs = jobs.filter((job) => job.status === 'succeeded');\n assert.lengthOf(jobs, 10);\n assert.lengthOf(failedJobs, 2);\n assert.lengthOf(successfulJobs, 8);\n assert.equal(migrationImplementation.executionCount, 10);\n assert.isTrue(jobs.every((job) => job.attempts === 1));\n failedJobs.forEach((job) => {\n const jobData = job.data as any;\n assert.isObject(jobData);\n assert.isObject(jobData.error);\n assert.hasAllKeys(jobData.error, ['name', 'message', 'stack', 'data', 'status']);\n assert.equal(jobData.error.name, 'Error');\n assert.equal(jobData.error.message, 'Execution failure');\n assert.equal(jobData.error.data.start, job.min_value.toString());\n assert.equal(jobData.error.data.end, job.max_value.toString());\n });\n\n const failedMigration = await getBatchedMigration(migration.id);\n assert.equal(failedMigration.status, 'failed');\n\n // Retry the failed jobs; ensure they succeed this time.\n await resetFailedBatchedMigrationJobs(migration.id);\n migration = await updateBatchedMigrationStatus(migration.id, 'running');\n\n migrationImplementation.setFailingIds([]);\n const retryRunner = new BatchedMigrationRunner(migration, migrationImplementation);\n await retryRunner.run();\n\n const finalJobs = await getBatchedMigrationJobs(migration.id);\n const finalFailedJobs = finalJobs.filter((job) => job.status === 'failed');\n const finalSuccessfulJobs = finalJobs.filter((job) => job.status === 'succeeded');\n const retriedJobs = finalJobs.filter((job) => job.attempts === 2);\n assert.lengthOf(finalJobs, 10);\n assert.lengthOf(finalFailedJobs, 0);\n assert.lengthOf(finalSuccessfulJobs, 10);\n assert.lengthOf(retriedJobs, 2);\n assert.isTrue(finalJobs.every((job) => job.data === null));\n\n migration = await getBatchedMigration(migration.id);\n assert.equal(migration.status, 'succeeded');\n\n // The runner should have run only the previously failed jobs, which\n // works out to 2 additional execution.\n assert.equal(migrationImplementation.executionCount, 12);\n });\n});\n"]}
|
|
@@ -14,7 +14,7 @@ describe('BatchedMigrationsRunner', () => {
|
|
|
14
14
|
await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {
|
|
15
15
|
throw err;
|
|
16
16
|
});
|
|
17
|
-
await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');
|
|
17
|
+
await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });
|
|
18
18
|
});
|
|
19
19
|
afterEach(async () => {
|
|
20
20
|
await postgresTestUtils.resetDatabase();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batched-migrations-runner.test.js","sourceRoot":"","sources":["../../src/batched-migrations/batched-migrations-runner.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEtF,OAAO,KAAK,UAAU,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAEzE,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;IAC9C,QAAQ,EAAE,yBAAyB;CACpC,CAAC,CAAC;AAEH,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,MAAM,iBAAiB,CAAC,cAAc,EAAE,CAAC;QACzC,MAAM,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,aAAa,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE;YAC/D,MAAM,GAAG,CAAC;QACZ,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,CAAC,sBAAsB,CAAC,EAAE,yBAAyB,CAAC,CAAC;
|
|
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 await postgresTestUtils.createDatabase();\n await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {\n throw err;\n });\n await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });\n });\n\n afterEach(async () => {\n await postgresTestUtils.resetDatabase();\n });\n\n afterAll(async () => {\n await namedLocks.close();\n await postgresTestUtils.dropDatabase();\n });\n\n it('enqueues migrations', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184107_failing_migration');\n await runner.enqueueBatchedMigration('20230407230446_no_rows_migration');\n\n const migrations = await selectAllBatchedMigrations('test');\n\n assert.lengthOf(migrations, 3);\n assert.equal(migrations[0].timestamp, '20230406184103');\n assert.equal(migrations[0].filename, '20230406184103_successful_migration.ts');\n assert.equal(migrations[0].status, 'pending');\n assert.equal(migrations[1].timestamp, '20230406184107');\n assert.equal(migrations[1].filename, '20230406184107_failing_migration.ts');\n assert.equal(migrations[1].status, 'pending');\n assert.equal(migrations[2].timestamp, '20230407230446');\n assert.equal(migrations[2].filename, '20230407230446_no_rows_migration.ts');\n assert.equal(migrations[2].status, 'succeeded');\n });\n\n it('safely enqueues migrations multiple times', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n\n const migrations = await selectAllBatchedMigrations('test');\n\n assert.lengthOf(migrations, 1);\n });\n\n it('finalizes a successful migration', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184103_successful_migration');\n await runner.finalizeBatchedMigration('20230406184103_successful_migration', {\n logProgress: false,\n });\n\n const migrations = await selectAllBatchedMigrations('test');\n assert.lengthOf(migrations, 1);\n assert.equal(migrations[0].timestamp, '20230406184103');\n assert.equal(migrations[0].status, 'succeeded');\n });\n\n it('finalizes a failing migration', async () => {\n const runner = new BatchedMigrationsRunner({\n project: 'test',\n directories: [path.join(import.meta.dirname, 'fixtures')],\n });\n\n await runner.enqueueBatchedMigration('20230406184107_failing_migration');\n\n await expect(\n runner.finalizeBatchedMigration('20230406184107_failing_migration', {\n logProgress: false,\n }),\n ).rejects.toThrow(\"but it is 'failed'\");\n const migrations = await selectAllBatchedMigrations('test');\n assert.lengthOf(migrations, 1);\n assert.equal(migrations[0].timestamp, '20230406184107');\n assert.equal(migrations[0].status, 'failed');\n });\n});\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { init } from './migrations/index.js';
|
|
2
2
|
export { type BatchedMigrationRow, type BatchedMigrationStatus, type BatchedMigrationJobRow, type BatchedMigrationJobStatus, makeBatchedMigration, initBatchedMigrations, startBatchedMigrations, stopBatchedMigrations, enqueueBatchedMigration, finalizeBatchedMigration, selectAllBatchedMigrations, selectBatchedMigration, selectBatchedMigrationForTimestamp, selectRecentJobsWithStatus, retryFailedBatchedMigrationJobs, } from './batched-migrations/index.js';
|
|
3
|
+
export { extractTimestampFromFilename } from './load-migrations.js';
|
|
3
4
|
export declare const SCHEMA_MIGRATIONS_PATH: string;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
export { init } from './migrations/index.js';
|
|
3
3
|
export { makeBatchedMigration, initBatchedMigrations, startBatchedMigrations, stopBatchedMigrations, enqueueBatchedMigration, finalizeBatchedMigration, selectAllBatchedMigrations, selectBatchedMigration, selectBatchedMigrationForTimestamp, selectRecentJobsWithStatus, retryFailedBatchedMigrationJobs, } from './batched-migrations/index.js';
|
|
4
|
+
export { extractTimestampFromFilename } from './load-migrations.js';
|
|
4
5
|
export const SCHEMA_MIGRATIONS_PATH = path.resolve(import.meta.dirname, '..', 'schema-migrations');
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EAKL,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,kCAAkC,EAClC,0BAA0B,EAC1B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,mBAAmB,CAAC,CAAC","sourcesContent":["import path from 'path';\n\nexport { init } from './migrations/index.js';\n\nexport {\n type BatchedMigrationRow,\n type BatchedMigrationStatus,\n type BatchedMigrationJobRow,\n type BatchedMigrationJobStatus,\n makeBatchedMigration,\n initBatchedMigrations,\n startBatchedMigrations,\n stopBatchedMigrations,\n enqueueBatchedMigration,\n finalizeBatchedMigration,\n selectAllBatchedMigrations,\n selectBatchedMigration,\n selectBatchedMigrationForTimestamp,\n selectRecentJobsWithStatus,\n retryFailedBatchedMigrationJobs,\n} from './batched-migrations/index.js';\n\nexport const SCHEMA_MIGRATIONS_PATH = path.resolve(import.meta.dirname, '..', 'schema-migrations');\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EAKL,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,kCAAkC,EAClC,0BAA0B,EAC1B,+BAA+B,GAChC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,mBAAmB,CAAC,CAAC","sourcesContent":["import path from 'path';\n\nexport { init } from './migrations/index.js';\n\nexport {\n type BatchedMigrationRow,\n type BatchedMigrationStatus,\n type BatchedMigrationJobRow,\n type BatchedMigrationJobStatus,\n makeBatchedMigration,\n initBatchedMigrations,\n startBatchedMigrations,\n stopBatchedMigrations,\n enqueueBatchedMigration,\n finalizeBatchedMigration,\n selectAllBatchedMigrations,\n selectBatchedMigration,\n selectBatchedMigrationForTimestamp,\n selectRecentJobsWithStatus,\n retryFailedBatchedMigrationJobs,\n} from './batched-migrations/index.js';\n\nexport { extractTimestampFromFilename } from './load-migrations.js';\nexport const SCHEMA_MIGRATIONS_PATH = path.resolve(import.meta.dirname, '..', 'schema-migrations');\n"]}
|
|
@@ -3,6 +3,7 @@ export interface MigrationFile {
|
|
|
3
3
|
filename: string;
|
|
4
4
|
timestamp: string;
|
|
5
5
|
}
|
|
6
|
+
export declare function extractTimestampFromFilename(filename: string): string;
|
|
6
7
|
export declare function readAndValidateMigrationsFromDirectory(dir: string, extensions: string[]): Promise<MigrationFile[]>;
|
|
7
8
|
export declare function readAndValidateMigrationsFromDirectories(directories: string[], extensions: string[]): Promise<MigrationFile[]>;
|
|
8
9
|
export declare function sortMigrationFiles(migrationFiles: MigrationFile[]): MigrationFile[];
|
package/dist/load-migrations.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
|
|
3
3
|
* Timestamp prefixes will be of the form `YYYYMMDDHHMMSS`, which will have 14 digits.
|
|
4
4
|
* If this code is still around in the year 10000... good luck.
|
|
5
5
|
*/
|
|
6
|
-
const MIGRATION_FILENAME_REGEX = /^([0-9]{14})_
|
|
6
|
+
const MIGRATION_FILENAME_REGEX = /^([0-9]{14})_.+/;
|
|
7
7
|
/**
|
|
8
8
|
* Annotations are expressed via the following:
|
|
9
9
|
*
|
|
@@ -15,17 +15,25 @@ const MIGRATION_FILENAME_REGEX = /^([0-9]{14})_.+\.[a-z]+/;
|
|
|
15
15
|
*/
|
|
16
16
|
const ANNOTATION_PREFIX = '-- prairielearn:migrations';
|
|
17
17
|
const ALLOWED_ANNOTATIONS = new Set(['NO TRANSACTION']);
|
|
18
|
+
export function extractTimestampFromFilename(filename) {
|
|
19
|
+
const match = filename.match(MIGRATION_FILENAME_REGEX);
|
|
20
|
+
if (!match) {
|
|
21
|
+
throw new Error(`Invalid migration filename: ${filename}`);
|
|
22
|
+
}
|
|
23
|
+
const timestamp = match.at(1) ?? null;
|
|
24
|
+
if (timestamp === null) {
|
|
25
|
+
throw new Error(`Migration ${filename} does not have a timestamp`);
|
|
26
|
+
}
|
|
27
|
+
return timestamp;
|
|
28
|
+
}
|
|
18
29
|
export async function readAndValidateMigrationsFromDirectory(dir, extensions) {
|
|
19
|
-
const migrationFiles = (await fs.readdir(dir)).filter((m) =>
|
|
30
|
+
const migrationFiles = (await fs.readdir(dir)).filter((m) => {
|
|
31
|
+
// Get the full extension of the file (e.g. for `foo.test.ts`, return `.test.ts`).
|
|
32
|
+
const [_name, ...extensionParts] = m.split('.');
|
|
33
|
+
return extensions.includes('.' + extensionParts.join('.'));
|
|
34
|
+
});
|
|
20
35
|
const migrations = migrationFiles.map((mf) => {
|
|
21
|
-
const
|
|
22
|
-
if (!match) {
|
|
23
|
-
throw new Error(`Invalid migration filename: ${mf}`);
|
|
24
|
-
}
|
|
25
|
-
const timestamp = match[1] ?? null;
|
|
26
|
-
if (timestamp === null) {
|
|
27
|
-
throw new Error(`Migration ${mf} does not have a timestamp`);
|
|
28
|
-
}
|
|
36
|
+
const timestamp = extractTimestampFromFilename(mf);
|
|
29
37
|
return {
|
|
30
38
|
directory: dir,
|
|
31
39
|
filename: mf,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-migrations.js","sourceRoot":"","sources":["../src/load-migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAE1B;;;GAGG;AACH,MAAM,wBAAwB,GAAG,
|
|
1
|
+
{"version":3,"file":"load-migrations.js","sourceRoot":"","sources":["../src/load-migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAE1B;;;GAGG;AACH,MAAM,wBAAwB,GAAG,iBAAiB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,iBAAiB,GAAG,4BAA4B,CAAC;AACvD,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC;AAQxD,MAAM,UAAU,4BAA4B,CAAC,QAAgB;IAC3D,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IACvD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,SAAS,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACtC,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,aAAa,QAAQ,4BAA4B,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sCAAsC,CAC1D,GAAW,EACX,UAAoB;IAEpB,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QAC1D,kFAAkF;QAClF,MAAM,CAAC,KAAK,EAAE,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAChD,OAAO,UAAU,CAAC,QAAQ,CAAC,GAAG,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAC3C,MAAM,SAAS,GAAG,4BAA4B,CAAC,EAAE,CAAC,CAAC;QAEnD,OAAO;YACL,SAAS,EAAE,GAAG;YACd,QAAQ,EAAE,EAAE;YACZ,SAAS;SACV,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,2EAA2E;IAC3E,mEAAmE;IACnE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAE,CAAC;IACjC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,SAAS,CAAC;QAE1C,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YACvB,IAAI,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBAClC,MAAM,IAAI,KAAK,CAAC,kCAAkC,SAAS,KAAK,QAAQ,GAAG,CAAC,CAAC;YAC/E,CAAC;YACD,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wCAAwC,CAC5D,WAAqB,EACrB,UAAoB;IAEpB,MAAM,aAAa,GAAoB,EAAE,CAAC;IAC1C,KAAK,MAAM,SAAS,IAAI,WAAW,EAAE,CAAC;QACpC,MAAM,UAAU,GAAG,MAAM,sCAAsC,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QACvF,aAAa,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,cAA+B;IAChE,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAClC,OAAO,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACrB,IAAI,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACvC,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/D,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;gBACzC,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAU,EAAE,CAAC,CAAC;YACvD,CAAC;YACD,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,WAAW,CAAC;AACrB,CAAC","sourcesContent":["import fs from 'fs-extra';\n\n/**\n * Timestamp prefixes will be of the form `YYYYMMDDHHMMSS`, which will have 14 digits.\n * If this code is still around in the year 10000... good luck.\n */\nconst MIGRATION_FILENAME_REGEX = /^([0-9]{14})_.+/;\n\n/**\n * Annotations are expressed via the following:\n *\n * -- prairielearn:migrations NO TRANSACTION\n *\n * Currently, `NO TRANSACTION` is the only supported annotation. This will run\n * the migration without a transaction. This is useful for migrations that use\n * features that can't be run in transactions, such as `CREATE INDEX CONCURRENTLY`.\n */\nconst ANNOTATION_PREFIX = '-- prairielearn:migrations';\nconst ALLOWED_ANNOTATIONS = new Set(['NO TRANSACTION']);\n\nexport interface MigrationFile {\n directory: string;\n filename: string;\n timestamp: string;\n}\n\nexport function extractTimestampFromFilename(filename: string): string {\n const match = filename.match(MIGRATION_FILENAME_REGEX);\n if (!match) {\n throw new Error(`Invalid migration filename: ${filename}`);\n }\n const timestamp = match.at(1) ?? null;\n if (timestamp === null) {\n throw new Error(`Migration ${filename} does not have a timestamp`);\n }\n return timestamp;\n}\n\nexport async function readAndValidateMigrationsFromDirectory(\n dir: string,\n extensions: string[],\n): Promise<MigrationFile[]> {\n const migrationFiles = (await fs.readdir(dir)).filter((m) => {\n // Get the full extension of the file (e.g. for `foo.test.ts`, return `.test.ts`).\n const [_name, ...extensionParts] = m.split('.');\n return extensions.includes('.' + extensionParts.join('.'));\n });\n\n const migrations = migrationFiles.map((mf) => {\n const timestamp = extractTimestampFromFilename(mf);\n\n return {\n directory: dir,\n filename: mf,\n timestamp,\n };\n });\n\n // First pass: validate that all migrations have a unique timestamp prefix.\n // This will avoid data loss and conflicts in unexpected scenarios.\n const seenTimestamps = new Set();\n for (const migration of migrations) {\n const { filename, timestamp } = migration;\n\n if (timestamp !== null) {\n if (seenTimestamps.has(timestamp)) {\n throw new Error(`Duplicate migration timestamp: ${timestamp} (${filename})`);\n }\n seenTimestamps.add(timestamp);\n }\n }\n\n return migrations;\n}\n\nexport async function readAndValidateMigrationsFromDirectories(\n directories: string[],\n extensions: string[],\n): Promise<MigrationFile[]> {\n const allMigrations: MigrationFile[] = [];\n for (const directory of directories) {\n const migrations = await readAndValidateMigrationsFromDirectory(directory, extensions);\n allMigrations.push(...migrations);\n }\n return allMigrations;\n}\n\nexport function sortMigrationFiles(migrationFiles: MigrationFile[]): MigrationFile[] {\n return migrationFiles.sort((a, b) => {\n return a.timestamp.localeCompare(b.timestamp);\n });\n}\n\nexport function parseAnnotations(contents: string): Set<string> {\n const lines = contents.split('\\n');\n const annotations = new Set<string>();\n\n lines.forEach((line) => {\n if (line.startsWith(ANNOTATION_PREFIX)) {\n const annotation = line.slice(ANNOTATION_PREFIX.length).trim();\n if (!ALLOWED_ANNOTATIONS.has(annotation)) {\n throw new Error(`Invalid annotation: ${annotation}`);\n }\n annotations.add(annotation);\n }\n });\n\n return annotations;\n}\n"]}
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { type MigrationFile } from '../load-migrations.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
interface InitOptions {
|
|
3
|
+
directories: string[];
|
|
4
|
+
project: string;
|
|
5
|
+
migrationFilters?: {
|
|
6
|
+
beforeTimestamp?: string | null;
|
|
7
|
+
inclusiveBefore?: boolean;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function init({ directories, project, migrationFilters }: InitOptions): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Get the migrations to execute.
|
|
13
|
+
*
|
|
14
|
+
* @param migrationFiles The full list of migration files.
|
|
15
|
+
* @param options The options for the migration execution.
|
|
16
|
+
* @param options.excludeMigrations The list of migrations to exclude.
|
|
17
|
+
* @param options.beforeTimestamp All migrations with timestamps before this timestamp will be excluded.
|
|
18
|
+
* @param options.inclusiveBefore Whether to include the migration with the timestamp equal to the beforeTimestamp.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getMigrationsToExecute(migrationFiles: MigrationFile[], { excludeMigrations, beforeTimestamp, inclusiveBefore, }: {
|
|
21
|
+
excludeMigrations?: {
|
|
22
|
+
timestamp: string | null;
|
|
23
|
+
}[];
|
|
24
|
+
beforeTimestamp?: string | null;
|
|
25
|
+
inclusiveBefore?: boolean;
|
|
26
|
+
}): MigrationFile[];
|
|
27
|
+
export declare function initWithLock({ directories, project, migrationFilters }: InitOptions): Promise<void>;
|
|
28
|
+
export {};
|
|
@@ -6,7 +6,7 @@ import * as namedLocks from '@prairielearn/named-locks';
|
|
|
6
6
|
import * as sqldb from '@prairielearn/postgres';
|
|
7
7
|
import { parseAnnotations, readAndValidateMigrationsFromDirectories, sortMigrationFiles, } from '../load-migrations.js';
|
|
8
8
|
const sql = sqldb.loadSqlEquiv(import.meta.filename);
|
|
9
|
-
export async function init(directories, project) {
|
|
9
|
+
export async function init({ directories, project, migrationFilters = {} }) {
|
|
10
10
|
const migrationDirectories = Array.isArray(directories) ? directories : [directories];
|
|
11
11
|
const lockName = 'migrations';
|
|
12
12
|
logger.verbose(`Waiting for lock ${lockName}`);
|
|
@@ -21,19 +21,37 @@ export async function init(directories, project) {
|
|
|
21
21
|
autoRenew: true,
|
|
22
22
|
}, async () => {
|
|
23
23
|
logger.verbose(`Acquired lock ${lockName}`);
|
|
24
|
-
await initWithLock(migrationDirectories, project);
|
|
24
|
+
await initWithLock({ directories: migrationDirectories, project, migrationFilters });
|
|
25
25
|
});
|
|
26
26
|
logger.verbose(`Released lock ${lockName}`);
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Get the migrations to execute.
|
|
30
|
+
*
|
|
31
|
+
* @param migrationFiles The full list of migration files.
|
|
32
|
+
* @param options The options for the migration execution.
|
|
33
|
+
* @param options.excludeMigrations The list of migrations to exclude.
|
|
34
|
+
* @param options.beforeTimestamp All migrations with timestamps before this timestamp will be excluded.
|
|
35
|
+
* @param options.inclusiveBefore Whether to include the migration with the timestamp equal to the beforeTimestamp.
|
|
36
|
+
*/
|
|
37
|
+
export function getMigrationsToExecute(migrationFiles, { excludeMigrations = [], beforeTimestamp = null, inclusiveBefore = false, }) {
|
|
29
38
|
// If no migrations have ever been run, run them all.
|
|
30
|
-
if (
|
|
39
|
+
if (excludeMigrations.length === 0 && beforeTimestamp === null) {
|
|
31
40
|
return migrationFiles;
|
|
32
41
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
42
|
+
const excludedMigrationTimestamps = new Set(excludeMigrations.map((m) => m.timestamp));
|
|
43
|
+
const remainingMigrationFiles = migrationFiles.filter((m) => !excludedMigrationTimestamps.has(m.timestamp));
|
|
44
|
+
if (beforeTimestamp === null) {
|
|
45
|
+
return remainingMigrationFiles;
|
|
46
|
+
}
|
|
47
|
+
return remainingMigrationFiles.filter((m) => inclusiveBefore ? m.timestamp <= beforeTimestamp : m.timestamp < beforeTimestamp);
|
|
35
48
|
}
|
|
36
|
-
export async function initWithLock(directories, project) {
|
|
49
|
+
export async function initWithLock({ directories, project, migrationFilters = {} }) {
|
|
50
|
+
const resolvedMigrationFilters = {
|
|
51
|
+
beforeTimestamp: null,
|
|
52
|
+
inclusiveBefore: false,
|
|
53
|
+
...migrationFilters,
|
|
54
|
+
};
|
|
37
55
|
logger.verbose('Starting DB schema migration');
|
|
38
56
|
const oldSchema = sqldb.defaultPool.getSearchSchema();
|
|
39
57
|
// Each postgres pool uses a unique schema every time the server starts up.
|
|
@@ -96,7 +114,10 @@ export async function initWithLock(directories, project) {
|
|
|
96
114
|
// Sort the migration files into execution order.
|
|
97
115
|
const sortedMigrationFiles = sortMigrationFiles(migrationFiles);
|
|
98
116
|
// Figure out which migrations have to be applied.
|
|
99
|
-
const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles,
|
|
117
|
+
const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, {
|
|
118
|
+
excludeMigrations: allMigrations.rows,
|
|
119
|
+
...resolvedMigrationFilters,
|
|
120
|
+
});
|
|
100
121
|
for (const { directory, filename, timestamp } of migrationsToExecute) {
|
|
101
122
|
if (allMigrations.rows.length === 0) {
|
|
102
123
|
// if we are running all the migrations then log at a lower level
|
|
@@ -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\nexport async function init(directories: string | string[], project: string) {\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(migrationDirectories, project);\n },\n );\n logger.verbose(`Released lock ${lockName}`);\n}\n\nexport function getMigrationsToExecute(\n migrationFiles: MigrationFile[],\n executedMigrations: { timestamp: string | null }[],\n): MigrationFile[] {\n // If no migrations have ever been run, run them all.\n if (executedMigrations.length === 0) {\n return migrationFiles;\n }\n\n const executedMigrationTimestamps = new Set(executedMigrations.map((m) => m.timestamp));\n return migrationFiles.filter((m) => !executedMigrationTimestamps.has(m.timestamp));\n}\n\nexport async function initWithLock(directories: string[], project: string) {\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, allMigrations.rows);\n for (const { directory, filename, timestamp } of migrationsToExecute) {\n if (allMigrations.rows.length === 0) {\n // if we are running all the migrations then log at a lower level\n logger.verbose(`Running migration ${filename}`);\n } else {\n logger.info(`Running migration ${filename}`);\n }\n\n const migrationPath = path.join(directory, filename);\n if (filename.endsWith('.sql')) {\n const migrationSql = await fs.readFile(migrationPath, 'utf8');\n const annotations = parseAnnotations(migrationSql);\n try {\n if (annotations.has('NO TRANSACTION')) {\n await sqldb.execute(migrationSql);\n } else {\n await sqldb.runInTransactionAsync(async () => {\n await sqldb.execute(migrationSql);\n });\n }\n } catch (err) {\n error.addData(err, { sqlFile: filename });\n throw err;\n }\n } else {\n const migrationModule = await import(/* @vite-ignore */ migrationPath);\n const implementation = migrationModule.default;\n if (typeof implementation !== 'function') {\n throw new Error(`Migration ${filename} does not export a default function`);\n }\n await implementation();\n }\n\n // Record the migration.\n await sqldb.execute(sql.insert_migration, {\n filename,\n timestamp,\n project,\n });\n }\n } finally {\n // Restore the search schema\n await sqldb.defaultPool.setSearchSchema(oldSchema);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/migrations/migrations.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'path';\n\nimport fs from 'fs-extra';\n\nimport * as error from '@prairielearn/error';\nimport { logger } from '@prairielearn/logger';\nimport * as namedLocks from '@prairielearn/named-locks';\nimport * as sqldb from '@prairielearn/postgres';\n\nimport {\n type MigrationFile,\n parseAnnotations,\n readAndValidateMigrationsFromDirectories,\n sortMigrationFiles,\n} from '../load-migrations.js';\n\nconst sql = sqldb.loadSqlEquiv(import.meta.filename);\n\ninterface InitOptions {\n directories: string[];\n project: string;\n migrationFilters?: {\n beforeTimestamp?: string | null;\n inclusiveBefore?: boolean;\n };\n}\n\nexport async function init({ directories, project, migrationFilters = {} }: InitOptions) {\n const migrationDirectories = Array.isArray(directories) ? directories : [directories];\n const lockName = 'migrations';\n logger.verbose(`Waiting for lock ${lockName}`);\n await namedLocks.doWithLock(\n lockName,\n {\n // Migrations *might* take a long time to run, so we'll enable automatic\n // lock renewal so that our lock doesn't get killed by the Postgres\n // idle session timeout.\n //\n // That said, we should generally try to keep migrations executing as\n // quickly as possible. A long-running migration likely means that\n // Postgres is locking a whole table, which is unacceptable in production.\n autoRenew: true,\n },\n async () => {\n logger.verbose(`Acquired lock ${lockName}`);\n await initWithLock({ directories: migrationDirectories, project, migrationFilters });\n },\n );\n logger.verbose(`Released lock ${lockName}`);\n}\n\n/**\n * Get the migrations to execute.\n *\n * @param migrationFiles The full list of migration files.\n * @param options The options for the migration execution.\n * @param options.excludeMigrations The list of migrations to exclude.\n * @param options.beforeTimestamp All migrations with timestamps before this timestamp will be excluded.\n * @param options.inclusiveBefore Whether to include the migration with the timestamp equal to the beforeTimestamp.\n */\nexport function getMigrationsToExecute(\n migrationFiles: MigrationFile[],\n {\n excludeMigrations = [],\n beforeTimestamp = null,\n inclusiveBefore = false,\n }: {\n excludeMigrations?: { timestamp: string | null }[];\n beforeTimestamp?: string | null;\n inclusiveBefore?: boolean;\n },\n): MigrationFile[] {\n // If no migrations have ever been run, run them all.\n if (excludeMigrations.length === 0 && beforeTimestamp === null) {\n return migrationFiles;\n }\n\n const excludedMigrationTimestamps = new Set(excludeMigrations.map((m) => m.timestamp));\n const remainingMigrationFiles = migrationFiles.filter(\n (m) => !excludedMigrationTimestamps.has(m.timestamp),\n );\n if (beforeTimestamp === null) {\n return remainingMigrationFiles;\n }\n return remainingMigrationFiles.filter((m) =>\n inclusiveBefore ? m.timestamp <= beforeTimestamp : m.timestamp < beforeTimestamp,\n );\n}\n\nexport async function initWithLock({ directories, project, migrationFilters = {} }: InitOptions) {\n const resolvedMigrationFilters = {\n beforeTimestamp: null,\n inclusiveBefore: false,\n ...migrationFilters,\n };\n\n logger.verbose('Starting DB schema migration');\n\n const oldSchema = sqldb.defaultPool.getSearchSchema();\n // Each postgres pool uses a unique schema every time the server starts up.\n // After that code runs, the default schema is set to that schema instead of public, which\n // causes the 'create_migrations_table' query to fail.\n //\n // We'll set the default schema to public before running the migrations, and then restore it\n // after the migrations are run.\n await sqldb.defaultPool.setSearchSchema('public');\n try {\n // Create the migrations table if needed\n await sqldb.execute(sql.create_migrations_table);\n\n // Apply necessary changes to the migrations table as needed.\n try {\n await sqldb.execute('SELECT project FROM migrations;');\n } catch (err: any) {\n if (err.routine === 'errorMissingColumn') {\n logger.info('Altering migrations table');\n await sqldb.execute(sql.add_projects_column);\n } else {\n throw err;\n }\n }\n try {\n await sqldb.execute('SELECT timestamp FROM migrations;');\n } catch (err: any) {\n if (err.routine === 'errorMissingColumn') {\n logger.info('Altering migrations table again');\n await sqldb.execute(sql.add_timestamp_column);\n } else {\n throw err;\n }\n }\n\n let allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });\n const migrationFiles = await readAndValidateMigrationsFromDirectories(directories, [\n '.sql',\n '.js',\n '.ts',\n '.mjs',\n ]);\n\n // Validation: if we not all previously-executed migrations have timestamps,\n // prompt the user to deploy an earlier version that includes both indexes\n // and timestamps.\n const migrationsMissingTimestamps = allMigrations.rows.filter((m) => !m.timestamp);\n if (migrationsMissingTimestamps.length > 0) {\n throw new Error(\n [\n 'The following migrations are missing timestamps:',\n migrationsMissingTimestamps.map((m) => ` ${m.filename}`),\n // This revision was the most recent commit to `master` before the\n // code handling indexes was removed.\n 'You must deploy revision 1aa43c7348fa24cf636413d720d06a2fa9e38ef2 first.',\n ].join('\\n'),\n );\n }\n\n // Refetch the list of migrations from the database.\n allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });\n\n // Sort the migration files into execution order.\n const sortedMigrationFiles = sortMigrationFiles(migrationFiles);\n\n // Figure out which migrations have to be applied.\n const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, {\n excludeMigrations: allMigrations.rows,\n ...resolvedMigrationFilters,\n });\n for (const { directory, filename, timestamp } of migrationsToExecute) {\n if (allMigrations.rows.length === 0) {\n // if we are running all the migrations then log at a lower level\n logger.verbose(`Running migration ${filename}`);\n } else {\n logger.info(`Running migration ${filename}`);\n }\n\n const migrationPath = path.join(directory, filename);\n if (filename.endsWith('.sql')) {\n const migrationSql = await fs.readFile(migrationPath, 'utf8');\n const annotations = parseAnnotations(migrationSql);\n try {\n if (annotations.has('NO TRANSACTION')) {\n await sqldb.execute(migrationSql);\n } else {\n await sqldb.runInTransactionAsync(async () => {\n await sqldb.execute(migrationSql);\n });\n }\n } catch (err) {\n error.addData(err, { sqlFile: filename });\n throw err;\n }\n } else {\n const migrationModule = await import(/* @vite-ignore */ migrationPath);\n const implementation = migrationModule.default;\n if (typeof implementation !== 'function') {\n throw new Error(`Migration ${filename} does not export a default function`);\n }\n await implementation();\n }\n\n // Record the migration.\n await sqldb.execute(sql.insert_migration, {\n filename,\n timestamp,\n project,\n });\n }\n } finally {\n // Restore the search schema\n await sqldb.defaultPool.setSearchSchema(oldSchema);\n }\n}\n"]}
|
|
@@ -12,7 +12,8 @@ describe('migrations', () => {
|
|
|
12
12
|
timestamp: '20220101010101',
|
|
13
13
|
},
|
|
14
14
|
];
|
|
15
|
-
assert.deepEqual(getMigrationsToExecute(migrationFiles, []), migrationFiles);
|
|
15
|
+
assert.deepEqual(getMigrationsToExecute(migrationFiles, { excludeMigrations: [] }), migrationFiles);
|
|
16
|
+
assert.deepEqual(getMigrationsToExecute(migrationFiles, {}), migrationFiles);
|
|
16
17
|
});
|
|
17
18
|
it('handles case where subset of migrations have been executed', () => {
|
|
18
19
|
const migrationFiles = [
|
|
@@ -40,7 +41,7 @@ describe('migrations', () => {
|
|
|
40
41
|
timestamp: '20220101010102',
|
|
41
42
|
},
|
|
42
43
|
];
|
|
43
|
-
assert.deepEqual(getMigrationsToExecute(migrationFiles, executedMigrations), [
|
|
44
|
+
assert.deepEqual(getMigrationsToExecute(migrationFiles, { excludeMigrations: executedMigrations }), [
|
|
44
45
|
{
|
|
45
46
|
directory: 'migrations',
|
|
46
47
|
timestamp: '20220101010103',
|
|
@@ -49,6 +50,70 @@ describe('migrations', () => {
|
|
|
49
50
|
]);
|
|
50
51
|
});
|
|
51
52
|
});
|
|
53
|
+
it('handles case where beforeTimestamp is specified', () => {
|
|
54
|
+
const migrationFiles = [
|
|
55
|
+
{
|
|
56
|
+
directory: 'migrations',
|
|
57
|
+
filename: '20220101010101_testing_1.sql',
|
|
58
|
+
timestamp: '20220101010101',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
directory: 'migrations',
|
|
62
|
+
filename: '20220101010102_testing_2.sql',
|
|
63
|
+
timestamp: '20220101010102',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
directory: 'migrations',
|
|
67
|
+
filename: '20220101010103_testing_3.sql',
|
|
68
|
+
timestamp: '20220101010103',
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
assert.deepEqual(getMigrationsToExecute(migrationFiles, {
|
|
72
|
+
excludeMigrations: [],
|
|
73
|
+
beforeTimestamp: '20220101010102',
|
|
74
|
+
}), [
|
|
75
|
+
{
|
|
76
|
+
directory: 'migrations',
|
|
77
|
+
filename: '20220101010101_testing_1.sql',
|
|
78
|
+
timestamp: '20220101010101',
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
it('handles case where inclusiveBefore is specified', () => {
|
|
83
|
+
const migrationFiles = [
|
|
84
|
+
{
|
|
85
|
+
directory: 'migrations',
|
|
86
|
+
filename: '20220101010101_testing_1.sql',
|
|
87
|
+
timestamp: '20220101010101',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
directory: 'migrations',
|
|
91
|
+
filename: '20220101010102_testing_2.sql',
|
|
92
|
+
timestamp: '20220101010102',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
directory: 'migrations',
|
|
96
|
+
filename: '20220101010103_testing_3.sql',
|
|
97
|
+
timestamp: '20220101010103',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
assert.deepEqual(getMigrationsToExecute(migrationFiles, {
|
|
101
|
+
excludeMigrations: [],
|
|
102
|
+
beforeTimestamp: '20220101010102',
|
|
103
|
+
inclusiveBefore: true,
|
|
104
|
+
}), [
|
|
105
|
+
{
|
|
106
|
+
directory: 'migrations',
|
|
107
|
+
filename: '20220101010101_testing_1.sql',
|
|
108
|
+
timestamp: '20220101010101',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
directory: 'migrations',
|
|
112
|
+
filename: '20220101010102_testing_2.sql',
|
|
113
|
+
timestamp: '20220101010102',
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
52
117
|
describe('initWithLock', () => {
|
|
53
118
|
const postgresTestUtils = makePostgresTestUtils({
|
|
54
119
|
database: 'prairielearn_migrations',
|
|
@@ -61,7 +126,7 @@ describe('migrations', () => {
|
|
|
61
126
|
});
|
|
62
127
|
it('runs both SQL and JavaScript migrations', async () => {
|
|
63
128
|
const migrationDir = path.join(import.meta.dirname, 'fixtures');
|
|
64
|
-
await initWithLock([migrationDir], 'prairielearn_migrations');
|
|
129
|
+
await initWithLock({ directories: [migrationDir], project: 'prairielearn_migrations' });
|
|
65
130
|
// If both migrations ran successfully, there should be a single user
|
|
66
131
|
// in the database.
|
|
67
132
|
const users = await queryAsync('SELECT * FROM users', {});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.test.js","sourceRoot":"","sources":["../../src/migrations/migrations.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEnE,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAE3E,OAAO,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEvE,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;QACtC,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;YACpD,MAAM,cAAc,GAAG;gBACrB;oBACE,SAAS,EAAE,YAAY;oBACvB,QAAQ,EAAE,iBAAiB;oBAC3B,SAAS,EAAE,gBAAgB;iBAC5B;aACF,CAAC;YACF,MAAM,CAAC,SAAS,CAAC,sBAAsB,CAAC,cAAc,EAAE,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC;QAC/E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;YACpE,MAAM,cAAc,GAAG;gBACrB;oBACE,SAAS,EAAE,YAAY;oBACvB,QAAQ,EAAE,8BAA8B;oBACxC,SAAS,EAAE,gBAAgB;iBAC5B;gBACD;oBACE,SAAS,EAAE,YAAY;oBACvB,QAAQ,EAAE,8BAA8B;oBACxC,SAAS,EAAE,gBAAgB;iBAC5B;gBACD;oBACE,SAAS,EAAE,YAAY;oBACvB,QAAQ,EAAE,8BAA8B;oBACxC,SAAS,EAAE,gBAAgB;iBAC5B;aACF,CAAC;YACF,MAAM,kBAAkB,GAAG;gBACzB;oBACE,SAAS,EAAE,gBAAgB;iBAC5B;gBACD;oBACE,SAAS,EAAE,gBAAgB;iBAC5B;aACF,CAAC;YACF,MAAM,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"migrations.test.js","sourceRoot":"","sources":["../../src/migrations/migrations.test.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\n\nimport { afterAll, assert, beforeAll, describe, it } from 'vitest';\n\nimport { makePostgresTestUtils, queryAsync } from '@prairielearn/postgres';\n\nimport { getMigrationsToExecute, initWithLock } from './migrations.js';\n\ndescribe('migrations', () => {\n describe('getMigrationsToExecute', () => {\n it('handles the case of no executed migrations', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '001_testing.sql',\n timestamp: '20220101010101',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, { excludeMigrations: [] }),\n migrationFiles,\n );\n assert.deepEqual(getMigrationsToExecute(migrationFiles, {}), migrationFiles);\n });\n\n it('handles case where subset of migrations have been executed', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n const executedMigrations = [\n {\n timestamp: '20220101010101',\n },\n {\n timestamp: '20220101010102',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, { excludeMigrations: executedMigrations }),\n [\n {\n directory: 'migrations',\n timestamp: '20220101010103',\n filename: '20220101010103_testing_3.sql',\n },\n ],\n );\n });\n });\n\n it('handles case where beforeTimestamp is specified', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, {\n excludeMigrations: [],\n beforeTimestamp: '20220101010102',\n }),\n [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n ],\n );\n });\n it('handles case where inclusiveBefore is specified', () => {\n const migrationFiles = [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n {\n directory: 'migrations',\n filename: '20220101010103_testing_3.sql',\n timestamp: '20220101010103',\n },\n ];\n assert.deepEqual(\n getMigrationsToExecute(migrationFiles, {\n excludeMigrations: [],\n beforeTimestamp: '20220101010102',\n inclusiveBefore: true,\n }),\n [\n {\n directory: 'migrations',\n filename: '20220101010101_testing_1.sql',\n timestamp: '20220101010101',\n },\n {\n directory: 'migrations',\n filename: '20220101010102_testing_2.sql',\n timestamp: '20220101010102',\n },\n ],\n );\n });\n\n describe('initWithLock', () => {\n const postgresTestUtils = makePostgresTestUtils({\n database: 'prairielearn_migrations',\n });\n\n beforeAll(async () => {\n await postgresTestUtils.createDatabase();\n });\n\n afterAll(async () => {\n await postgresTestUtils.dropDatabase();\n });\n\n it('runs both SQL and JavaScript migrations', async () => {\n const migrationDir = path.join(import.meta.dirname, 'fixtures');\n await initWithLock({ directories: [migrationDir], project: 'prairielearn_migrations' });\n\n // If both migrations ran successfully, there should be a single user\n // in the database.\n const users = await queryAsync('SELECT * FROM users', {});\n assert.lengthOf(users.rows, 1);\n assert.equal(users.rows[0].name, 'Test User');\n });\n });\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/migrations",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@prairielearn/error": "^2.0.19",
|
|
18
18
|
"@prairielearn/logger": "^2.0.20",
|
|
19
19
|
"@prairielearn/named-locks": "^3.0.23",
|
|
20
|
-
"@prairielearn/postgres": "^4.
|
|
20
|
+
"@prairielearn/postgres": "^4.3.0",
|
|
21
21
|
"fs-extra": "^11.3.1",
|
|
22
22
|
"serialize-error": "^12.0.0",
|
|
23
23
|
"zod": "^3.25.76"
|
|
@@ -95,7 +95,7 @@ describe('BatchedMigrationExecutor', () => {
|
|
|
95
95
|
await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {
|
|
96
96
|
throw err;
|
|
97
97
|
});
|
|
98
|
-
await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');
|
|
98
|
+
await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
beforeEach(async () => {
|
|
@@ -20,7 +20,7 @@ describe('BatchedMigrationsRunner', () => {
|
|
|
20
20
|
await namedLocks.init(postgresTestUtils.getPoolConfig(), (err) => {
|
|
21
21
|
throw err;
|
|
22
22
|
});
|
|
23
|
-
await init([SCHEMA_MIGRATIONS_PATH], 'prairielearn_migrations');
|
|
23
|
+
await init({ directories: [SCHEMA_MIGRATIONS_PATH], project: 'prairielearn_migrations' });
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
afterEach(async () => {
|
package/src/index.ts
CHANGED
package/src/load-migrations.ts
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
|
|
|
4
4
|
* Timestamp prefixes will be of the form `YYYYMMDDHHMMSS`, which will have 14 digits.
|
|
5
5
|
* If this code is still around in the year 10000... good luck.
|
|
6
6
|
*/
|
|
7
|
-
const MIGRATION_FILENAME_REGEX = /^([0-9]{14})_
|
|
7
|
+
const MIGRATION_FILENAME_REGEX = /^([0-9]{14})_.+/;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Annotations are expressed via the following:
|
|
@@ -24,26 +24,30 @@ export interface MigrationFile {
|
|
|
24
24
|
timestamp: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function extractTimestampFromFilename(filename: string): string {
|
|
28
|
+
const match = filename.match(MIGRATION_FILENAME_REGEX);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error(`Invalid migration filename: ${filename}`);
|
|
31
|
+
}
|
|
32
|
+
const timestamp = match.at(1) ?? null;
|
|
33
|
+
if (timestamp === null) {
|
|
34
|
+
throw new Error(`Migration ${filename} does not have a timestamp`);
|
|
35
|
+
}
|
|
36
|
+
return timestamp;
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
export async function readAndValidateMigrationsFromDirectory(
|
|
28
40
|
dir: string,
|
|
29
41
|
extensions: string[],
|
|
30
42
|
): Promise<MigrationFile[]> {
|
|
31
|
-
const migrationFiles = (await fs.readdir(dir)).filter((m) =>
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
const migrationFiles = (await fs.readdir(dir)).filter((m) => {
|
|
44
|
+
// Get the full extension of the file (e.g. for `foo.test.ts`, return `.test.ts`).
|
|
45
|
+
const [_name, ...extensionParts] = m.split('.');
|
|
46
|
+
return extensions.includes('.' + extensionParts.join('.'));
|
|
47
|
+
});
|
|
34
48
|
|
|
35
49
|
const migrations = migrationFiles.map((mf) => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (!match) {
|
|
39
|
-
throw new Error(`Invalid migration filename: ${mf}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const timestamp = match[1] ?? null;
|
|
43
|
-
|
|
44
|
-
if (timestamp === null) {
|
|
45
|
-
throw new Error(`Migration ${mf} does not have a timestamp`);
|
|
46
|
-
}
|
|
50
|
+
const timestamp = extractTimestampFromFilename(mf);
|
|
47
51
|
|
|
48
52
|
return {
|
|
49
53
|
directory: dir,
|
|
@@ -16,7 +16,11 @@ describe('migrations', () => {
|
|
|
16
16
|
timestamp: '20220101010101',
|
|
17
17
|
},
|
|
18
18
|
];
|
|
19
|
-
assert.deepEqual(
|
|
19
|
+
assert.deepEqual(
|
|
20
|
+
getMigrationsToExecute(migrationFiles, { excludeMigrations: [] }),
|
|
21
|
+
migrationFiles,
|
|
22
|
+
);
|
|
23
|
+
assert.deepEqual(getMigrationsToExecute(migrationFiles, {}), migrationFiles);
|
|
20
24
|
});
|
|
21
25
|
|
|
22
26
|
it('handles case where subset of migrations have been executed', () => {
|
|
@@ -45,14 +49,88 @@ describe('migrations', () => {
|
|
|
45
49
|
timestamp: '20220101010102',
|
|
46
50
|
},
|
|
47
51
|
];
|
|
48
|
-
assert.deepEqual(
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
getMigrationsToExecute(migrationFiles, { excludeMigrations: executedMigrations }),
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
directory: 'migrations',
|
|
57
|
+
timestamp: '20220101010103',
|
|
58
|
+
filename: '20220101010103_testing_3.sql',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('handles case where beforeTimestamp is specified', () => {
|
|
66
|
+
const migrationFiles = [
|
|
67
|
+
{
|
|
68
|
+
directory: 'migrations',
|
|
69
|
+
filename: '20220101010101_testing_1.sql',
|
|
70
|
+
timestamp: '20220101010101',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
directory: 'migrations',
|
|
74
|
+
filename: '20220101010102_testing_2.sql',
|
|
75
|
+
timestamp: '20220101010102',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
directory: 'migrations',
|
|
79
|
+
filename: '20220101010103_testing_3.sql',
|
|
80
|
+
timestamp: '20220101010103',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
getMigrationsToExecute(migrationFiles, {
|
|
85
|
+
excludeMigrations: [],
|
|
86
|
+
beforeTimestamp: '20220101010102',
|
|
87
|
+
}),
|
|
88
|
+
[
|
|
49
89
|
{
|
|
50
90
|
directory: 'migrations',
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
filename: '20220101010101_testing_1.sql',
|
|
92
|
+
timestamp: '20220101010101',
|
|
53
93
|
},
|
|
54
|
-
]
|
|
55
|
-
|
|
94
|
+
],
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
it('handles case where inclusiveBefore is specified', () => {
|
|
98
|
+
const migrationFiles = [
|
|
99
|
+
{
|
|
100
|
+
directory: 'migrations',
|
|
101
|
+
filename: '20220101010101_testing_1.sql',
|
|
102
|
+
timestamp: '20220101010101',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
directory: 'migrations',
|
|
106
|
+
filename: '20220101010102_testing_2.sql',
|
|
107
|
+
timestamp: '20220101010102',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
directory: 'migrations',
|
|
111
|
+
filename: '20220101010103_testing_3.sql',
|
|
112
|
+
timestamp: '20220101010103',
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
getMigrationsToExecute(migrationFiles, {
|
|
117
|
+
excludeMigrations: [],
|
|
118
|
+
beforeTimestamp: '20220101010102',
|
|
119
|
+
inclusiveBefore: true,
|
|
120
|
+
}),
|
|
121
|
+
[
|
|
122
|
+
{
|
|
123
|
+
directory: 'migrations',
|
|
124
|
+
filename: '20220101010101_testing_1.sql',
|
|
125
|
+
timestamp: '20220101010101',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
directory: 'migrations',
|
|
129
|
+
filename: '20220101010102_testing_2.sql',
|
|
130
|
+
timestamp: '20220101010102',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
);
|
|
56
134
|
});
|
|
57
135
|
|
|
58
136
|
describe('initWithLock', () => {
|
|
@@ -70,7 +148,7 @@ describe('migrations', () => {
|
|
|
70
148
|
|
|
71
149
|
it('runs both SQL and JavaScript migrations', async () => {
|
|
72
150
|
const migrationDir = path.join(import.meta.dirname, 'fixtures');
|
|
73
|
-
await initWithLock([migrationDir], 'prairielearn_migrations');
|
|
151
|
+
await initWithLock({ directories: [migrationDir], project: 'prairielearn_migrations' });
|
|
74
152
|
|
|
75
153
|
// If both migrations ran successfully, there should be a single user
|
|
76
154
|
// in the database.
|
|
@@ -16,7 +16,16 @@ import {
|
|
|
16
16
|
|
|
17
17
|
const sql = sqldb.loadSqlEquiv(import.meta.filename);
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
interface InitOptions {
|
|
20
|
+
directories: string[];
|
|
21
|
+
project: string;
|
|
22
|
+
migrationFilters?: {
|
|
23
|
+
beforeTimestamp?: string | null;
|
|
24
|
+
inclusiveBefore?: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function init({ directories, project, migrationFilters = {} }: InitOptions) {
|
|
20
29
|
const migrationDirectories = Array.isArray(directories) ? directories : [directories];
|
|
21
30
|
const lockName = 'migrations';
|
|
22
31
|
logger.verbose(`Waiting for lock ${lockName}`);
|
|
@@ -34,26 +43,57 @@ export async function init(directories: string | string[], project: string) {
|
|
|
34
43
|
},
|
|
35
44
|
async () => {
|
|
36
45
|
logger.verbose(`Acquired lock ${lockName}`);
|
|
37
|
-
await initWithLock(migrationDirectories, project);
|
|
46
|
+
await initWithLock({ directories: migrationDirectories, project, migrationFilters });
|
|
38
47
|
},
|
|
39
48
|
);
|
|
40
49
|
logger.verbose(`Released lock ${lockName}`);
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Get the migrations to execute.
|
|
54
|
+
*
|
|
55
|
+
* @param migrationFiles The full list of migration files.
|
|
56
|
+
* @param options The options for the migration execution.
|
|
57
|
+
* @param options.excludeMigrations The list of migrations to exclude.
|
|
58
|
+
* @param options.beforeTimestamp All migrations with timestamps before this timestamp will be excluded.
|
|
59
|
+
* @param options.inclusiveBefore Whether to include the migration with the timestamp equal to the beforeTimestamp.
|
|
60
|
+
*/
|
|
43
61
|
export function getMigrationsToExecute(
|
|
44
62
|
migrationFiles: MigrationFile[],
|
|
45
|
-
|
|
63
|
+
{
|
|
64
|
+
excludeMigrations = [],
|
|
65
|
+
beforeTimestamp = null,
|
|
66
|
+
inclusiveBefore = false,
|
|
67
|
+
}: {
|
|
68
|
+
excludeMigrations?: { timestamp: string | null }[];
|
|
69
|
+
beforeTimestamp?: string | null;
|
|
70
|
+
inclusiveBefore?: boolean;
|
|
71
|
+
},
|
|
46
72
|
): MigrationFile[] {
|
|
47
73
|
// If no migrations have ever been run, run them all.
|
|
48
|
-
if (
|
|
74
|
+
if (excludeMigrations.length === 0 && beforeTimestamp === null) {
|
|
49
75
|
return migrationFiles;
|
|
50
76
|
}
|
|
51
77
|
|
|
52
|
-
const
|
|
53
|
-
|
|
78
|
+
const excludedMigrationTimestamps = new Set(excludeMigrations.map((m) => m.timestamp));
|
|
79
|
+
const remainingMigrationFiles = migrationFiles.filter(
|
|
80
|
+
(m) => !excludedMigrationTimestamps.has(m.timestamp),
|
|
81
|
+
);
|
|
82
|
+
if (beforeTimestamp === null) {
|
|
83
|
+
return remainingMigrationFiles;
|
|
84
|
+
}
|
|
85
|
+
return remainingMigrationFiles.filter((m) =>
|
|
86
|
+
inclusiveBefore ? m.timestamp <= beforeTimestamp : m.timestamp < beforeTimestamp,
|
|
87
|
+
);
|
|
54
88
|
}
|
|
55
89
|
|
|
56
|
-
export async function initWithLock(directories
|
|
90
|
+
export async function initWithLock({ directories, project, migrationFilters = {} }: InitOptions) {
|
|
91
|
+
const resolvedMigrationFilters = {
|
|
92
|
+
beforeTimestamp: null,
|
|
93
|
+
inclusiveBefore: false,
|
|
94
|
+
...migrationFilters,
|
|
95
|
+
};
|
|
96
|
+
|
|
57
97
|
logger.verbose('Starting DB schema migration');
|
|
58
98
|
|
|
59
99
|
const oldSchema = sqldb.defaultPool.getSearchSchema();
|
|
@@ -121,7 +161,10 @@ export async function initWithLock(directories: string[], project: string) {
|
|
|
121
161
|
const sortedMigrationFiles = sortMigrationFiles(migrationFiles);
|
|
122
162
|
|
|
123
163
|
// Figure out which migrations have to be applied.
|
|
124
|
-
const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles,
|
|
164
|
+
const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, {
|
|
165
|
+
excludeMigrations: allMigrations.rows,
|
|
166
|
+
...resolvedMigrationFilters,
|
|
167
|
+
});
|
|
125
168
|
for (const { directory, filename, timestamp } of migrationsToExecute) {
|
|
126
169
|
if (allMigrations.rows.length === 0) {
|
|
127
170
|
// if we are running all the migrations then log at a lower level
|