-
-
Notifications
You must be signed in to change notification settings - Fork 496
/
Migrator.postgres.test.ts
449 lines (385 loc) · 20.6 KB
/
Migrator.postgres.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
(global as any).process.env.FORCE_COLOR = 0;
import { Umzug } from 'umzug';
import { format } from 'sql-formatter';
import { MetadataStorage, MikroORM } from '@mikro-orm/core';
import { Migration, MigrationStorage, Migrator, TSMigrationGenerator } from '@mikro-orm/migrations';
import type { DatabaseTable } from '@mikro-orm/postgresql';
import { DatabaseSchema, PostgreSqlDriver } from '@mikro-orm/postgresql';
import { remove } from 'fs-extra';
import { Address2, Author2, Book2, BookTag2, Configuration2, FooBar2, FooBaz2, FooParam2, Publisher2, Test2 } from '../../entities-sql';
import { BASE_DIR, mockLogger } from '../../bootstrap';
class MigrationTest1 extends Migration {
async up(): Promise<void> {
this.addSql('select 1 + 1');
}
}
class MigrationTest2 extends Migration {
async up(): Promise<void> {
this.addSql('select 1 + 1');
const knex = this.getKnex();
this.addSql(knex.raw('select 1 + 1'));
this.addSql(knex.select(knex.raw('2 + 2 as count2')));
const res = await this.execute('select 1 + 1 as count1');
expect(res).toEqual([{ count1: 2 }]);
}
isTransactional(): boolean {
return false;
}
}
describe('Migrator (postgres)', () => {
let orm: MikroORM<PostgreSqlDriver>;
beforeAll(async () => {
orm = await MikroORM.init<PostgreSqlDriver>({
entities: [Author2, Address2, Book2, BookTag2, Publisher2, Test2, FooBar2, FooBaz2, FooParam2, Configuration2],
dbName: `mikro_orm_test_migrations`,
driver: PostgreSqlDriver,
schema: 'custom',
logger: () => void 0,
migrations: { path: BASE_DIR + '/../temp/migrations', snapshot: false },
});
const schemaGenerator = orm.schema;
await schemaGenerator.refreshDatabase();
await schemaGenerator.execute('alter table "custom"."book2" add column "foo" varchar null default \'lol\';');
await schemaGenerator.execute('alter table "custom"."book2" alter column "double" type numeric using ("double"::numeric);');
await schemaGenerator.execute('alter table "custom"."test2" add column "path" polygon null default null;');
await remove(process.cwd() + '/temp/migrations');
});
beforeEach(() => orm.config.resetServiceCache());
afterAll(async () => orm.close(true));
test('generate js schema migration', async () => {
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
const migrationsSettings = orm.config.get('migrations');
orm.config.set('migrations', { ...migrationsSettings, emit: 'js' }); // Set migration type to js
const migrator = orm.migrator;
const migration = await migrator.createMigration();
expect(migration).toMatchSnapshot('migration-js-dump');
orm.config.set('migrations', migrationsSettings); // Revert migration config changes
await remove(process.cwd() + '/temp/migrations/' + migration.fileName);
});
test('generate migration with custom migrator', async () => {
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
const migrationsSettings = orm.config.get('migrations');
orm.config.set('migrations', { ...migrationsSettings, generator: class extends TSMigrationGenerator {
generateMigrationFile(className: string, diff: { up: string[]; down: string[] }): string {
const comment = '// this file was generated via custom migration generator\n\n';
return comment + super.generateMigrationFile(className, diff);
}
createStatement(sql: string, padLeft: number): string {
sql = format(sql, { language: 'postgresql' });
sql = sql.split('\n').map((l, i) => i === 0 ? l : `${' '.repeat(padLeft + 13)}${l}`).join('\n');
return super.createStatement(sql, padLeft);
}
} });
const migrator = orm.migrator;
const migration = await migrator.createMigration();
expect(migration).toMatchSnapshot('migration-ts-dump');
orm.config.set('migrations', migrationsSettings); // Revert migration config changes
await remove(process.cwd() + '/temp/migrations/' + migration.fileName);
});
test('generate migration with custom name', async () => {
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
const migrationsSettings = orm.config.get('migrations');
orm.config.set('migrations', { ...migrationsSettings, fileName: time => `migration-${time}` });
const migrator = orm.migrator;
const migration = await migrator.createMigration();
expect(migration).toMatchSnapshot('migration-dump');
const upMock = jest.spyOn(Umzug.prototype, 'up');
upMock.mockImplementation(() => void 0 as any);
const downMock = jest.spyOn(Umzug.prototype, 'down');
downMock.mockImplementation(() => void 0 as any);
await migrator.up();
await migrator.down(migration.fileName.replace('.ts', ''));
await migrator.up();
await migrator.down(migration.fileName);
await migrator.up();
await migrator.down(migration.fileName.replace('migration-', '').replace('.ts', ''));
orm.config.set('migrations', migrationsSettings); // Revert migration config changes
await remove(process.cwd() + '/temp/migrations/' + migration.fileName);
upMock.mockRestore();
downMock.mockRestore();
});
test('generate migration with custom name with name option', async () => {
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
const migrationsSettings = orm.config.get('migrations');
orm.config.set('migrations', { ...migrationsSettings, fileName: (time, name) => `migration${time}_${name}` });
const migrator = orm.migrator;
const migration = await migrator.createMigration(undefined, false, false, 'custom_name');
expect(migration).toMatchSnapshot('migration-dump');
expect(migration.fileName).toEqual('migration20191013214813_custom_name.ts');
const upMock = jest.spyOn(Umzug.prototype, 'up');
upMock.mockImplementation(() => void 0 as any);
const downMock = jest.spyOn(Umzug.prototype, 'down');
downMock.mockImplementation(() => void 0 as any);
await migrator.up();
await migrator.down(migration.fileName.replace('.ts', ''));
await migrator.up();
await migrator.down(migration.fileName);
await migrator.up();
orm.config.set('migrations', migrationsSettings); // Revert migration config changes
await remove(process.cwd() + '/temp/migrations/' + migration.fileName);
upMock.mockRestore();
downMock.mockRestore();
});
test('generate schema migration', async () => {
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
const migrator = orm.migrator;
const migration = await migrator.createMigration();
expect(migration).toMatchSnapshot('migration-dump');
await remove(process.cwd() + '/temp/migrations/' + migration.fileName);
});
test('generate migration with snapshot', async () => {
const migrations = orm.config.get('migrations');
migrations.snapshot = true;
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
const migrator = orm.migrator;
const migration1 = await migrator.createMigration();
expect(migration1).toMatchSnapshot('migration-snapshot-dump-1');
await remove(process.cwd() + '/temp/migrations/' + migration1.fileName);
// will use the snapshot, so should be empty
const migration2 = await migrator.createMigration();
expect(migration2.diff).toEqual({ down: [], up: [] });
expect(migration2).toMatchSnapshot('migration-snapshot-dump-2');
migrations.snapshot = false;
});
test('generate initial migration', async () => {
await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!).withSchema('custom');
const getExecutedMigrationsMock = jest.spyOn<any, any>(Migrator.prototype, 'getExecutedMigrations');
const getPendingMigrationsMock = jest.spyOn<any, any>(Migrator.prototype, 'getPendingMigrations');
getExecutedMigrationsMock.mockResolvedValueOnce(['test.ts']);
const migrator = orm.migrator;
const err = 'Initial migration cannot be created, as some migrations already exist';
await expect(migrator.createMigration(undefined, false, true)).rejects.toThrowError(err);
getExecutedMigrationsMock.mockResolvedValueOnce([]);
const logMigrationMock = jest.spyOn<any, any>(MigrationStorage.prototype, 'logMigration');
logMigrationMock.mockImplementationOnce(i => i);
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValue('2019-10-13T21:48:13.382Z');
const metadataMock = jest.spyOn(MetadataStorage.prototype, 'getAll');
const schemaMock = jest.spyOn(DatabaseSchema.prototype, 'getTables');
schemaMock.mockReturnValueOnce([
{ name: 'author2', schema: 'custom' } as DatabaseTable,
{ name: 'book2', schema: 'custom' } as DatabaseTable,
]);
getPendingMigrationsMock.mockResolvedValueOnce([]);
const err2 = `Some tables already exist in your schema, remove them first to create the initial migration: custom.author2, custom.book2`;
await expect(migrator.createInitialMigration(undefined)).rejects.toThrowError(err2);
metadataMock.mockReturnValueOnce({});
const err3 = `No entities found`;
await expect(migrator.createInitialMigration(undefined)).rejects.toThrowError(err3);
schemaMock.mockReturnValueOnce([]);
getPendingMigrationsMock.mockResolvedValueOnce([]);
const migration1 = await migrator.createInitialMigration(undefined);
expect(logMigrationMock).not.toBeCalledWith('Migration20191013214813.ts');
expect(migration1).toMatchSnapshot('initial-migration-dump');
await remove(process.cwd() + '/temp/migrations/' + migration1.fileName);
await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!).withSchema('custom');
const migration2 = await migrator.createInitialMigration(undefined);
expect(logMigrationMock).toBeCalledWith({ name: 'Migration20191013214813.ts', context: null });
expect(migration2).toMatchSnapshot('initial-migration-dump');
await remove(process.cwd() + '/temp/migrations/' + migration2.fileName);
});
test('migration storage getter', async () => {
const migrator = orm.migrator;
expect(migrator.getStorage()).toBeInstanceOf(MigrationStorage);
expect(migrator.getStorage().getTableName!()).toEqual({
schemaName: 'custom',
tableName: 'mikro_orm_migrations',
});
// @ts-expect-error private property
migrator.options.tableName = 'custom.mikro_orm_migrations';
expect(migrator.getStorage().getTableName!()).toEqual({
schemaName: 'custom',
tableName: 'mikro_orm_migrations',
});
// @ts-expect-error private property
migrator.options.tableName = 'mikro_orm_migrations';
});
test('migration is skipped when no diff', async () => {
const migrator = orm.migrator;
const getSchemaDiffMock = jest.spyOn<any, any>(Migrator.prototype, 'getSchemaDiff');
getSchemaDiffMock.mockResolvedValueOnce({ up: [], down: [] });
const migration = await migrator.createMigration();
expect(migration).toEqual({ fileName: '', code: '', diff: { up: [], down: [] } });
});
test('run schema migration', async () => {
const upMock = jest.spyOn(Umzug.prototype, 'up');
const downMock = jest.spyOn(Umzug.prototype, 'down');
upMock.mockImplementationOnce(() => void 0 as any);
downMock.mockImplementationOnce(() => void 0 as any);
const migrator = orm.migrator;
await migrator.up();
expect(upMock).toBeCalledTimes(1);
expect(downMock).toBeCalledTimes(0);
await orm.em.begin();
await migrator.down({ transaction: orm.em.getTransactionContext() });
await orm.em.commit();
expect(upMock).toBeCalledTimes(1);
expect(downMock).toBeCalledTimes(1);
upMock.mockRestore();
});
test('run schema migration without existing migrations folder (GH #907)', async () => {
await remove(process.cwd() + '/temp/migrations');
const migrator = orm.migrator;
await migrator.up();
});
test('ensureTable and list executed migrations', async () => {
await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!).withSchema('custom');
const migrator = orm.migrator;
const storage = migrator.getStorage();
await storage.ensureTable!(); // creates the table
await storage.logMigration({ name: 'test', context: null });
await expect(storage.getExecutedMigrations()).resolves.toMatchObject([{ name: 'test' }]);
await expect(storage.executed()).resolves.toEqual(['test']);
await storage.ensureTable!(); // table exists, no-op
await storage.unlogMigration({ name: 'test', context: null });
await expect(storage.executed()).resolves.toEqual([]);
await expect(migrator.getPendingMigrations()).resolves.toEqual([]);
});
test('runner', async () => {
await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!).withSchema('custom');
const migrator = orm.migrator;
await migrator.getStorage().ensureTable!();
// @ts-ignore
const runner = migrator.runner;
const mock = mockLogger(orm, ['query']);
const migration1 = new MigrationTest1(orm.em.getDriver(), orm.config);
const spy1 = jest.spyOn(Migration.prototype, 'addSql');
mock.mock.calls.length = 0;
await runner.run(migration1, 'up');
expect(spy1).toBeCalledWith('select 1 + 1');
expect(mock.mock.calls.length).toBe(6);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('set names \'utf8\';');
expect(mock.mock.calls[2][0]).toMatch('set session_replication_role = \'replica\';');
expect(mock.mock.calls[3][0]).toMatch('select 1 + 1');
expect(mock.mock.calls[4][0]).toMatch('set session_replication_role = \'origin\';');
expect(mock.mock.calls[5][0]).toMatch('commit');
mock.mock.calls.length = 0;
await expect(runner.run(migration1, 'down')).rejects.toThrowError('This migration cannot be reverted');
const executed = await migrator.getExecutedMigrations();
expect(executed).toEqual([]);
mock.mock.calls.length = 0;
// @ts-ignore
migrator.options.disableForeignKeys = false;
const migration2 = new MigrationTest2(orm.em.getDriver(), orm.config);
await runner.run(migration2, 'up');
expect(mock.mock.calls.length).toBe(4);
expect(mock.mock.calls[0][0]).toMatch('select 1 + 1 as count1');
expect(mock.mock.calls[1][0]).toMatch('select 1 + 1');
expect(mock.mock.calls[2][0]).toMatch('select 1 + 1');
expect(mock.mock.calls[3][0]).toMatch('select 2 + 2 as count2');
});
test('up/down params [all or nothing enabled]', async () => {
await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!).withSchema('custom').withSchema('custom');
const migrator = orm.migrator;
// @ts-ignore
migrator.options.disableForeignKeys = false;
const path = process.cwd() + '/temp/migrations';
const migration = await migrator.createMigration(path, true);
const migratorMock = jest.spyOn(Migration.prototype, 'down');
migratorMock.mockImplementation(async () => void 0);
const mock = mockLogger(orm, ['query']);
await migrator.up(migration.fileName);
await migrator.down(migration.fileName.replace('Migration', '').replace('.ts', ''));
await migrator.up({ migrations: [migration.fileName] });
await migrator.down({ from: 0, to: 0 } as any);
await migrator.up({ to: migration.fileName });
await migrator.up({ from: migration.fileName } as any);
await migrator.down();
await remove(path + '/' + migration.fileName);
const calls = mock.mock.calls.map(call => {
return call[0]
.replace(/ \[took \d+ ms]/, '')
.replace(/\[query] /, '')
.replace(/ trx\d+/, 'trx\\d+');
});
expect(calls).toMatchSnapshot('all-or-nothing');
});
test('up/down with explicit transaction', async () => {
await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!).withSchema('custom').withSchema('custom');
const migrator = orm.migrator;
const path = process.cwd() + '/temp/migrations';
// @ts-ignore
migrator.options.disableForeignKeys = false;
const dateMock = jest.spyOn(Date.prototype, 'toISOString');
dateMock.mockReturnValueOnce('2020-09-22T10:00:01.000Z');
dateMock.mockReturnValueOnce('2020-09-22T10:00:02.000Z');
const migration1 = await migrator.createMigration(path, true);
const migration2 = await migrator.createMigration(path, true);
const migrationMock = jest.spyOn(Migration.prototype, 'down');
migrationMock.mockImplementation(async () => void 0);
const mock = mockLogger(orm, ['query']);
await orm.em.transactional(async em => {
const ret1 = await migrator.up({ transaction: em.getTransactionContext() });
const ret2 = await migrator.down({ transaction: em.getTransactionContext() });
const ret3 = await migrator.down({ transaction: em.getTransactionContext() });
const ret4 = await migrator.down({ transaction: em.getTransactionContext() });
expect(ret1).toHaveLength(2);
expect(ret2).toHaveLength(1);
expect(ret3).toHaveLength(1);
expect(ret4).toHaveLength(0);
});
await remove(path + '/' + migration1.fileName);
await remove(path + '/' + migration2.fileName);
const calls = mock.mock.calls.map(call => {
return call[0]
.replace(/ \[took \d+ ms]/, '')
.replace(/\[query] /, '')
.replace(/ trx\d+/, 'trx_xx');
});
expect(calls).toMatchSnapshot('explicit-tx');
});
test('up/down params [all or nothing disabled]', async () => {
await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!).withSchema('custom');
const migrator = orm.migrator;
// @ts-ignore
migrator.options.disableForeignKeys = false;
// @ts-ignore
migrator.options.allOrNothing = false;
const path = process.cwd() + '/temp/migrations';
const migration = await migrator.createMigration(path, true);
const migratorMock = jest.spyOn(Migration.prototype, 'down');
migratorMock.mockImplementation(async () => void 0);
const mock = mockLogger(orm, ['query']);
await migrator.up(migration.fileName);
await migrator.down(migration.fileName.replace('Migration', ''));
await migrator.up({ migrations: [migration.fileName] });
await migrator.down({ from: 0, to: 0 } as any);
await migrator.up({ to: migration.fileName });
await migrator.up({ from: migration.fileName } as any);
await migrator.down();
await remove(path + '/' + migration.fileName);
const calls = mock.mock.calls.map(call => {
return call[0]
.replace(/ \[took \d+ ms]/, '')
.replace(/\[query] /, '')
.replace(/ trx\d+/, 'trx_xx');
});
expect(calls).toMatchSnapshot('all-or-nothing-disabled');
});
});
test('ensureTable when the schema does not exist', async () => {
const orm = await MikroORM.init<PostgreSqlDriver>({
entities: [Author2, Address2, Book2, BookTag2, Publisher2, Test2, FooBar2, FooBaz2, FooParam2, Configuration2],
dbName: `mikro_orm_test_migrations2`,
driver: PostgreSqlDriver,
schema: 'custom2',
migrations: { path: BASE_DIR + '/../temp/migrations', snapshot: false },
});
await orm.schema.ensureDatabase();
await orm.schema.execute('drop schema if exists "custom2" cascade');
const storage = orm.migrator.getStorage();
const mock = mockLogger(orm);
await storage.ensureTable!(); // ensures the schema first
expect(mock.mock.calls[0][0]).toMatch(`select table_name, table_schema as schema_name, (select pg_catalog.obj_description(c.oid) from pg_catalog.pg_class c where c.oid = (select ('"' || table_schema || '"."' || table_name || '"')::regclass::oid) and c.relname = table_name) as table_comment from information_schema.tables where "table_schema" not like 'pg_%' and "table_schema" not like 'crdb_%' and "table_schema" not like '_timescaledb_%' and "table_schema" not in ('information_schema', 'tiger', 'topology') and table_name != 'geometry_columns' and table_name != 'spatial_ref_sys' and table_type != 'VIEW' order by table_name`);
expect(mock.mock.calls[1][0]).toMatch(`select schema_name from information_schema.schemata where "schema_name" not like 'pg_%' and "schema_name" not like 'crdb_%' and "schema_name" not like '_timescaledb_%' and "schema_name" not in ('information_schema', 'tiger', 'topology') order by schema_name`);
expect(mock.mock.calls[2][0]).toMatch(`create schema "custom2"`);
expect(mock.mock.calls[3][0]).toMatch(`create table "custom2"."mikro_orm_migrations" ("id" serial primary key, "name" varchar(255), "executed_at" timestamptz default current_timestamp)`);
await orm.close();
});