rake-db 1.3.2 → 2.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.
Files changed (72) hide show
  1. package/.env +3 -0
  2. package/.env.local +1 -0
  3. package/README.md +1 -545
  4. package/db.ts +16 -0
  5. package/dist/index.d.ts +94 -0
  6. package/dist/index.esm.js +190 -0
  7. package/dist/index.esm.js.map +1 -0
  8. package/dist/index.js +201 -0
  9. package/dist/index.js.map +1 -0
  10. package/jest-setup.ts +3 -0
  11. package/migrations/20221009210157_first.ts +8 -0
  12. package/migrations/20221009210200_second.ts +5 -0
  13. package/package.json +55 -41
  14. package/rollup.config.js +3 -0
  15. package/src/commands/createOrDrop.test.ts +145 -0
  16. package/src/commands/createOrDrop.ts +107 -0
  17. package/src/commands/generate.test.ts +133 -0
  18. package/src/commands/generate.ts +85 -0
  19. package/src/commands/migrateOrRollback.test.ts +118 -0
  20. package/src/commands/migrateOrRollback.ts +108 -0
  21. package/src/common.test.ts +281 -0
  22. package/src/common.ts +224 -0
  23. package/src/index.ts +2 -0
  24. package/src/migration/change.ts +20 -0
  25. package/src/migration/changeTable.test.ts +417 -0
  26. package/src/migration/changeTable.ts +375 -0
  27. package/src/migration/createTable.test.ts +269 -0
  28. package/src/migration/createTable.ts +169 -0
  29. package/src/migration/migration.test.ts +341 -0
  30. package/src/migration/migration.ts +296 -0
  31. package/src/migration/migrationUtils.ts +281 -0
  32. package/src/rakeDb.ts +29 -0
  33. package/src/test-utils.ts +45 -0
  34. package/tsconfig.json +12 -0
  35. package/dist/lib/createAndDrop.d.ts +0 -2
  36. package/dist/lib/createAndDrop.js +0 -63
  37. package/dist/lib/defaults.d.ts +0 -2
  38. package/dist/lib/defaults.js +0 -5
  39. package/dist/lib/errorCodes.d.ts +0 -4
  40. package/dist/lib/errorCodes.js +0 -7
  41. package/dist/lib/generate.d.ts +0 -1
  42. package/dist/lib/generate.js +0 -99
  43. package/dist/lib/help.d.ts +0 -2
  44. package/dist/lib/help.js +0 -24
  45. package/dist/lib/init.d.ts +0 -2
  46. package/dist/lib/init.js +0 -276
  47. package/dist/lib/migrate.d.ts +0 -4
  48. package/dist/lib/migrate.js +0 -189
  49. package/dist/lib/migration.d.ts +0 -37
  50. package/dist/lib/migration.js +0 -159
  51. package/dist/lib/schema/changeTable.d.ts +0 -23
  52. package/dist/lib/schema/changeTable.js +0 -109
  53. package/dist/lib/schema/column.d.ts +0 -31
  54. package/dist/lib/schema/column.js +0 -201
  55. package/dist/lib/schema/createTable.d.ts +0 -10
  56. package/dist/lib/schema/createTable.js +0 -53
  57. package/dist/lib/schema/foreignKey.d.ts +0 -11
  58. package/dist/lib/schema/foreignKey.js +0 -53
  59. package/dist/lib/schema/index.d.ts +0 -3
  60. package/dist/lib/schema/index.js +0 -54
  61. package/dist/lib/schema/primaryKey.d.ts +0 -9
  62. package/dist/lib/schema/primaryKey.js +0 -24
  63. package/dist/lib/schema/table.d.ts +0 -43
  64. package/dist/lib/schema/table.js +0 -110
  65. package/dist/lib/schema/timestamps.d.ts +0 -3
  66. package/dist/lib/schema/timestamps.js +0 -9
  67. package/dist/lib/utils.d.ts +0 -26
  68. package/dist/lib/utils.js +0 -114
  69. package/dist/rake-db.d.ts +0 -2
  70. package/dist/rake-db.js +0 -34
  71. package/dist/types.d.ts +0 -94
  72. package/dist/types.js +0 -40
@@ -0,0 +1,375 @@
1
+ import {
2
+ ColumnTypes,
3
+ ColumnType,
4
+ columnTypes,
5
+ resetTableData,
6
+ quote,
7
+ getTableData,
8
+ EmptyObject,
9
+ emptyObject,
10
+ TableData,
11
+ RawExpression,
12
+ getRaw,
13
+ isRaw,
14
+ } from 'pqb';
15
+ import {
16
+ ChangeTableCallback,
17
+ ChangeTableOptions,
18
+ ColumnComment,
19
+ ColumnIndex,
20
+ DropMode,
21
+ Migration,
22
+ } from './migration';
23
+ import {
24
+ addColumnComment,
25
+ addColumnIndex,
26
+ columnToSql,
27
+ constraintToSql,
28
+ migrateComments,
29
+ migrateIndexes,
30
+ primaryKeyToSql,
31
+ } from './migrationUtils';
32
+
33
+ const newChangeTableData = () => ({
34
+ add: [],
35
+ drop: [],
36
+ });
37
+
38
+ let changeTableData: { add: TableData[]; drop: TableData[] } =
39
+ newChangeTableData();
40
+
41
+ const resetChangeTableData = () => {
42
+ changeTableData = newChangeTableData();
43
+ };
44
+
45
+ function add(item: ColumnType, options?: { dropMode?: DropMode }): ChangeItem;
46
+ function add(emptyObject: EmptyObject): EmptyObject;
47
+ function add(
48
+ items: Record<string, ColumnType>,
49
+ options?: { dropMode?: DropMode },
50
+ ): Record<string, ChangeItem>;
51
+ function add(
52
+ item: ColumnType | EmptyObject | Record<string, ColumnType>,
53
+ options?: { dropMode?: DropMode },
54
+ ): ChangeItem | EmptyObject | Record<string, ChangeItem> {
55
+ if (item instanceof ColumnType) {
56
+ return ['add', item, options];
57
+ } else if (item === emptyObject) {
58
+ changeTableData.add.push(getTableData());
59
+ resetTableData();
60
+ return emptyObject;
61
+ } else {
62
+ const result: Record<string, ChangeItem> = {};
63
+ for (const key in item) {
64
+ result[key] = ['add', (item as Record<string, ColumnType>)[key], options];
65
+ }
66
+ return result;
67
+ }
68
+ }
69
+
70
+ const drop = ((item, options) => {
71
+ if (item instanceof ColumnType) {
72
+ return ['drop', item, options];
73
+ } else if (item === emptyObject) {
74
+ changeTableData.drop.push(getTableData());
75
+ resetTableData();
76
+ return emptyObject;
77
+ } else {
78
+ const result: Record<string, ChangeItem> = {};
79
+ for (const key in item) {
80
+ result[key] = [
81
+ 'drop',
82
+ (item as Record<string, ColumnType>)[key],
83
+ options,
84
+ ];
85
+ }
86
+ return result;
87
+ }
88
+ }) as typeof add;
89
+
90
+ type ChangeOptions = {
91
+ usingUp?: RawExpression;
92
+ usingDown?: RawExpression;
93
+ };
94
+
95
+ type ChangeArg =
96
+ | ColumnType
97
+ | ['default', unknown | RawExpression]
98
+ | ['nullable', boolean]
99
+ | ['comment', string | null];
100
+
101
+ type TableChangeMethods = typeof tableChangeMethods;
102
+ const tableChangeMethods = {
103
+ add,
104
+ drop,
105
+ change(from: ChangeArg, to: ChangeArg, options?: ChangeOptions): ChangeItem {
106
+ return ['change', from, to, options];
107
+ },
108
+ default(value: unknown | RawExpression): ChangeArg {
109
+ return ['default', value];
110
+ },
111
+ nullable(): ChangeArg {
112
+ return ['nullable', true];
113
+ },
114
+ nonNullable(): ChangeArg {
115
+ return ['nullable', false];
116
+ },
117
+ comment(name: string | null): ChangeArg {
118
+ return ['comment', name];
119
+ },
120
+ rename(name: string): ChangeItem {
121
+ return ['rename', name];
122
+ },
123
+ };
124
+
125
+ export type ChangeItem =
126
+ | [
127
+ action: 'add' | 'drop',
128
+ item: ColumnType,
129
+ options?: { dropMode?: DropMode },
130
+ ]
131
+ | [action: 'change', from: ChangeArg, to: ChangeArg, options?: ChangeOptions]
132
+ | ['rename', string];
133
+
134
+ export type TableChanger = ColumnTypes & TableChangeMethods;
135
+
136
+ export type TableChangeData = Record<string, ChangeItem | EmptyObject>;
137
+
138
+ type ChangeTableState = {
139
+ migration: Migration;
140
+ up: boolean;
141
+ tableName: string;
142
+ alterTable: string[];
143
+ values: unknown[];
144
+ indexes: ColumnIndex[];
145
+ dropIndexes: ColumnIndex[];
146
+ comments: ColumnComment[];
147
+ };
148
+
149
+ export const changeTable = async (
150
+ migration: Migration,
151
+ up: boolean,
152
+ tableName: string,
153
+ options: ChangeTableOptions,
154
+ fn?: ChangeTableCallback,
155
+ ) => {
156
+ resetTableData();
157
+ resetChangeTableData();
158
+
159
+ const tableChanger = Object.create(columnTypes) as TableChanger;
160
+ Object.assign(tableChanger, tableChangeMethods);
161
+
162
+ const changeData = fn?.(tableChanger) || {};
163
+
164
+ const state: ChangeTableState = {
165
+ migration,
166
+ up,
167
+ tableName,
168
+ alterTable: [],
169
+ values: [],
170
+ indexes: [],
171
+ dropIndexes: [],
172
+ comments: [],
173
+ };
174
+
175
+ if (options.comment !== undefined) {
176
+ await changeActions.tableComment(state, tableName, options.comment);
177
+ }
178
+
179
+ for (const key in changeData) {
180
+ const result = changeData[key];
181
+ if (Array.isArray(result)) {
182
+ const [action] = result;
183
+ if (action === 'change') {
184
+ const [, from, to, options] = result;
185
+ changeActions.change(state, up, key, from, to, options);
186
+ } else if (action === 'rename') {
187
+ const [, name] = result;
188
+ changeActions.rename(state, up, key, name);
189
+ } else {
190
+ const [action, item, options] = result;
191
+ changeActions[action](state, up, key, item, options);
192
+ }
193
+ }
194
+ }
195
+
196
+ changeTableData.add.forEach((tableData) => {
197
+ handleTableData(state, up, tableName, tableData);
198
+ });
199
+
200
+ changeTableData.drop.forEach((tableData) => {
201
+ handleTableData(state, !up, tableName, tableData);
202
+ });
203
+
204
+ if (state.alterTable.length) {
205
+ await migration.query(
206
+ `ALTER TABLE "${tableName}"` + `\n ${state.alterTable.join(',\n ')}`,
207
+ );
208
+ }
209
+
210
+ const createIndexes = up ? state.indexes : state.dropIndexes;
211
+ const dropIndexes = up ? state.dropIndexes : state.indexes;
212
+ await migrateIndexes(state, createIndexes, up);
213
+ await migrateIndexes(state, dropIndexes, !up);
214
+ await migrateComments(state, state.comments);
215
+ };
216
+
217
+ const changeActions = {
218
+ tableComment(
219
+ { migration, up }: ChangeTableState,
220
+ tableName: string,
221
+ comment: Exclude<ChangeTableOptions['comment'], undefined>,
222
+ ) {
223
+ let value;
224
+ if (up) {
225
+ value = Array.isArray(comment) ? comment[1] : comment;
226
+ } else {
227
+ value = Array.isArray(comment) ? comment[0] : null;
228
+ }
229
+ return migration.query(
230
+ `COMMENT ON TABLE "${tableName}" IS ${quote(value)}`,
231
+ );
232
+ },
233
+
234
+ add(
235
+ state: ChangeTableState,
236
+ up: boolean,
237
+ key: string,
238
+ item: ColumnType,
239
+ options?: { dropMode?: DropMode },
240
+ ) {
241
+ addColumnIndex(state[up ? 'indexes' : 'dropIndexes'], key, item);
242
+
243
+ if (up) {
244
+ addColumnComment(state.comments, key, item);
245
+ }
246
+
247
+ if (up) {
248
+ state.alterTable.push(`ADD COLUMN ${columnToSql(key, item, state)}`);
249
+ } else {
250
+ state.alterTable.push(
251
+ `DROP COLUMN "${key}"${
252
+ options?.dropMode ? ` ${options.dropMode}` : ''
253
+ }`,
254
+ );
255
+ }
256
+ },
257
+
258
+ drop(
259
+ state: ChangeTableState,
260
+ up: boolean,
261
+ key: string,
262
+ item: ColumnType,
263
+ options?: { dropMode?: DropMode },
264
+ ) {
265
+ this.add(state, !up, key, item, options);
266
+ },
267
+
268
+ change(
269
+ state: ChangeTableState,
270
+ up: boolean,
271
+ key: string,
272
+ first: ChangeArg,
273
+ second: ChangeArg,
274
+ options?: ChangeOptions,
275
+ ) {
276
+ const [fromItem, toItem] = up ? [first, second] : [second, first];
277
+
278
+ const from = getChangeProperties(fromItem);
279
+ const to = getChangeProperties(toItem);
280
+
281
+ if (from.type !== to.type || from.collate !== to.collate) {
282
+ const using = up ? options?.usingUp : options?.usingDown;
283
+ state.alterTable.push(
284
+ `ALTER COLUMN "${key}" TYPE ${to.type}${
285
+ to.collate ? ` COLLATE ${quote(to.collate)}` : ''
286
+ }${using ? ` USING ${getRaw(using, state.values)}` : ''}`,
287
+ );
288
+ }
289
+
290
+ if (from.default !== to.default) {
291
+ const value = getRawOrValue(to.default, state.values);
292
+ const expr =
293
+ value === undefined ? `DROP DEFAULT` : `SET DEFAULT ${value}`;
294
+ state.alterTable.push(`ALTER COLUMN "${key}" ${expr}`);
295
+ }
296
+
297
+ if (from.nullable !== to.nullable) {
298
+ state.alterTable.push(
299
+ `ALTER COLUMN "${key}" ${to.nullable ? 'DROP' : 'SET'} NOT NULL`,
300
+ );
301
+ }
302
+
303
+ if (from.comment !== to.comment) {
304
+ state.comments.push({ column: key, comment: to.comment || null });
305
+ }
306
+ },
307
+
308
+ rename(state: ChangeTableState, up: boolean, key: string, name: string) {
309
+ const [from, to] = up ? [key, name] : [name, key];
310
+ state.alterTable.push(`RENAME COLUMN "${from}" TO "${to}"`);
311
+ },
312
+ };
313
+
314
+ const getChangeProperties = (
315
+ item: ChangeArg,
316
+ ): {
317
+ type?: string;
318
+ collate?: string;
319
+ default?: unknown | RawExpression;
320
+ nullable?: boolean;
321
+ comment?: string | null;
322
+ } => {
323
+ if (item instanceof ColumnType) {
324
+ return {
325
+ type: item.toSQL(),
326
+ collate: item.data.collate,
327
+ default: item.data.default,
328
+ nullable: item.isNullable,
329
+ comment: item.data.comment,
330
+ };
331
+ } else {
332
+ return {
333
+ type: undefined,
334
+ collate: undefined,
335
+ default: item[0] === 'default' ? item[1] : undefined,
336
+ nullable: item[0] === 'nullable' ? item[1] : undefined,
337
+ comment: item[0] === 'comment' ? item[1] : undefined,
338
+ };
339
+ }
340
+ };
341
+
342
+ const handleTableData = (
343
+ state: ChangeTableState,
344
+ up: boolean,
345
+ tableName: string,
346
+ tableData: TableData,
347
+ ) => {
348
+ if (tableData.primaryKey) {
349
+ if (up) {
350
+ state.alterTable.push(`ADD ${primaryKeyToSql(tableData.primaryKey)}`);
351
+ } else {
352
+ const name = tableData.primaryKey.options?.name || `${tableName}_pkey`;
353
+ state.alterTable.push(`DROP CONSTRAINT "${name}"`);
354
+ }
355
+ }
356
+
357
+ if (tableData.indexes.length) {
358
+ state[up ? 'indexes' : 'dropIndexes'].push(...tableData.indexes);
359
+ }
360
+
361
+ if (tableData.foreignKeys.length) {
362
+ tableData.foreignKeys.forEach((foreignKey) => {
363
+ const action = up ? 'ADD' : 'DROP';
364
+ state.alterTable.push(
365
+ `\n ${action} ${constraintToSql(state.tableName, up, foreignKey)}`,
366
+ );
367
+ });
368
+ }
369
+ };
370
+
371
+ const getRawOrValue = (item: unknown | RawExpression, values: unknown[]) => {
372
+ return typeof item === 'object' && item && isRaw(item)
373
+ ? getRaw(item, values)
374
+ : quote(item);
375
+ };
@@ -0,0 +1,269 @@
1
+ import { raw } from 'pqb';
2
+ import { expectSql, getDb, queryMock, resetDb, toLine } from '../test-utils';
3
+
4
+ const db = getDb();
5
+
6
+ (['createTable', 'dropTable'] as const).forEach((action) => {
7
+ describe(action, () => {
8
+ beforeEach(resetDb);
9
+
10
+ it(`should ${action} with comment`, async () => {
11
+ await db[action](
12
+ 'name',
13
+ { comment: 'this is a table comment' },
14
+ () => ({}),
15
+ );
16
+
17
+ if (action === 'createTable') {
18
+ expectSql([
19
+ `
20
+ CREATE TABLE "name" (
21
+ )
22
+ `,
23
+ `COMMENT ON TABLE "name" IS 'this is a table comment'`,
24
+ ]);
25
+ } else {
26
+ expectSql(`
27
+ DROP TABLE "name"
28
+ `);
29
+ }
30
+ });
31
+
32
+ it(`should ${action} and revert on rollback`, async () => {
33
+ const fn = () => {
34
+ return db[action]('table', { dropMode: 'CASCADE' }, (t) => ({
35
+ id: t.serial().primaryKey(),
36
+ nullable: t.text().nullable(),
37
+ nonNullable: t.text(),
38
+ withDefault: t.boolean().default(false),
39
+ withDefaultRaw: t.date().default(raw(`now()`)),
40
+ withIndex: t.text().index({
41
+ name: 'indexName',
42
+ unique: true,
43
+ using: 'gin',
44
+ expression: 10,
45
+ collate: 'utf-8',
46
+ operator: 'operator',
47
+ order: 'ASC',
48
+ include: 'id',
49
+ with: 'fillfactor = 70',
50
+ tablespace: 'tablespace',
51
+ where: 'column = 123',
52
+ }),
53
+ uniqueColumn: t.text().unique(),
54
+ columnWithComment: t.text().comment('this is a column comment'),
55
+ varcharWithLength: t.varchar(20),
56
+ decimalWithPrecisionAndScale: t.decimal(10, 5),
57
+ columnWithCompression: t.text().compression('compression'),
58
+ columnWithCollate: t.text().collate('utf-8'),
59
+ columnWithForeignKey: t.integer().foreignKey('table', 'column', {
60
+ name: 'fkeyConstraint',
61
+ match: 'FULL',
62
+ onUpdate: 'CASCADE',
63
+ onDelete: 'CASCADE',
64
+ }),
65
+ ...t.timestamps(),
66
+ }));
67
+ };
68
+
69
+ const expectCreateTable = () => {
70
+ expectSql([
71
+ `
72
+ CREATE TABLE "table" (
73
+ "id" serial PRIMARY KEY,
74
+ "nullable" text,
75
+ "nonNullable" text NOT NULL,
76
+ "withDefault" boolean NOT NULL DEFAULT false,
77
+ "withDefaultRaw" date NOT NULL DEFAULT now(),
78
+ "withIndex" text NOT NULL,
79
+ "uniqueColumn" text NOT NULL,
80
+ "columnWithComment" text NOT NULL,
81
+ "varcharWithLength" varchar(20) NOT NULL,
82
+ "decimalWithPrecisionAndScale" decimal(10, 5) NOT NULL,
83
+ "columnWithCompression" text COMPRESSION compression NOT NULL,
84
+ "columnWithCollate" text COLLATE 'utf-8' NOT NULL,
85
+ "columnWithForeignKey" integer NOT NULL CONSTRAINT "fkeyConstraint" REFERENCES "table"("column") MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE,
86
+ "createdAt" timestamp NOT NULL DEFAULT now(),
87
+ "updatedAt" timestamp NOT NULL DEFAULT now()
88
+ )
89
+ `,
90
+ toLine(`
91
+ CREATE UNIQUE INDEX "indexName"
92
+ ON "table"
93
+ USING gin
94
+ ("withIndex"(10) COLLATE 'utf-8' operator ASC)
95
+ INCLUDE ("id")
96
+ WITH (fillfactor = 70)
97
+ TABLESPACE tablespace
98
+ WHERE column = 123
99
+ `),
100
+ toLine(`
101
+ CREATE UNIQUE INDEX "tableUniqueColumnIndex"
102
+ ON "table"
103
+ ("uniqueColumn")
104
+ `),
105
+ `COMMENT ON COLUMN "table"."columnWithComment" IS 'this is a column comment'`,
106
+ ]);
107
+ };
108
+
109
+ const expectDropTable = () => {
110
+ expectSql(`
111
+ DROP TABLE "table" CASCADE
112
+ `);
113
+ };
114
+
115
+ await fn();
116
+ (action === 'createTable' ? expectCreateTable : expectDropTable)();
117
+
118
+ db.up = false;
119
+ queryMock.mockClear();
120
+ await fn();
121
+ (action === 'createTable' ? expectDropTable : expectCreateTable)();
122
+ });
123
+
124
+ it('should support composite primary key', async () => {
125
+ await db[action]('table', (t) => ({
126
+ id: t.integer(),
127
+ name: t.text(),
128
+ active: t.boolean(),
129
+ ...t.primaryKey(['id', 'name', 'active']),
130
+ }));
131
+
132
+ if (action === 'createTable') {
133
+ expectSql(`
134
+ CREATE TABLE "table" (
135
+ "id" integer NOT NULL,
136
+ "name" text NOT NULL,
137
+ "active" boolean NOT NULL,
138
+ PRIMARY KEY ("id", "name", "active")
139
+ )
140
+ `);
141
+ } else {
142
+ expectSql(`
143
+ DROP TABLE "table"
144
+ `);
145
+ }
146
+ });
147
+
148
+ it('should support composite primary key with constraint name', async () => {
149
+ await db[action]('table', (t) => ({
150
+ id: t.integer(),
151
+ name: t.text(),
152
+ active: t.boolean(),
153
+ ...t.primaryKey(['id', 'name', 'active'], { name: 'primaryKeyName' }),
154
+ }));
155
+
156
+ if (action === 'createTable') {
157
+ expectSql(`
158
+ CREATE TABLE "table" (
159
+ "id" integer NOT NULL,
160
+ "name" text NOT NULL,
161
+ "active" boolean NOT NULL,
162
+ CONSTRAINT "primaryKeyName" PRIMARY KEY ("id", "name", "active")
163
+ )
164
+ `);
165
+ } else {
166
+ expectSql(`
167
+ DROP TABLE "table"
168
+ `);
169
+ }
170
+ });
171
+
172
+ it('should support composite index', async () => {
173
+ await db[action]('table', (t) => ({
174
+ id: t.integer(),
175
+ name: t.text(),
176
+ ...t.index(['id', { column: 'name', order: 'DESC' }], {
177
+ name: 'compositeIndexOnTable',
178
+ }),
179
+ }));
180
+
181
+ if (action === 'createTable') {
182
+ expectSql([
183
+ `
184
+ CREATE TABLE "table" (
185
+ "id" integer NOT NULL,
186
+ "name" text NOT NULL
187
+ )
188
+ `,
189
+ `
190
+ CREATE INDEX "compositeIndexOnTable" ON "table" ("id", "name" DESC)
191
+ `,
192
+ ]);
193
+ } else {
194
+ expectSql(`
195
+ DROP TABLE "table"
196
+ `);
197
+ }
198
+ });
199
+
200
+ it('should support composite unique index', async () => {
201
+ await db[action]('table', (t) => ({
202
+ id: t.integer(),
203
+ name: t.text(),
204
+ ...t.unique(['id', { column: 'name', order: 'DESC' }], {
205
+ name: 'compositeIndexOnTable',
206
+ }),
207
+ }));
208
+
209
+ if (action === 'createTable') {
210
+ expectSql([
211
+ `
212
+ CREATE TABLE "table" (
213
+ "id" integer NOT NULL,
214
+ "name" text NOT NULL
215
+ )
216
+ `,
217
+ `
218
+ CREATE UNIQUE INDEX "compositeIndexOnTable" ON "table" ("id", "name" DESC)
219
+ `,
220
+ ]);
221
+ } else {
222
+ expectSql(`
223
+ DROP TABLE "table"
224
+ `);
225
+ }
226
+ });
227
+
228
+ it('should support composite foreign key', async () => {
229
+ await db[action]('table', (t) => ({
230
+ id: t.integer(),
231
+ name: t.text(),
232
+ ...t.foreignKey(
233
+ ['id', 'name'],
234
+ 'otherTable',
235
+ ['foreignId', 'foreignName'],
236
+ {
237
+ name: 'constraintName',
238
+ match: 'FULL',
239
+ onUpdate: 'CASCADE',
240
+ onDelete: 'CASCADE',
241
+ },
242
+ ),
243
+ }));
244
+
245
+ if (action === 'createTable') {
246
+ const expectedConstraint = toLine(`
247
+ CONSTRAINT "constraintName"
248
+ FOREIGN KEY ("id", "name")
249
+ REFERENCES "otherTable"("foreignId", "foreignName")
250
+ MATCH FULL
251
+ ON DELETE CASCADE
252
+ ON UPDATE CASCADE
253
+ `);
254
+
255
+ expectSql(`
256
+ CREATE TABLE "table" (
257
+ "id" integer NOT NULL,
258
+ "name" text NOT NULL,
259
+ ${expectedConstraint}
260
+ )
261
+ `);
262
+ } else {
263
+ expectSql(`
264
+ DROP TABLE "table"
265
+ `);
266
+ }
267
+ });
268
+ });
269
+ });