@prairielearn/migrations 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +145 -0
  3. package/dist/batched-migrations/batched-migration-job.d.ts +42 -0
  4. package/dist/batched-migrations/batched-migration-job.js +25 -0
  5. package/dist/batched-migrations/batched-migration-job.js.map +1 -0
  6. package/dist/batched-migrations/batched-migration-job.sql +12 -0
  7. package/dist/batched-migrations/batched-migration-runner.d.ts +28 -0
  8. package/dist/batched-migrations/batched-migration-runner.js +136 -0
  9. package/dist/batched-migrations/batched-migration-runner.js.map +1 -0
  10. package/dist/batched-migrations/batched-migration-runner.sql +93 -0
  11. package/dist/batched-migrations/batched-migration-runner.test.js +185 -0
  12. package/dist/batched-migrations/batched-migration-runner.test.js.map +1 -0
  13. package/dist/batched-migrations/batched-migration.d.ts +79 -0
  14. package/dist/batched-migrations/batched-migration.js +73 -0
  15. package/dist/batched-migrations/batched-migration.js.map +1 -0
  16. package/dist/batched-migrations/batched-migration.sql +95 -0
  17. package/dist/batched-migrations/batched-migrations-runner.d.ts +63 -0
  18. package/dist/batched-migrations/batched-migrations-runner.js +273 -0
  19. package/dist/batched-migrations/batched-migrations-runner.js.map +1 -0
  20. package/dist/batched-migrations/batched-migrations-runner.sql +35 -0
  21. package/dist/batched-migrations/batched-migrations-runner.test.d.ts +1 -0
  22. package/dist/batched-migrations/batched-migrations-runner.test.js +116 -0
  23. package/dist/batched-migrations/batched-migrations-runner.test.js.map +1 -0
  24. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.d.ts +9 -0
  25. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js +14 -0
  26. package/dist/batched-migrations/fixtures/20230406184103_successful_migration.js.map +1 -0
  27. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.d.ts +8 -0
  28. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js +16 -0
  29. package/dist/batched-migrations/fixtures/20230407230446_no_rows_migration.js.map +1 -0
  30. package/dist/batched-migrations/index.d.ts +3 -0
  31. package/dist/batched-migrations/index.js +18 -0
  32. package/dist/batched-migrations/index.js.map +1 -0
  33. package/dist/index.d.ts +3 -11
  34. package/dist/index.js +15 -167
  35. package/dist/index.js.map +1 -1
  36. package/dist/load-migrations.d.ts +8 -0
  37. package/dist/load-migrations.js +60 -0
  38. package/dist/load-migrations.js.map +1 -0
  39. package/dist/load-migrations.test.d.ts +1 -0
  40. package/dist/{index.test.js → load-migrations.test.js} +12 -44
  41. package/dist/load-migrations.test.js.map +1 -0
  42. package/dist/migrations/fixtures/20230407210409_create_users.sql +2 -0
  43. package/dist/migrations/fixtures/20230407210430_insert_user.d.ts +1 -0
  44. package/dist/migrations/fixtures/20230407210430_insert_user.js +7 -0
  45. package/dist/migrations/fixtures/20230407210430_insert_user.js.map +1 -0
  46. package/dist/migrations/index.d.ts +1 -0
  47. package/dist/migrations/index.js +6 -0
  48. package/dist/migrations/index.js.map +1 -0
  49. package/dist/migrations/migrations.d.ts +6 -0
  50. package/dist/migrations/migrations.js +159 -0
  51. package/dist/migrations/migrations.js.map +1 -0
  52. package/dist/migrations/migrations.test.d.ts +1 -0
  53. package/dist/migrations/migrations.test.js +78 -0
  54. package/dist/migrations/migrations.test.js.map +1 -0
  55. package/package.json +16 -8
  56. package/schema-migrations/20230303193423_batched_migrations__create.sql +49 -0
  57. package/src/batched-migrations/batched-migration-job.sql +12 -0
  58. package/src/batched-migrations/batched-migration-job.ts +34 -0
  59. package/src/batched-migrations/batched-migration-runner.sql +93 -0
  60. package/src/batched-migrations/batched-migration-runner.test.ts +208 -0
  61. package/src/batched-migrations/batched-migration-runner.ts +215 -0
  62. package/src/batched-migrations/batched-migration.sql +95 -0
  63. package/src/batched-migrations/batched-migration.ts +129 -0
  64. package/src/batched-migrations/batched-migrations-runner.sql +35 -0
  65. package/src/batched-migrations/batched-migrations-runner.test.ts +111 -0
  66. package/src/batched-migrations/batched-migrations-runner.ts +327 -0
  67. package/src/batched-migrations/fixtures/20230406184103_successful_migration.ts +13 -0
  68. package/src/batched-migrations/fixtures/20230406184107_failing_migration.js +16 -0
  69. package/src/batched-migrations/fixtures/20230407230446_no_rows_migration.ts +15 -0
  70. package/src/batched-migrations/index.ts +21 -0
  71. package/src/index.ts +20 -173
  72. package/src/{index.test.ts → load-migrations.test.ts} +10 -48
  73. package/src/load-migrations.ts +76 -0
  74. package/src/migrations/fixtures/20230407210409_create_users.sql +2 -0
  75. package/src/migrations/fixtures/20230407210430_insert_user.ts +5 -0
  76. package/src/migrations/index.ts +1 -0
  77. package/src/migrations/migrations.test.ts +80 -0
  78. package/src/migrations/migrations.ts +149 -0
  79. package/dist/index.test.js.map +0 -1
  80. /package/dist/{index.test.d.ts → batched-migrations/batched-migration-runner.test.d.ts} +0 -0
  81. /package/dist/{index.sql → migrations/migrations.sql} +0 -0
  82. /package/src/{index.sql → migrations/migrations.sql} +0 -0
@@ -4,11 +4,7 @@ import path from 'path';
4
4
  import tmp from 'tmp-promise';
5
5
  import fs from 'fs-extra';
6
6
 
7
- import {
8
- readAndValidateMigrationsFromDirectory,
9
- sortMigrationFiles,
10
- getMigrationsToExecute,
11
- } from './index';
7
+ import { readAndValidateMigrationsFromDirectory, sortMigrationFiles } from './load-migrations';
12
8
 
13
9
  chai.use(chaiAsPromised);
14
10
 
@@ -24,12 +20,12 @@ async function withMigrationFiles(files: string[], fn: (tmpDir: string) => Promi
24
20
  );
25
21
  }
26
22
 
27
- describe('migrations', () => {
23
+ describe('load-migrations', () => {
28
24
  describe('readAndValidateMigrationsFromDirectory', () => {
29
25
  it('handles migrations without a timestamp', async () => {
30
26
  await withMigrationFiles(['001_testing.sql'], async (tmpDir) => {
31
27
  await assert.isRejected(
32
- readAndValidateMigrationsFromDirectory(tmpDir),
28
+ readAndValidateMigrationsFromDirectory(tmpDir, ['.sql']),
33
29
  'Invalid migration filename: 001_testing.sql'
34
30
  );
35
31
  });
@@ -40,7 +36,7 @@ describe('migrations', () => {
40
36
  ['20220101010101_testing.sql', '20220101010101_testing_again.sql'],
41
37
  async (tmpDir) => {
42
38
  await assert.isRejected(
43
- readAndValidateMigrationsFromDirectory(tmpDir),
39
+ readAndValidateMigrationsFromDirectory(tmpDir, ['.sql']),
44
40
  'Duplicate migration timestamp'
45
41
  );
46
42
  }
@@ -53,28 +49,34 @@ describe('migrations', () => {
53
49
  assert.deepEqual(
54
50
  sortMigrationFiles([
55
51
  {
52
+ directory: 'migrations',
56
53
  filename: '20220101010103_testing_3.sql',
57
54
  timestamp: '20220101010103',
58
55
  },
59
56
  {
57
+ directory: 'migrations',
60
58
  filename: '20220101010101_testing_1.sql',
61
59
  timestamp: '20220101010101',
62
60
  },
63
61
  {
62
+ directory: 'migrations',
64
63
  filename: '20220101010102_testing_2.sql',
65
64
  timestamp: '20220101010102',
66
65
  },
67
66
  ]),
68
67
  [
69
68
  {
69
+ directory: 'migrations',
70
70
  filename: '20220101010101_testing_1.sql',
71
71
  timestamp: '20220101010101',
72
72
  },
73
73
  {
74
+ directory: 'migrations',
74
75
  filename: '20220101010102_testing_2.sql',
75
76
  timestamp: '20220101010102',
76
77
  },
77
78
  {
79
+ directory: 'migrations',
78
80
  filename: '20220101010103_testing_3.sql',
79
81
  timestamp: '20220101010103',
80
82
  },
@@ -82,44 +84,4 @@ describe('migrations', () => {
82
84
  );
83
85
  });
84
86
  });
85
-
86
- describe('getMigrationsToExecute', () => {
87
- it('handles the case of no executed migrations', () => {
88
- const migrationFiles = [
89
- {
90
- filename: '001_testing.sql',
91
- timestamp: '20220101010101',
92
- },
93
- ];
94
- assert.deepEqual(getMigrationsToExecute(migrationFiles, []), migrationFiles);
95
- });
96
-
97
- it('handles case where subset of migrations have been executed', () => {
98
- const migrationFiles = [
99
- {
100
- filename: '20220101010101_testing_1.sql',
101
- timestamp: '20220101010101',
102
- },
103
- {
104
- filename: '20220101010102_testing_2.sql',
105
- timestamp: '20220101010102',
106
- },
107
- {
108
- filename: '20220101010103_testing_3.sql',
109
- timestamp: '20220101010103',
110
- },
111
- ];
112
- const executedMigrations = [
113
- {
114
- timestamp: '20220101010101',
115
- },
116
- {
117
- timestamp: '20220101010102',
118
- },
119
- ];
120
- assert.deepEqual(getMigrationsToExecute(migrationFiles, executedMigrations), [
121
- { timestamp: '20220101010103', filename: '20220101010103_testing_3.sql' },
122
- ]);
123
- });
124
- });
125
87
  });
@@ -0,0 +1,76 @@
1
+ import fs from 'fs-extra';
2
+
3
+ /**
4
+ * Timestamp prefixes will be of the form `YYYYMMDDHHMMSS`, which will have 14 digits.
5
+ * If this code is still around in the year 10000... good luck.
6
+ */
7
+ const MIGRATION_FILENAME_REGEX = /^([0-9]{14})_.+\.[a-z]+/;
8
+
9
+ export interface MigrationFile {
10
+ directory: string;
11
+ filename: string;
12
+ timestamp: string;
13
+ }
14
+
15
+ export async function readAndValidateMigrationsFromDirectory(
16
+ dir: string,
17
+ extensions: string[]
18
+ ): Promise<MigrationFile[]> {
19
+ const migrationFiles = (await fs.readdir(dir)).filter((m) =>
20
+ extensions.some((e) => m.endsWith(e))
21
+ );
22
+
23
+ const migrations = migrationFiles.map((mf) => {
24
+ const match = mf.match(MIGRATION_FILENAME_REGEX);
25
+
26
+ if (!match) {
27
+ throw new Error(`Invalid migration filename: ${mf}`);
28
+ }
29
+
30
+ const timestamp = match[1] ?? null;
31
+
32
+ if (timestamp === null) {
33
+ throw new Error(`Migration ${mf} does not have a timestamp`);
34
+ }
35
+
36
+ return {
37
+ directory: dir,
38
+ filename: mf,
39
+ timestamp,
40
+ };
41
+ });
42
+
43
+ // First pass: validate that all migrations have a unique timestamp prefix.
44
+ // This will avoid data loss and conflicts in unexpected scenarios.
45
+ const seenTimestamps = new Set();
46
+ for (const migration of migrations) {
47
+ const { filename, timestamp } = migration;
48
+
49
+ if (timestamp !== null) {
50
+ if (seenTimestamps.has(timestamp)) {
51
+ throw new Error(`Duplicate migration timestamp: ${timestamp} (${filename})`);
52
+ }
53
+ seenTimestamps.add(timestamp);
54
+ }
55
+ }
56
+
57
+ return migrations;
58
+ }
59
+
60
+ export async function readAndValidateMigrationsFromDirectories(
61
+ directories: string[],
62
+ extensions: string[]
63
+ ): Promise<MigrationFile[]> {
64
+ const allMigrations: MigrationFile[] = [];
65
+ for (const directory of directories) {
66
+ const migrations = await readAndValidateMigrationsFromDirectory(directory, extensions);
67
+ allMigrations.push(...migrations);
68
+ }
69
+ return allMigrations;
70
+ }
71
+
72
+ export function sortMigrationFiles(migrationFiles: MigrationFile[]): MigrationFile[] {
73
+ return migrationFiles.sort((a, b) => {
74
+ return a.timestamp.localeCompare(b.timestamp);
75
+ });
76
+ }
@@ -0,0 +1,2 @@
1
+ CREATE TABLE IF NOT EXISTS
2
+ users (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL);
@@ -0,0 +1,5 @@
1
+ import { queryAsync } from '@prairielearn/postgres';
2
+
3
+ module.exports = async function migrate() {
4
+ await queryAsync("INSERT INTO users (name) VALUES ('Test User')", {});
5
+ };
@@ -0,0 +1 @@
1
+ export { init } from './migrations';
@@ -0,0 +1,80 @@
1
+ import { assert } from 'chai';
2
+ import path from 'node:path';
3
+ import { makePostgresTestUtils, queryAsync } from '@prairielearn/postgres';
4
+
5
+ import { getMigrationsToExecute, initWithLock } from './migrations';
6
+
7
+ describe('migrations', () => {
8
+ describe('getMigrationsToExecute', () => {
9
+ it('handles the case of no executed migrations', () => {
10
+ const migrationFiles = [
11
+ {
12
+ directory: 'migrations',
13
+ filename: '001_testing.sql',
14
+ timestamp: '20220101010101',
15
+ },
16
+ ];
17
+ assert.deepEqual(getMigrationsToExecute(migrationFiles, []), migrationFiles);
18
+ });
19
+
20
+ it('handles case where subset of migrations have been executed', () => {
21
+ const migrationFiles = [
22
+ {
23
+ directory: 'migrations',
24
+ filename: '20220101010101_testing_1.sql',
25
+ timestamp: '20220101010101',
26
+ },
27
+ {
28
+ directory: 'migrations',
29
+ filename: '20220101010102_testing_2.sql',
30
+ timestamp: '20220101010102',
31
+ },
32
+ {
33
+ directory: 'migrations',
34
+ filename: '20220101010103_testing_3.sql',
35
+ timestamp: '20220101010103',
36
+ },
37
+ ];
38
+ const executedMigrations = [
39
+ {
40
+ timestamp: '20220101010101',
41
+ },
42
+ {
43
+ timestamp: '20220101010102',
44
+ },
45
+ ];
46
+ assert.deepEqual(getMigrationsToExecute(migrationFiles, executedMigrations), [
47
+ {
48
+ directory: 'migrations',
49
+ timestamp: '20220101010103',
50
+ filename: '20220101010103_testing_3.sql',
51
+ },
52
+ ]);
53
+ });
54
+ });
55
+
56
+ describe('initWithLock', () => {
57
+ const postgresTestUtils = makePostgresTestUtils({
58
+ database: 'prairielearn_migrations',
59
+ });
60
+
61
+ before(async () => {
62
+ await postgresTestUtils.createDatabase();
63
+ });
64
+
65
+ after(async () => {
66
+ await postgresTestUtils.dropDatabase();
67
+ });
68
+
69
+ it('runs both SQL and JavaScript migrations', async () => {
70
+ const migrationDir = path.join(__dirname, 'fixtures');
71
+ await initWithLock([migrationDir], 'prairielearn_migrations');
72
+
73
+ // If both migrations ran successfully, there should be a single user
74
+ // in the database.
75
+ const users = await queryAsync('SELECT * FROM users', {});
76
+ assert.lengthOf(users.rows, 1);
77
+ assert.equal(users.rows[0].name, 'Test User');
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,149 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import * as namedLocks from '@prairielearn/named-locks';
5
+ import { logger } from '@prairielearn/logger';
6
+ import * as sqldb from '@prairielearn/postgres';
7
+ import * as error from '@prairielearn/error';
8
+
9
+ import {
10
+ MigrationFile,
11
+ readAndValidateMigrationsFromDirectories,
12
+ sortMigrationFiles,
13
+ } from '../load-migrations';
14
+
15
+ const sql = sqldb.loadSqlEquiv(__filename);
16
+
17
+ export async function init(directories: string | string[], project: string) {
18
+ const migrationDirectories = Array.isArray(directories) ? directories : [directories];
19
+ const lockName = 'migrations';
20
+ logger.verbose(`Waiting for lock ${lockName}`);
21
+ await namedLocks.doWithLock(
22
+ lockName,
23
+ {
24
+ // Migrations *might* take a long time to run, so we'll enable automatic
25
+ // lock renewal so that our lock doesn't get killed by the Postgres
26
+ // idle session timeout.
27
+ //
28
+ // That said, we should generally try to keep migrations executing as
29
+ // quickly as possible. A long-running migration likely means that
30
+ // Postgres is locking a whole table, which is unacceptable in production.
31
+ autoRenew: true,
32
+ },
33
+ async () => {
34
+ logger.verbose(`Acquired lock ${lockName}`);
35
+ await initWithLock(migrationDirectories, project);
36
+ }
37
+ );
38
+ logger.verbose(`Released lock ${lockName}`);
39
+ }
40
+
41
+ export function getMigrationsToExecute(
42
+ migrationFiles: MigrationFile[],
43
+ executedMigrations: { timestamp: string | null }[]
44
+ ): MigrationFile[] {
45
+ // If no migrations have ever been run, run them all.
46
+ if (executedMigrations.length === 0) {
47
+ return migrationFiles;
48
+ }
49
+
50
+ const executedMigrationTimestamps = new Set(executedMigrations.map((m) => m.timestamp));
51
+ return migrationFiles.filter((m) => !executedMigrationTimestamps.has(m.timestamp));
52
+ }
53
+
54
+ export async function initWithLock(directories: string[], project: string) {
55
+ logger.verbose('Starting DB schema migration');
56
+
57
+ // Create the migrations table if needed
58
+ await sqldb.queryAsync(sql.create_migrations_table, {});
59
+
60
+ // Apply necessary changes to the migrations table as needed.
61
+ try {
62
+ await sqldb.queryAsync('SELECT project FROM migrations;', {});
63
+ } catch (err: any) {
64
+ if (err.routine === 'errorMissingColumn') {
65
+ logger.info('Altering migrations table');
66
+ await sqldb.queryAsync(sql.add_projects_column, {});
67
+ } else {
68
+ throw err;
69
+ }
70
+ }
71
+ try {
72
+ await sqldb.queryAsync('SELECT timestamp FROM migrations;', {});
73
+ } catch (err: any) {
74
+ if (err.routine === 'errorMissingColumn') {
75
+ logger.info('Altering migrations table again');
76
+ await sqldb.queryAsync(sql.add_timestamp_column, {});
77
+ } else {
78
+ throw err;
79
+ }
80
+ }
81
+
82
+ let allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });
83
+
84
+ const migrationFiles = await readAndValidateMigrationsFromDirectories(directories, [
85
+ '.sql',
86
+ '.js',
87
+ '.ts',
88
+ '.mjs',
89
+ ]);
90
+
91
+ // Validation: if we not all previously-executed migrations have timestamps,
92
+ // prompt the user to deploy an earlier version that includes both indexes
93
+ // and timestamps.
94
+ const migrationsMissingTimestamps = allMigrations.rows.filter((m) => !m.timestamp);
95
+ if (migrationsMissingTimestamps.length > 0) {
96
+ throw new Error(
97
+ [
98
+ 'The following migrations are missing timestamps:',
99
+ migrationsMissingTimestamps.map((m) => ` ${m.filename}`),
100
+ // This revision was the most recent commit to `master` before the
101
+ // code handling indexes was removed.
102
+ 'You must deploy revision 1aa43c7348fa24cf636413d720d06a2fa9e38ef2 first.',
103
+ ].join('\n')
104
+ );
105
+ }
106
+
107
+ // Refetch the list of migrations from the database.
108
+ allMigrations = await sqldb.queryAsync(sql.get_migrations, { project });
109
+
110
+ // Sort the migration files into execution order.
111
+ const sortedMigrationFiles = sortMigrationFiles(migrationFiles);
112
+
113
+ // Figure out which migrations have to be applied.
114
+ const migrationsToExecute = getMigrationsToExecute(sortedMigrationFiles, allMigrations.rows);
115
+
116
+ for (const { directory, filename, timestamp } of migrationsToExecute) {
117
+ if (allMigrations.rows.length === 0) {
118
+ // if we are running all the migrations then log at a lower level
119
+ logger.verbose(`Running migration ${filename}`);
120
+ } else {
121
+ logger.info(`Running migration ${filename}`);
122
+ }
123
+
124
+ const migrationPath = path.join(directory, filename);
125
+ if (filename.endsWith('.sql')) {
126
+ const migrationSql = await fs.readFile(migrationPath, 'utf8');
127
+ try {
128
+ await sqldb.queryAsync(migrationSql, {});
129
+ } catch (err) {
130
+ error.addData(err, { sqlFile: filename });
131
+ throw err;
132
+ }
133
+ } else {
134
+ const migrationModule = await import(migrationPath);
135
+ const implementation = migrationModule.default;
136
+ if (typeof implementation !== 'function') {
137
+ throw new Error(`Migration ${filename} does not export a default function`);
138
+ }
139
+ await implementation();
140
+ }
141
+
142
+ // Record the migration.
143
+ await sqldb.queryAsync(sql.insert_migration, {
144
+ filename: filename,
145
+ timestamp,
146
+ project,
147
+ });
148
+ }
149
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
File without changes
File without changes