rake-db 2.3.10 → 2.3.13

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.
@@ -6,6 +6,7 @@ import {
6
6
  ForeignKeyOptions,
7
7
  instantiateColumn,
8
8
  singleQuote,
9
+ TableData,
9
10
  } from 'pqb';
10
11
  import { getForeignKeyName, getIndexName } from '../migration/migrationUtils';
11
12
 
@@ -23,28 +24,27 @@ const fkeyActionMap = {
23
24
  d: 'SET DEFAULT',
24
25
  };
25
26
 
27
+ type Data = {
28
+ schemas: string[];
29
+ tables: DbStructure.Table[];
30
+ columns: DbStructure.Column[];
31
+ primaryKeys: DbStructure.PrimaryKey[];
32
+ indexes: DbStructure.Index[];
33
+ foreignKeys: DbStructure.ForeignKey[];
34
+ extensions: DbStructure.Extension[];
35
+ };
36
+
37
+ type PendingTables = Record<
38
+ string,
39
+ { table: DbStructure.Table; dependsOn: Set<string> }
40
+ >;
41
+
26
42
  export const structureToAst = async (db: DbStructure): Promise<RakeDbAst[]> => {
27
43
  const ast: RakeDbAst[] = [];
28
44
 
29
- const [
30
- schemas,
31
- tables,
32
- allColumns,
33
- allPrimaryKeys,
34
- allIndexes,
35
- allForeignKeys,
36
- extensions,
37
- ] = await Promise.all([
38
- db.getSchemas(),
39
- db.getTables(),
40
- db.getColumns(),
41
- db.getPrimaryKeys(),
42
- db.getIndexes(),
43
- db.getForeignKeys(),
44
- db.getExtensions(),
45
- ]);
45
+ const data = await getData(db);
46
46
 
47
- for (const name of schemas) {
47
+ for (const name of data.schemas) {
48
48
  if (name === 'public') continue;
49
49
 
50
50
  ast.push({
@@ -54,154 +54,63 @@ export const structureToAst = async (db: DbStructure): Promise<RakeDbAst[]> => {
54
54
  });
55
55
  }
56
56
 
57
- for (const table of tables) {
58
- const { schemaName, name } = table;
59
-
60
- if (name === 'schemaMigrations') continue;
57
+ const pendingTables: PendingTables = {};
58
+ for (const table of data.tables) {
59
+ const key = `${table.schemaName}.${table.name}`;
60
+ const dependsOn = new Set<string>();
61
61
 
62
- const belongsToTable = makeBelongsToTable(schemaName, name);
62
+ for (const fk of data.foreignKeys) {
63
+ if (fk.schemaName !== table.schemaName || fk.tableName !== table.name)
64
+ continue;
63
65
 
64
- const columns = allColumns.filter(belongsToTable);
65
- const primaryKey = allPrimaryKeys.find(belongsToTable);
66
- const tableIndexes = allIndexes.filter(belongsToTable);
67
- const tableForeignKeys = allForeignKeys.filter(belongsToTable);
68
-
69
- const shape: ColumnsShape = {};
70
- for (let item of columns) {
71
- const isSerial = getIsSerial(item);
72
- if (isSerial) {
73
- item = { ...item, default: undefined };
66
+ const otherKey = `${fk.foreignTableSchemaName}.${fk.foreignTableName}`;
67
+ if (otherKey !== key) {
68
+ dependsOn.add(otherKey);
74
69
  }
70
+ }
75
71
 
76
- const klass = columnsByType[getColumnType(item, isSerial)];
77
- if (!klass) {
78
- throw new Error(`Column type \`${item.type}\` is not supported`);
79
- }
72
+ pendingTables[key] = { table, dependsOn };
73
+ }
80
74
 
81
- let column = instantiateColumn(klass, item);
75
+ for (const key in pendingTables) {
76
+ const { table, dependsOn } = pendingTables[key];
77
+ if (!dependsOn.size) {
78
+ pushTableAst(ast, data, table, pendingTables);
79
+ }
80
+ }
82
81
 
83
- if (
84
- primaryKey?.columnNames.length === 1 &&
85
- primaryKey?.columnNames[0] === item.name
86
- ) {
87
- column = column.primaryKey();
88
- }
82
+ const outerFKeys: [DbStructure.ForeignKey, DbStructure.Table][] = [];
89
83
 
90
- const indexes = tableIndexes.filter(
91
- (it) =>
92
- it.columns.length === 1 &&
93
- 'column' in it.columns[0] &&
94
- it.columns[0].column === item.name,
95
- );
96
- for (const index of indexes) {
97
- const options = index.columns[0];
98
- column = column.index({
99
- collate: options.collate,
100
- opclass: options.opclass,
101
- order: options.order,
102
- name:
103
- index.name !== getIndexName(name, index.columns)
104
- ? index.name
105
- : undefined,
106
- using: index.using === 'btree' ? undefined : index.using,
107
- unique: index.isUnique,
108
- include: index.include,
109
- with: index.with,
110
- tablespace: index.tablespace,
111
- where: index.where,
112
- });
113
- }
84
+ for (const key in pendingTables) {
85
+ const innerFKeys: DbStructure.ForeignKey[] = [];
86
+ const { table } = pendingTables[key];
114
87
 
115
- const foreignKeys = tableForeignKeys.filter(
116
- (it) => it.columnNames.length === 1 && it.columnNames[0] === item.name,
117
- );
118
- for (const foreignKey of foreignKeys) {
119
- column = column.foreignKey(
120
- foreignKey.foreignTableName,
121
- foreignKey.foreignColumnNames[0],
122
- {
123
- name:
124
- foreignKey.name &&
125
- foreignKey.name !==
126
- getForeignKeyName(name, foreignKey.columnNames)
127
- ? foreignKey.name
128
- : undefined,
129
- match: matchMap[foreignKey.match],
130
- onUpdate: fkeyActionMap[foreignKey.onUpdate],
131
- onDelete: fkeyActionMap[foreignKey.onDelete],
132
- } as ForeignKeyOptions,
133
- );
134
- }
88
+ for (const fkey of data.foreignKeys) {
89
+ if (fkey.schemaName !== table.schemaName || fkey.tableName !== table.name)
90
+ continue;
135
91
 
136
- shape[item.name] = column;
92
+ const otherKey = `${fkey.foreignTableSchemaName}.${fkey.foreignTableName}`;
93
+ if (!pendingTables[otherKey] || otherKey === key) {
94
+ innerFKeys.push(fkey);
95
+ } else {
96
+ outerFKeys.push([fkey, table]);
97
+ }
137
98
  }
138
99
 
100
+ pushTableAst(ast, data, table, pendingTables, innerFKeys);
101
+ }
102
+
103
+ for (const [fkey, table] of outerFKeys) {
139
104
  ast.push({
140
- type: 'table',
105
+ ...foreignKeyToAst(fkey),
106
+ type: 'foreignKey',
141
107
  action: 'create',
142
- schema: schemaName === 'public' ? undefined : schemaName,
143
- comment: table.comment,
144
- name: name,
145
- shape,
146
- noPrimaryKey: primaryKey ? 'error' : 'ignore',
147
- primaryKey:
148
- primaryKey && primaryKey.columnNames.length > 1
149
- ? {
150
- columns: primaryKey.columnNames,
151
- options:
152
- primaryKey.name === `${name}_pkey`
153
- ? undefined
154
- : { name: primaryKey.name },
155
- }
156
- : undefined,
157
- indexes: tableIndexes
158
- .filter(
159
- (index) =>
160
- index.columns.length > 1 ||
161
- index.columns.some((it) => 'expression' in it),
162
- )
163
- .map((index) => ({
164
- columns: index.columns.map((it) => ({
165
- ...('column' in it
166
- ? { column: it.column }
167
- : { expression: it.expression }),
168
- collate: it.collate,
169
- opclass: it.opclass,
170
- order: it.order,
171
- })),
172
- options: {
173
- name:
174
- index.name !== getIndexName(name, index.columns)
175
- ? index.name
176
- : undefined,
177
- using: index.using === 'btree' ? undefined : index.using,
178
- unique: index.isUnique,
179
- include: index.include,
180
- with: index.with,
181
- tablespace: index.tablespace,
182
- where: index.where,
183
- },
184
- })),
185
- foreignKeys: tableForeignKeys
186
- .filter((it) => it.columnNames.length > 1)
187
- .map((it) => ({
188
- columns: it.columnNames,
189
- fnOrTable: it.foreignTableName,
190
- foreignColumns: it.foreignColumnNames,
191
- options: {
192
- name:
193
- it.name && it.name !== getForeignKeyName(name, it.columnNames)
194
- ? it.name
195
- : undefined,
196
- match: matchMap[it.match],
197
- onUpdate: fkeyActionMap[it.onUpdate],
198
- onDelete: fkeyActionMap[it.onDelete],
199
- } as ForeignKeyOptions,
200
- })),
108
+ tableSchema: table.schemaName === 'public' ? undefined : table.schemaName,
109
+ tableName: fkey.tableName,
201
110
  });
202
111
  }
203
112
 
204
- for (const it of extensions) {
113
+ for (const it of data.extensions) {
205
114
  ast.push({
206
115
  type: 'extension',
207
116
  action: 'create',
@@ -214,6 +123,36 @@ export const structureToAst = async (db: DbStructure): Promise<RakeDbAst[]> => {
214
123
  return ast;
215
124
  };
216
125
 
126
+ const getData = async (db: DbStructure): Promise<Data> => {
127
+ const [
128
+ schemas,
129
+ tables,
130
+ columns,
131
+ primaryKeys,
132
+ indexes,
133
+ foreignKeys,
134
+ extensions,
135
+ ] = await Promise.all([
136
+ db.getSchemas(),
137
+ db.getTables(),
138
+ db.getColumns(),
139
+ db.getPrimaryKeys(),
140
+ db.getIndexes(),
141
+ db.getForeignKeys(),
142
+ db.getExtensions(),
143
+ ]);
144
+
145
+ return {
146
+ schemas,
147
+ tables,
148
+ columns,
149
+ primaryKeys,
150
+ indexes,
151
+ foreignKeys,
152
+ extensions,
153
+ };
154
+ };
155
+
217
156
  const makeBelongsToTable =
218
157
  (schema: string | undefined, table: string) =>
219
158
  (item: { schemaName: string; tableName: string }) =>
@@ -250,3 +189,170 @@ const getColumnType = (item: DbStructure.Column, isSerial: boolean) => {
250
189
 
251
190
  return item.type;
252
191
  };
192
+
193
+ const pushTableAst = (
194
+ ast: RakeDbAst[],
195
+ data: Data,
196
+ table: DbStructure.Table,
197
+ pendingTables: PendingTables,
198
+ innerFKeys = data.foreignKeys,
199
+ ) => {
200
+ const { schemaName, name } = table;
201
+
202
+ const key = `${schemaName}.${table.name}`;
203
+ delete pendingTables[key];
204
+
205
+ if (name === 'schemaMigrations') return;
206
+
207
+ const belongsToTable = makeBelongsToTable(schemaName, name);
208
+
209
+ const columns = data.columns.filter(belongsToTable);
210
+ const primaryKey = data.primaryKeys.find(belongsToTable);
211
+ const tableIndexes = data.indexes.filter(belongsToTable);
212
+ const tableForeignKeys = innerFKeys.filter(belongsToTable);
213
+
214
+ const shape: ColumnsShape = {};
215
+ for (let item of columns) {
216
+ const isSerial = getIsSerial(item);
217
+ if (isSerial) {
218
+ item = { ...item, default: undefined };
219
+ }
220
+
221
+ const klass = columnsByType[getColumnType(item, isSerial)];
222
+ if (!klass) {
223
+ throw new Error(`Column type \`${item.type}\` is not supported`);
224
+ }
225
+
226
+ let column = instantiateColumn(klass, item);
227
+
228
+ if (
229
+ primaryKey?.columnNames.length === 1 &&
230
+ primaryKey?.columnNames[0] === item.name
231
+ ) {
232
+ column = column.primaryKey();
233
+ }
234
+
235
+ const indexes = tableIndexes.filter(
236
+ (it) =>
237
+ it.columns.length === 1 &&
238
+ 'column' in it.columns[0] &&
239
+ it.columns[0].column === item.name,
240
+ );
241
+ for (const index of indexes) {
242
+ const options = index.columns[0];
243
+ column = column.index({
244
+ collate: options.collate,
245
+ opclass: options.opclass,
246
+ order: options.order,
247
+ name:
248
+ index.name !== getIndexName(name, index.columns)
249
+ ? index.name
250
+ : undefined,
251
+ using: index.using === 'btree' ? undefined : index.using,
252
+ unique: index.isUnique,
253
+ include: index.include,
254
+ with: index.with,
255
+ tablespace: index.tablespace,
256
+ where: index.where,
257
+ });
258
+ }
259
+
260
+ const foreignKeys = tableForeignKeys.filter(
261
+ (it) => it.columnNames.length === 1 && it.columnNames[0] === item.name,
262
+ );
263
+ for (const foreignKey of foreignKeys) {
264
+ column = column.foreignKey(
265
+ foreignKey.foreignTableName,
266
+ foreignKey.foreignColumnNames[0],
267
+ {
268
+ name:
269
+ foreignKey.name &&
270
+ foreignKey.name !== getForeignKeyName(name, foreignKey.columnNames)
271
+ ? foreignKey.name
272
+ : undefined,
273
+ match: matchMap[foreignKey.match],
274
+ onUpdate: fkeyActionMap[foreignKey.onUpdate],
275
+ onDelete: fkeyActionMap[foreignKey.onDelete],
276
+ } as ForeignKeyOptions,
277
+ );
278
+ }
279
+
280
+ shape[item.name] = column;
281
+ }
282
+
283
+ ast.push({
284
+ type: 'table',
285
+ action: 'create',
286
+ schema: schemaName === 'public' ? undefined : schemaName,
287
+ comment: table.comment,
288
+ name: name,
289
+ shape,
290
+ noPrimaryKey: primaryKey ? 'error' : 'ignore',
291
+ primaryKey:
292
+ primaryKey && primaryKey.columnNames.length > 1
293
+ ? {
294
+ columns: primaryKey.columnNames,
295
+ options:
296
+ primaryKey.name === `${name}_pkey`
297
+ ? undefined
298
+ : { name: primaryKey.name },
299
+ }
300
+ : undefined,
301
+ indexes: tableIndexes
302
+ .filter(
303
+ (index) =>
304
+ index.columns.length > 1 ||
305
+ index.columns.some((it) => 'expression' in it),
306
+ )
307
+ .map((index) => ({
308
+ columns: index.columns.map((it) => ({
309
+ ...('column' in it
310
+ ? { column: it.column }
311
+ : { expression: it.expression }),
312
+ collate: it.collate,
313
+ opclass: it.opclass,
314
+ order: it.order,
315
+ })),
316
+ options: {
317
+ name:
318
+ index.name !== getIndexName(name, index.columns)
319
+ ? index.name
320
+ : undefined,
321
+ using: index.using === 'btree' ? undefined : index.using,
322
+ unique: index.isUnique,
323
+ include: index.include,
324
+ with: index.with,
325
+ tablespace: index.tablespace,
326
+ where: index.where,
327
+ },
328
+ })),
329
+ foreignKeys: tableForeignKeys
330
+ .filter((it) => it.columnNames.length > 1)
331
+ .map(foreignKeyToAst),
332
+ });
333
+
334
+ for (const otherKey in pendingTables) {
335
+ const item = pendingTables[otherKey];
336
+ if (item.dependsOn.delete(key) && item.dependsOn.size === 0) {
337
+ pushTableAst(ast, data, item.table, pendingTables);
338
+ }
339
+ }
340
+ };
341
+
342
+ const foreignKeyToAst = (
343
+ fkey: DbStructure.ForeignKey,
344
+ ): TableData.ForeignKey => ({
345
+ columns: fkey.columnNames,
346
+ fnOrTable: fkey.foreignTableName,
347
+ foreignColumns: fkey.foreignColumnNames,
348
+ options: {
349
+ name:
350
+ fkey.name &&
351
+ fkey.name !== getForeignKeyName(fkey.tableName, fkey.columnNames)
352
+ ? fkey.name
353
+ : undefined,
354
+ match: matchMap[fkey.match],
355
+ onUpdate: fkeyActionMap[fkey.onUpdate],
356
+ onDelete: fkeyActionMap[fkey.onDelete],
357
+ } as ForeignKeyOptions,
358
+ });