rake-db 2.3.26 → 2.3.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rake-db",
3
- "version": "2.3.26",
3
+ "version": "2.3.28",
4
4
  "description": "Migrations tool for Postgresql DB",
5
5
  "homepage": "https://orchid-orm.netlify.app/guide/migration-setup-and-overview.html",
6
6
  "repository": {
@@ -1,4 +1,4 @@
1
- import { migrate, rollback } from './migrateOrRollback';
1
+ import { changeCache, migrate, rollback } from './migrateOrRollback';
2
2
  import { createSchemaMigrations, migrationConfigDefaults } from '../common';
3
3
  import { getMigrationFiles } from '../common';
4
4
  import { Adapter, noop, TransactionAdapter } from 'pqb';
@@ -33,6 +33,7 @@ Adapter.prototype.transaction = (cb) => {
33
33
 
34
34
  const transactionQueryMock = jest.fn();
35
35
  TransactionAdapter.prototype.query = transactionQueryMock;
36
+ TransactionAdapter.prototype.arrays = transactionQueryMock;
36
37
 
37
38
  const importMock = jest.fn();
38
39
  const config = {
@@ -67,6 +68,9 @@ describe('migrateOrRollback', () => {
67
68
  beforeEach(() => {
68
69
  jest.clearAllMocks();
69
70
  importMock.mockImplementation(() => undefined);
71
+ for (const key in changeCache) {
72
+ delete changeCache[key];
73
+ }
70
74
  });
71
75
 
72
76
  describe('migrate', () => {
@@ -176,6 +180,25 @@ describe('migrateOrRollback', () => {
176
180
 
177
181
  expect(appCodeUpdater).toBeCalled();
178
182
  });
183
+
184
+ it('should call multiple change callbacks from top to bottom', async () => {
185
+ migrationFiles = [files[0]];
186
+ migratedVersions = [];
187
+
188
+ const called: string[] = [];
189
+ importMock.mockImplementation(() => {
190
+ change(async () => {
191
+ called.push('one');
192
+ });
193
+ change(async () => {
194
+ called.push('two');
195
+ });
196
+ });
197
+
198
+ await migrate(options, config, []);
199
+
200
+ expect(called).toEqual(['one', 'two']);
201
+ });
179
202
  });
180
203
 
181
204
  describe('rollback', () => {
@@ -221,5 +244,24 @@ describe('migrateOrRollback', () => {
221
244
  expect(transactionQueryMock).not.toBeCalled();
222
245
  expect(config.logger.log).not.toBeCalled();
223
246
  });
247
+
248
+ it('should call multiple change callbacks from top to bottom', async () => {
249
+ migrationFiles = [files[0]];
250
+ migratedVersions = [files[0].version];
251
+
252
+ const called: string[] = [];
253
+ importMock.mockImplementation(() => {
254
+ change(async () => {
255
+ called.push('one');
256
+ });
257
+ change(async () => {
258
+ called.push('two');
259
+ });
260
+ });
261
+
262
+ await rollback(options, config, []);
263
+
264
+ expect(called).toEqual(['two', 'one']);
265
+ });
224
266
  });
225
267
  });
@@ -15,19 +15,16 @@ import {
15
15
  quoteWithSchema,
16
16
  } from '../common';
17
17
  import {
18
- getCurrentPromise,
19
- setCurrentMigrationUp,
20
- setCurrentMigration,
18
+ clearChanges,
21
19
  ChangeCallback,
22
- change,
23
- getCurrentChangeCallback,
20
+ getCurrentChanges,
24
21
  } from '../migration/change';
25
22
  import { createMigrationInterface } from '../migration/migration';
26
23
  import { pathToFileURL } from 'url';
27
24
 
28
25
  const getDb = (adapter: Adapter) => createDb({ adapter });
29
26
 
30
- const migrateOrRollback = async (
27
+ export const migrateOrRollback = async (
31
28
  options: MaybeArray<AdapterOptions>,
32
29
  config: RakeDbConfig,
33
30
  args: string[],
@@ -99,7 +96,7 @@ const migrateOrRollback = async (
99
96
  }
100
97
  };
101
98
 
102
- const changeCache: Record<string, ChangeCallback | undefined> = {};
99
+ export const changeCache: Record<string, ChangeCallback[] | undefined> = {};
103
100
 
104
101
  const processMigration = async (
105
102
  db: Adapter,
@@ -117,18 +114,19 @@ const processMigration = async (
117
114
  options,
118
115
  appCodeUpdaterCache,
119
116
  );
120
- setCurrentMigration(db);
121
- setCurrentMigrationUp(up);
117
+ clearChanges();
122
118
 
123
- const callback = changeCache[file.path];
124
- if (callback) {
125
- change(callback);
126
- } else {
119
+ let changes = changeCache[file.path];
120
+ if (!changes) {
127
121
  await config.import(pathToFileURL(file.path).pathname);
128
- changeCache[file.path] = getCurrentChangeCallback();
122
+ changes = getCurrentChanges();
123
+ changeCache[file.path] = changes;
124
+ }
125
+
126
+ for (const fn of up ? changes : changes.reverse()) {
127
+ await fn(db, up);
129
128
  }
130
129
 
131
- await getCurrentPromise();
132
130
  await (up ? saveMigratedVersion : removeMigratedVersion)(
133
131
  db.adapter,
134
132
  file.version,
package/src/common.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  AdapterOptions,
4
4
  DbResult,
5
5
  DefaultColumnTypes,
6
+ EnumColumn,
6
7
  NoPrimaryKeyOption,
7
8
  QueryLogOptions,
8
9
  singleQuote,
@@ -11,6 +12,7 @@ import path from 'path';
11
12
  import { readdir } from 'fs/promises';
12
13
  import { RakeDbAst } from './ast';
13
14
  import prompts from 'prompts';
15
+ import { TableQuery } from './migration/createTable';
14
16
 
15
17
  type Db = DbResult<DefaultColumnTypes>;
16
18
 
@@ -335,3 +337,17 @@ export const quoteSchemaTable = ({
335
337
  }) => {
336
338
  return singleQuote(schema ? `${schema}.${name}` : name);
337
339
  };
340
+
341
+ export const makePopulateEnumQuery = (item: EnumColumn): TableQuery => {
342
+ const [schema, name] = getSchemaAndTableFromName(item.enumName);
343
+ return {
344
+ text: `SELECT unnest(enum_range(NULL::${quoteWithSchema({
345
+ schema,
346
+ name,
347
+ })}))::text`,
348
+ then(result) {
349
+ // populate empty options array with values from db
350
+ item.options.push(...result.rows.map(([value]) => value));
351
+ },
352
+ };
353
+ };
package/src/errors.ts ADDED
@@ -0,0 +1,3 @@
1
+ export class RakeDbError extends Error {}
2
+
3
+ export class NoPrimaryKey extends RakeDbError {}
@@ -0,0 +1,16 @@
1
+ import { change, clearChanges, getCurrentChanges } from './change';
2
+
3
+ describe('change', () => {
4
+ const fn = async () => {};
5
+
6
+ it('should push callback to currentChanges', () => {
7
+ change(fn);
8
+ expect(getCurrentChanges()).toEqual([fn]);
9
+ });
10
+
11
+ it('should clear changes', () => {
12
+ getCurrentChanges().push(fn);
13
+ clearChanges();
14
+ expect(getCurrentChanges()).toEqual([]);
15
+ });
16
+ });
@@ -1,26 +1,15 @@
1
1
  import { Migration } from './migration';
2
2
 
3
- let currentMigration: Migration | undefined;
4
- let currentPromise: Promise<void> | undefined;
5
- let currentUp = true;
6
- let currentChangeCallback: ChangeCallback | undefined;
3
+ const currentChanges: ChangeCallback[] = [];
7
4
 
8
5
  export type ChangeCallback = (db: Migration, up: boolean) => Promise<void>;
9
6
 
10
7
  export const change = (fn: ChangeCallback) => {
11
- if (!currentMigration) throw new Error('Database instance is not set');
12
- currentPromise = fn(currentMigration, currentUp);
13
- currentChangeCallback = fn;
8
+ currentChanges.push(fn);
14
9
  };
15
10
 
16
- export const setCurrentMigration = (db: Migration) => {
17
- currentMigration = db;
11
+ export const clearChanges = () => {
12
+ currentChanges.length = 0;
18
13
  };
19
14
 
20
- export const setCurrentMigrationUp = (up: boolean) => {
21
- currentUp = up;
22
- };
23
-
24
- export const getCurrentPromise = () => currentPromise;
25
-
26
- export const getCurrentChangeCallback = () => currentChangeCallback;
15
+ export const getCurrentChanges = () => currentChanges;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ asMock,
2
3
  expectSql,
3
4
  getDb,
4
5
  queryMock,
@@ -71,27 +72,8 @@ describe('changeTable', () => {
71
72
  dropCascade: t[action](t.text(), { dropMode: 'CASCADE' }),
72
73
  nullable: t[action](t.text().nullable()),
73
74
  nonNullable: t[action](t.text()),
74
- enum: t[action](t.enum('mood')),
75
75
  withDefault: t[action](t.boolean().default(false)),
76
76
  withDefaultRaw: t[action](t.date().default(t.raw(`now()`))),
77
- withIndex: t[action](
78
- t.text().index({
79
- name: 'indexName',
80
- unique: true,
81
- using: 'gin',
82
- collate: 'utf-8',
83
- opclass: 'opclass',
84
- order: 'ASC',
85
- include: 'id',
86
- with: 'fillfactor = 70',
87
- tablespace: 'tablespace',
88
- where: 'column = 123',
89
- }),
90
- ),
91
- uniqueColumn: t[action](t.text().unique({ dropMode: 'CASCADE' })),
92
- columnWithComment: t[action](
93
- t.text().comment('this is a column comment'),
94
- ),
95
77
  varcharWithLength: t[action](t.varchar(20)),
96
78
  decimalWithPrecisionAndScale: t[action](t.decimal(10, 5)),
97
79
  columnWithCompression: t[action](t.text().compression('compression')),
@@ -116,12 +98,8 @@ describe('changeTable', () => {
116
98
  ADD COLUMN "dropCascade" text NOT NULL,
117
99
  ADD COLUMN "nullable" text,
118
100
  ADD COLUMN "nonNullable" text NOT NULL,
119
- ADD COLUMN "enum" "mood" NOT NULL,
120
101
  ADD COLUMN "withDefault" boolean NOT NULL DEFAULT false,
121
102
  ADD COLUMN "withDefaultRaw" date NOT NULL DEFAULT now(),
122
- ADD COLUMN "withIndex" text NOT NULL,
123
- ADD COLUMN "uniqueColumn" text NOT NULL,
124
- ADD COLUMN "columnWithComment" text NOT NULL,
125
103
  ADD COLUMN "varcharWithLength" varchar(20) NOT NULL,
126
104
  ADD COLUMN "decimalWithPrecisionAndScale" decimal(10, 5) NOT NULL,
127
105
  ADD COLUMN "columnWithCompression" text COMPRESSION compression NOT NULL,
@@ -130,22 +108,6 @@ describe('changeTable', () => {
130
108
  ADD COLUMN "createdAt" timestamp NOT NULL DEFAULT now(),
131
109
  ADD COLUMN "updatedAt" timestamp NOT NULL DEFAULT now()
132
110
  `,
133
- toLine(`
134
- CREATE UNIQUE INDEX "indexName"
135
- ON "table"
136
- USING gin
137
- ("withIndex" COLLATE 'utf-8' opclass ASC)
138
- INCLUDE ("id")
139
- WITH (fillfactor = 70)
140
- TABLESPACE tablespace
141
- WHERE column = 123
142
- `),
143
- toLine(`
144
- CREATE UNIQUE INDEX "table_uniqueColumn_idx"
145
- ON "table"
146
- ("uniqueColumn")
147
- `),
148
- `COMMENT ON COLUMN "table"."columnWithComment" IS 'this is a column comment'`,
149
111
  ]);
150
112
  };
151
113
 
@@ -157,12 +119,8 @@ describe('changeTable', () => {
157
119
  DROP COLUMN "dropCascade" CASCADE,
158
120
  DROP COLUMN "nullable",
159
121
  DROP COLUMN "nonNullable",
160
- DROP COLUMN "enum",
161
122
  DROP COLUMN "withDefault",
162
123
  DROP COLUMN "withDefaultRaw",
163
- DROP COLUMN "withIndex",
164
- DROP COLUMN "uniqueColumn",
165
- DROP COLUMN "columnWithComment",
166
124
  DROP COLUMN "varcharWithLength",
167
125
  DROP COLUMN "decimalWithPrecisionAndScale",
168
126
  DROP COLUMN "columnWithCompression",
@@ -171,20 +129,205 @@ describe('changeTable', () => {
171
129
  DROP COLUMN "createdAt",
172
130
  DROP COLUMN "updatedAt"
173
131
  `,
174
- toLine(`DROP INDEX "indexName"`),
175
- toLine(`DROP INDEX "table_uniqueColumn_idx" CASCADE`),
176
132
  ]);
177
133
  };
178
134
 
135
+ asMock(queryMock).mockResolvedValue({ rows: [['one'], ['two']] });
136
+
179
137
  await fn();
180
138
  (action === 'add' ? expectAddColumns : expectRemoveColumns)();
181
139
 
182
140
  queryMock.mockClear();
183
141
  db.up = false;
142
+
184
143
  await fn();
144
+
185
145
  (action === 'add' ? expectRemoveColumns : expectAddColumns)();
186
146
  });
187
147
 
148
+ it(`should ${action} index`, async () => {
149
+ const fn = () => {
150
+ return db.changeTable('table', (t) => ({
151
+ withIndex: t[action](
152
+ t.text().index({
153
+ name: 'indexName',
154
+ unique: true,
155
+ using: 'gin',
156
+ collate: 'utf-8',
157
+ opclass: 'opclass',
158
+ order: 'ASC',
159
+ include: 'id',
160
+ with: 'fillfactor = 70',
161
+ tablespace: 'tablespace',
162
+ where: 'column = 123',
163
+ }),
164
+ ),
165
+ }));
166
+ };
167
+
168
+ const expectAdd = () => {
169
+ expectSql([
170
+ `ALTER TABLE "table"
171
+ ADD COLUMN "withIndex" text NOT NULL`,
172
+ toLine(`
173
+ CREATE UNIQUE INDEX "indexName"
174
+ ON "table"
175
+ USING gin
176
+ ("withIndex" COLLATE 'utf-8' opclass ASC)
177
+ INCLUDE ("id")
178
+ WITH (fillfactor = 70)
179
+ TABLESPACE tablespace
180
+ WHERE column = 123
181
+ `),
182
+ ]);
183
+ };
184
+
185
+ const expectRemove = () => {
186
+ expectSql([
187
+ `ALTER TABLE "table"
188
+ DROP COLUMN "withIndex"`,
189
+ toLine(`DROP INDEX "indexName"`),
190
+ ]);
191
+ };
192
+
193
+ asMock(queryMock).mockResolvedValue({ rows: [['one'], ['two']] });
194
+
195
+ await fn();
196
+ (action === 'add' ? expectAdd : expectRemove)();
197
+
198
+ queryMock.mockClear();
199
+ db.up = false;
200
+
201
+ await fn();
202
+
203
+ (action === 'add' ? expectRemove : expectAdd)();
204
+ });
205
+
206
+ it(`should ${action} unique index`, async () => {
207
+ const fn = () => {
208
+ return db.changeTable('table', (t) => ({
209
+ uniqueColumn: t[action](t.text().unique({ dropMode: 'CASCADE' })),
210
+ }));
211
+ };
212
+
213
+ const expectAdd = () => {
214
+ expectSql([
215
+ `ALTER TABLE "table"
216
+ ADD COLUMN "uniqueColumn" text NOT NULL`,
217
+ toLine(`
218
+ CREATE UNIQUE INDEX "table_uniqueColumn_idx"
219
+ ON "table"
220
+ ("uniqueColumn")
221
+ `),
222
+ ]);
223
+ };
224
+
225
+ const expectRemove = () => {
226
+ expectSql([
227
+ `ALTER TABLE "table"
228
+ DROP COLUMN "uniqueColumn"`,
229
+ toLine(`DROP INDEX "table_uniqueColumn_idx" CASCADE`),
230
+ ]);
231
+ };
232
+
233
+ asMock(queryMock).mockResolvedValue({ rows: [['one'], ['two']] });
234
+
235
+ await fn();
236
+ (action === 'add' ? expectAdd : expectRemove)();
237
+
238
+ queryMock.mockClear();
239
+ db.up = false;
240
+
241
+ await fn();
242
+
243
+ (action === 'add' ? expectRemove : expectAdd)();
244
+ });
245
+
246
+ it(`should ${action} column comment`, async () => {
247
+ const fn = () => {
248
+ return db.changeTable('table', (t) => ({
249
+ columnWithComment: t[action](
250
+ t.text().comment('this is a column comment'),
251
+ ),
252
+ }));
253
+ };
254
+
255
+ const expectAdd = () => {
256
+ expectSql([
257
+ `ALTER TABLE "table"
258
+ ADD COLUMN "columnWithComment" text NOT NULL`,
259
+ `COMMENT ON COLUMN "table"."columnWithComment" IS 'this is a column comment'`,
260
+ ]);
261
+ };
262
+
263
+ const expectRemove = () => {
264
+ expectSql(
265
+ `ALTER TABLE "table"
266
+ DROP COLUMN "columnWithComment"`,
267
+ );
268
+ };
269
+
270
+ asMock(queryMock).mockResolvedValue({ rows: [['one'], ['two']] });
271
+
272
+ await fn();
273
+ (action === 'add' ? expectAdd : expectRemove)();
274
+
275
+ queryMock.mockClear();
276
+ db.up = false;
277
+
278
+ await fn();
279
+
280
+ (action === 'add' ? expectRemove : expectAdd)();
281
+ });
282
+
283
+ it(`should ${action} enum`, async () => {
284
+ const fn = () => {
285
+ return db.changeTable('table', (t) => ({
286
+ enum: t[action](t.enum('mood')),
287
+ }));
288
+ };
289
+
290
+ const expectAdd = () => {
291
+ expectSql([
292
+ 'SELECT unnest(enum_range(NULL::"mood"))::text',
293
+ `
294
+ ALTER TABLE "table"
295
+ ADD COLUMN "enum" "mood" NOT NULL
296
+ `,
297
+ ]);
298
+ };
299
+
300
+ const expectRemove = () => {
301
+ expectSql([
302
+ 'SELECT unnest(enum_range(NULL::"mood"))::text',
303
+ `
304
+ ALTER TABLE "table"
305
+ DROP COLUMN "enum"
306
+ `,
307
+ ]);
308
+ };
309
+
310
+ asMock(queryMock).mockResolvedValue({ rows: [['one'], ['two']] });
311
+
312
+ await fn();
313
+
314
+ (action === 'add' ? expectAdd : expectRemove)();
315
+
316
+ const [{ ast: ast1 }] = asMock(db.options.appCodeUpdater).mock.calls[0];
317
+ expect(ast1.shape.enum.item.options).toEqual(['one', 'two']);
318
+
319
+ queryMock.mockClear();
320
+ asMock(db.options.appCodeUpdater).mockClear();
321
+ db.up = false;
322
+
323
+ await fn();
324
+
325
+ (action === 'add' ? expectRemove : expectAdd)();
326
+
327
+ const [{ ast: ast2 }] = asMock(db.options.appCodeUpdater).mock.calls[0];
328
+ expect(ast2.shape.enum.item.options).toEqual(['one', 'two']);
329
+ });
330
+
188
331
  it(`should ${action} columns with a primary key`, async () => {
189
332
  const fn = () => {
190
333
  return db.changeTable('table', (t) => ({
@@ -406,7 +549,7 @@ describe('changeTable', () => {
406
549
  });
407
550
  });
408
551
 
409
- it('should change column', async () => {
552
+ describe('column change', () => {
410
553
  const fn = () => {
411
554
  return db.changeTable('table', (t) => ({
412
555
  changeType: t.change(t.integer(), t.text()),
@@ -426,9 +569,23 @@ describe('changeTable', () => {
426
569
  }));
427
570
  };
428
571
 
429
- await fn();
430
- expectSql([
431
- `
572
+ const enumOne = ['one', 'two'];
573
+ const enumTwo = ['three', 'four'];
574
+
575
+ it('should change column up', async () => {
576
+ asMock(queryMock).mockResolvedValueOnce({
577
+ rows: enumOne.map((value) => [value]),
578
+ });
579
+ asMock(queryMock).mockResolvedValueOnce({
580
+ rows: enumTwo.map((value) => [value]),
581
+ });
582
+
583
+ await fn();
584
+
585
+ expectSql([
586
+ 'SELECT unnest(enum_range(NULL::"one"))::text',
587
+ 'SELECT unnest(enum_range(NULL::"two"))::text',
588
+ `
432
589
  ALTER TABLE "table"
433
590
  ALTER COLUMN "changeType" TYPE text,
434
591
  ALTER COLUMN "changeEnum" TYPE "two",
@@ -438,14 +595,30 @@ describe('changeTable', () => {
438
595
  ALTER COLUMN "changeNull" DROP NOT NULL,
439
596
  ALTER COLUMN "changeCompression" SET COMPRESSION value
440
597
  `,
441
- `COMMENT ON COLUMN "table"."changeComment" IS 'comment 2'`,
442
- ]);
598
+ `COMMENT ON COLUMN "table"."changeComment" IS 'comment 2'`,
599
+ ]);
443
600
 
444
- queryMock.mockClear();
445
- db.up = false;
446
- await fn();
447
- expectSql([
448
- `
601
+ const [{ ast }] = asMock(db.options.appCodeUpdater).mock.calls[0];
602
+ expect(ast.shape.changeEnum.from.column.options).toEqual(enumOne);
603
+ expect(ast.shape.changeEnum.to.column.options).toEqual(enumTwo);
604
+ });
605
+
606
+ it('should change column down', async () => {
607
+ asMock(queryMock).mockResolvedValueOnce({
608
+ rows: enumTwo.map((value) => [value]),
609
+ });
610
+ asMock(queryMock).mockResolvedValueOnce({
611
+ rows: enumOne.map((value) => [value]),
612
+ });
613
+
614
+ db.up = false;
615
+
616
+ await fn();
617
+
618
+ expectSql([
619
+ 'SELECT unnest(enum_range(NULL::"two"))::text',
620
+ 'SELECT unnest(enum_range(NULL::"one"))::text',
621
+ `
449
622
  ALTER TABLE "table"
450
623
  ALTER COLUMN "changeType" TYPE integer,
451
624
  ALTER COLUMN "changeEnum" TYPE "one",
@@ -455,8 +628,13 @@ describe('changeTable', () => {
455
628
  ALTER COLUMN "changeNull" SET NOT NULL,
456
629
  ALTER COLUMN "changeCompression" SET COMPRESSION DEFAULT
457
630
  `,
458
- `COMMENT ON COLUMN "table"."changeComment" IS 'comment 1'`,
459
- ]);
631
+ `COMMENT ON COLUMN "table"."changeComment" IS 'comment 1'`,
632
+ ]);
633
+
634
+ const [{ ast }] = asMock(db.options.appCodeUpdater).mock.calls[0];
635
+ expect(ast.shape.changeEnum.from.column.options).toEqual(enumTwo);
636
+ expect(ast.shape.changeEnum.to.column.options).toEqual(enumOne);
637
+ });
460
638
  });
461
639
 
462
640
  it('should add composite primary key via change', async () => {