rake-db 2.2.4 → 2.2.6
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/CHANGELOG.md +14 -0
- package/db.ts +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +92 -60
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +92 -60
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/ast.ts +4 -0
- package/src/commands/migrateOrRollback.ts +8 -6
- package/src/common.test.ts +9 -6
- package/src/common.ts +21 -11
- package/src/migration/changeTable.test.ts +17 -0
- package/src/migration/changeTable.ts +13 -10
- package/src/migration/createJoinTable.test.ts +96 -0
- package/src/migration/createJoinTable.ts +17 -6
- package/src/migration/createTable.test.ts +16 -0
- package/src/migration/createTable.ts +11 -8
- package/src/migration/migration.test.ts +18 -86
- package/src/migration/migration.ts +18 -4
- package/src/migration/migrationUtils.ts +33 -21
- package/src/pull/dbStructure.test.ts +158 -0
- package/src/pull/dbStructure.ts +272 -0
- package/src/pull/getColumnByType.ts +15 -0
- package/src/pull/structureToAst.test.ts +308 -0
- package/src/pull/structureToAst.ts +145 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { DbStructure } from './dbStructure';
|
|
2
|
+
import { Adapter, IntegerColumn, TextColumn } from 'pqb';
|
|
3
|
+
import { structureToAst } from './structureToAst';
|
|
4
|
+
import { RakeDbAst } from '../ast';
|
|
5
|
+
|
|
6
|
+
const adapter = new Adapter({ databaseURL: 'file:path' });
|
|
7
|
+
const query = jest.fn().mockImplementation(() => ({ rows: [] }));
|
|
8
|
+
adapter.query = query;
|
|
9
|
+
adapter.arrays = query;
|
|
10
|
+
|
|
11
|
+
const tableColumn: DbStructure.Column = {
|
|
12
|
+
schemaName: 'public',
|
|
13
|
+
tableName: 'table',
|
|
14
|
+
name: 'column',
|
|
15
|
+
type: 'int4',
|
|
16
|
+
default: '123',
|
|
17
|
+
isNullable: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const tableColumns = [
|
|
21
|
+
{ ...tableColumn, name: 'id' },
|
|
22
|
+
{ ...tableColumn, name: 'name', type: 'text' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const otherTableColumn = { ...tableColumn, tableName: 'otherTable' };
|
|
26
|
+
|
|
27
|
+
const table = { schemaName: 'public', name: 'table' };
|
|
28
|
+
|
|
29
|
+
const columns = [...tableColumns, otherTableColumn];
|
|
30
|
+
|
|
31
|
+
const primaryKey: DbStructure.Constraint = {
|
|
32
|
+
schemaName: 'public',
|
|
33
|
+
tableName: 'table',
|
|
34
|
+
name: 'pkey',
|
|
35
|
+
type: 'PRIMARY KEY',
|
|
36
|
+
columnNames: ['id'],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const index: DbStructure.Index = {
|
|
40
|
+
schemaName: 'public',
|
|
41
|
+
tableName: 'table',
|
|
42
|
+
columnNames: ['name'],
|
|
43
|
+
name: 'index',
|
|
44
|
+
isUnique: false,
|
|
45
|
+
isPrimary: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const foreignKey: DbStructure.ForeignKey = {
|
|
49
|
+
schemaName: 'public',
|
|
50
|
+
tableName: 'table',
|
|
51
|
+
foreignTableSchemaName: 'public',
|
|
52
|
+
foreignTableName: 'otherTable',
|
|
53
|
+
name: 'fkey',
|
|
54
|
+
columnNames: ['otherId'],
|
|
55
|
+
foreignColumnNames: ['id'],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const extension: DbStructure.Extension = {
|
|
59
|
+
schemaName: 'public',
|
|
60
|
+
name: 'name',
|
|
61
|
+
version: '123',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe('structureToAst', () => {
|
|
65
|
+
it('should add schema except public', async () => {
|
|
66
|
+
const db = new DbStructure(adapter);
|
|
67
|
+
db.getSchemas = async () => ['public', 'one', 'two'];
|
|
68
|
+
const ast = await structureToAst(db);
|
|
69
|
+
expect(ast).toEqual([
|
|
70
|
+
{
|
|
71
|
+
type: 'schema',
|
|
72
|
+
action: 'create',
|
|
73
|
+
name: 'one',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'schema',
|
|
77
|
+
action: 'create',
|
|
78
|
+
name: 'two',
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('table', () => {
|
|
84
|
+
it('should add table', async () => {
|
|
85
|
+
const db = new DbStructure(adapter);
|
|
86
|
+
db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
|
|
87
|
+
const ast = await structureToAst(db);
|
|
88
|
+
expect(ast).toEqual([
|
|
89
|
+
{
|
|
90
|
+
type: 'table',
|
|
91
|
+
action: 'create',
|
|
92
|
+
name: 'table',
|
|
93
|
+
shape: {},
|
|
94
|
+
noPrimaryKey: 'ignore',
|
|
95
|
+
indexes: [],
|
|
96
|
+
foreignKeys: [],
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should add table with schema', async () => {
|
|
102
|
+
const db = new DbStructure(adapter);
|
|
103
|
+
db.getTables = async () => [{ schemaName: 'custom', name: 'table' }];
|
|
104
|
+
const ast = await structureToAst(db);
|
|
105
|
+
expect(ast).toEqual([
|
|
106
|
+
{
|
|
107
|
+
type: 'table',
|
|
108
|
+
action: 'create',
|
|
109
|
+
schema: 'custom',
|
|
110
|
+
name: 'table',
|
|
111
|
+
shape: {},
|
|
112
|
+
noPrimaryKey: 'ignore',
|
|
113
|
+
indexes: [],
|
|
114
|
+
foreignKeys: [],
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should add columns', async () => {
|
|
120
|
+
const db = new DbStructure(adapter);
|
|
121
|
+
db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
|
|
122
|
+
db.getColumns = async () => columns;
|
|
123
|
+
|
|
124
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
125
|
+
|
|
126
|
+
expect(Object.keys(ast.shape).length).toBe(tableColumns.length);
|
|
127
|
+
expect(ast.noPrimaryKey).toBe('ignore');
|
|
128
|
+
expect(ast.shape.id).toBeInstanceOf(IntegerColumn);
|
|
129
|
+
expect(ast.shape.name).toBeInstanceOf(TextColumn);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should set primaryKey to column', async () => {
|
|
133
|
+
const db = new DbStructure(adapter);
|
|
134
|
+
db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
|
|
135
|
+
db.getColumns = async () => columns;
|
|
136
|
+
db.getConstraints = async () => [primaryKey];
|
|
137
|
+
|
|
138
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
139
|
+
|
|
140
|
+
expect(ast.noPrimaryKey).toBe('error');
|
|
141
|
+
expect(ast.shape.id.isPrimaryKey).toBe(true);
|
|
142
|
+
expect(ast.primaryKey).toBe(undefined);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should add composite primary key', async () => {
|
|
146
|
+
const db = new DbStructure(adapter);
|
|
147
|
+
db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
|
|
148
|
+
db.getColumns = async () => columns;
|
|
149
|
+
db.getConstraints = async () => [
|
|
150
|
+
{ ...primaryKey, columnNames: ['id', 'name'] },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
154
|
+
|
|
155
|
+
expect(ast.noPrimaryKey).toBe('error');
|
|
156
|
+
expect(ast.shape.id.isPrimaryKey).toBe(false);
|
|
157
|
+
expect(ast.primaryKey).toEqual({
|
|
158
|
+
columns: ['id', 'name'],
|
|
159
|
+
options: { name: 'pkey' },
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should ignore primary key name if it is standard', async () => {
|
|
164
|
+
const db = new DbStructure(adapter);
|
|
165
|
+
db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
|
|
166
|
+
db.getColumns = async () => columns;
|
|
167
|
+
db.getConstraints = async () => [
|
|
168
|
+
{ ...primaryKey, columnNames: ['id', 'name'], name: 'table_pkey' },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
172
|
+
|
|
173
|
+
expect(ast.noPrimaryKey).toBe('error');
|
|
174
|
+
expect(ast.shape.id.isPrimaryKey).toBe(false);
|
|
175
|
+
expect(ast.primaryKey).toEqual({
|
|
176
|
+
columns: ['id', 'name'],
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should ignore primary key indexes', async () => {
|
|
181
|
+
const db = new DbStructure(adapter);
|
|
182
|
+
db.getTables = async () => [{ schemaName: 'public', name: 'table' }];
|
|
183
|
+
db.getColumns = async () => columns;
|
|
184
|
+
db.getIndexes = async () => [{ ...index, isPrimary: true }];
|
|
185
|
+
|
|
186
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
187
|
+
expect(ast.shape.name.data.index).toBe(undefined);
|
|
188
|
+
expect(ast.indexes).toHaveLength(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should add index to column', async () => {
|
|
192
|
+
const db = new DbStructure(adapter);
|
|
193
|
+
db.getTables = async () => [table];
|
|
194
|
+
db.getColumns = async () => columns;
|
|
195
|
+
db.getIndexes = async () => [index];
|
|
196
|
+
|
|
197
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
198
|
+
expect(ast.shape.name.data.index).toEqual({
|
|
199
|
+
name: 'index',
|
|
200
|
+
unique: false,
|
|
201
|
+
});
|
|
202
|
+
expect(ast.indexes).toHaveLength(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should add composite indexes to table', async () => {
|
|
206
|
+
const db = new DbStructure(adapter);
|
|
207
|
+
db.getTables = async () => [table];
|
|
208
|
+
db.getColumns = async () => columns;
|
|
209
|
+
db.getIndexes = async () => [
|
|
210
|
+
{ ...index, columnNames: ['id', 'name'] },
|
|
211
|
+
{ ...index, columnNames: ['id', 'name'], isUnique: true },
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
215
|
+
expect(ast.shape.name.data.index).toBe(undefined);
|
|
216
|
+
expect(ast.indexes).toEqual([
|
|
217
|
+
{
|
|
218
|
+
columns: [{ column: 'id' }, { column: 'name' }],
|
|
219
|
+
options: { name: 'index', unique: false },
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
columns: [{ column: 'id' }, { column: 'name' }],
|
|
223
|
+
options: { name: 'index', unique: true },
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should add foreign key to the column', async () => {
|
|
229
|
+
const db = new DbStructure(adapter);
|
|
230
|
+
db.getTables = async () => [table];
|
|
231
|
+
db.getColumns = async () => [
|
|
232
|
+
...columns,
|
|
233
|
+
{ ...tableColumn, name: 'otherId' },
|
|
234
|
+
];
|
|
235
|
+
db.getForeignKeys = async () => [foreignKey];
|
|
236
|
+
|
|
237
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
238
|
+
|
|
239
|
+
expect(ast.shape.otherId.data.foreignKey).toEqual({
|
|
240
|
+
columns: ['id'],
|
|
241
|
+
name: 'fkey',
|
|
242
|
+
table: 'otherTable',
|
|
243
|
+
});
|
|
244
|
+
expect(ast.foreignKeys).toHaveLength(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should add composite foreign key', async () => {
|
|
248
|
+
const db = new DbStructure(adapter);
|
|
249
|
+
db.getTables = async () => [table];
|
|
250
|
+
db.getColumns = async () => [
|
|
251
|
+
...columns,
|
|
252
|
+
{ ...tableColumn, name: 'otherId' },
|
|
253
|
+
];
|
|
254
|
+
db.getForeignKeys = async () => [
|
|
255
|
+
{
|
|
256
|
+
...foreignKey,
|
|
257
|
+
columnNames: ['name', 'otherId'],
|
|
258
|
+
foreignColumnNames: ['name', 'id'],
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Table];
|
|
263
|
+
|
|
264
|
+
expect(ast.shape.otherId.data.foreignKey).toBe(undefined);
|
|
265
|
+
expect(ast.foreignKeys).toEqual([
|
|
266
|
+
{
|
|
267
|
+
columns: ['name', 'otherId'],
|
|
268
|
+
fnOrTable: 'otherTable',
|
|
269
|
+
foreignColumns: ['name', 'id'],
|
|
270
|
+
options: {
|
|
271
|
+
name: 'fkey',
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('extension', () => {
|
|
279
|
+
it('should add extension', async () => {
|
|
280
|
+
const db = new DbStructure(adapter);
|
|
281
|
+
db.getExtensions = async () => [{ ...extension, schemaName: 'custom' }];
|
|
282
|
+
|
|
283
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Extension];
|
|
284
|
+
|
|
285
|
+
expect(ast).toEqual({
|
|
286
|
+
type: 'extension',
|
|
287
|
+
action: 'create',
|
|
288
|
+
name: 'name',
|
|
289
|
+
schema: 'custom',
|
|
290
|
+
version: '123',
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should ignore schema if it is `public`', async () => {
|
|
295
|
+
const db = new DbStructure(adapter);
|
|
296
|
+
db.getExtensions = async () => [extension];
|
|
297
|
+
|
|
298
|
+
const [ast] = (await structureToAst(db)) as [RakeDbAst.Extension];
|
|
299
|
+
|
|
300
|
+
expect(ast).toEqual({
|
|
301
|
+
type: 'extension',
|
|
302
|
+
action: 'create',
|
|
303
|
+
name: 'name',
|
|
304
|
+
version: '123',
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { DbStructure } from './dbStructure';
|
|
2
|
+
import { RakeDbAst } from '../ast';
|
|
3
|
+
import { columnsByType, ColumnsShape, instantiateColumn } from 'pqb';
|
|
4
|
+
|
|
5
|
+
export const structureToAst = async (db: DbStructure): Promise<RakeDbAst[]> => {
|
|
6
|
+
const ast: RakeDbAst[] = [];
|
|
7
|
+
|
|
8
|
+
const [
|
|
9
|
+
schemas,
|
|
10
|
+
tables,
|
|
11
|
+
allColumns,
|
|
12
|
+
allConstraints,
|
|
13
|
+
allIndexes,
|
|
14
|
+
allForeignKeys,
|
|
15
|
+
extensions,
|
|
16
|
+
] = await Promise.all([
|
|
17
|
+
db.getSchemas(),
|
|
18
|
+
db.getTables(),
|
|
19
|
+
db.getColumns(),
|
|
20
|
+
db.getConstraints(),
|
|
21
|
+
db.getIndexes(),
|
|
22
|
+
db.getForeignKeys(),
|
|
23
|
+
db.getExtensions(),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
for (const name of schemas) {
|
|
27
|
+
if (name === 'public') continue;
|
|
28
|
+
|
|
29
|
+
ast.push({
|
|
30
|
+
type: 'schema',
|
|
31
|
+
action: 'create',
|
|
32
|
+
name,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const table of tables) {
|
|
37
|
+
const { schemaName, name } = table;
|
|
38
|
+
|
|
39
|
+
const belongsToTable = makeBelongsToTable(schemaName, name);
|
|
40
|
+
|
|
41
|
+
const columns = allColumns.filter(belongsToTable);
|
|
42
|
+
const tableConstraints = allConstraints.filter(belongsToTable);
|
|
43
|
+
const primaryKey = tableConstraints.find(
|
|
44
|
+
(item) => item.type === 'PRIMARY KEY',
|
|
45
|
+
);
|
|
46
|
+
const tableIndexes = allIndexes.filter(belongsToTable);
|
|
47
|
+
const tableForeignKeys = allForeignKeys.filter(belongsToTable);
|
|
48
|
+
|
|
49
|
+
const shape: ColumnsShape = {};
|
|
50
|
+
for (const item of columns) {
|
|
51
|
+
const klass = columnsByType[item.type];
|
|
52
|
+
let column = instantiateColumn(klass, item);
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
primaryKey?.columnNames.length === 1 &&
|
|
56
|
+
primaryKey?.columnNames[0] === item.name
|
|
57
|
+
) {
|
|
58
|
+
column = column.primaryKey();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const index = tableIndexes.find(
|
|
62
|
+
(it) =>
|
|
63
|
+
it.isPrimary === false &&
|
|
64
|
+
it.columnNames.length === 1 &&
|
|
65
|
+
it.columnNames[0] === item.name,
|
|
66
|
+
);
|
|
67
|
+
if (index) {
|
|
68
|
+
column = column.index({
|
|
69
|
+
name: index.name,
|
|
70
|
+
unique: index.isUnique,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const foreignKey = tableForeignKeys.find(
|
|
75
|
+
(it) => it.columnNames.length === 1 && it.columnNames[0] === item.name,
|
|
76
|
+
);
|
|
77
|
+
if (foreignKey) {
|
|
78
|
+
column = column.foreignKey(
|
|
79
|
+
foreignKey.foreignTableName,
|
|
80
|
+
foreignKey.foreignColumnNames[0],
|
|
81
|
+
{
|
|
82
|
+
name: foreignKey.name,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
shape[item.name] = column;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ast.push({
|
|
91
|
+
type: 'table',
|
|
92
|
+
action: 'create',
|
|
93
|
+
schema: schemaName === 'public' ? undefined : schemaName,
|
|
94
|
+
name: name,
|
|
95
|
+
shape,
|
|
96
|
+
noPrimaryKey: primaryKey ? 'error' : 'ignore',
|
|
97
|
+
primaryKey:
|
|
98
|
+
primaryKey && primaryKey.columnNames.length > 1
|
|
99
|
+
? {
|
|
100
|
+
columns: primaryKey.columnNames,
|
|
101
|
+
options:
|
|
102
|
+
primaryKey.name === `${name}_pkey`
|
|
103
|
+
? undefined
|
|
104
|
+
: { name: primaryKey.name },
|
|
105
|
+
}
|
|
106
|
+
: undefined,
|
|
107
|
+
indexes: tableIndexes
|
|
108
|
+
.filter((index) => index.columnNames.length > 1)
|
|
109
|
+
.map((index) => ({
|
|
110
|
+
columns: index.columnNames.map((column) => ({ column })),
|
|
111
|
+
options: {
|
|
112
|
+
name: index.name,
|
|
113
|
+
unique: index.isUnique,
|
|
114
|
+
},
|
|
115
|
+
})),
|
|
116
|
+
foreignKeys: tableForeignKeys
|
|
117
|
+
.filter((it) => it.columnNames.length > 1)
|
|
118
|
+
.map((it) => ({
|
|
119
|
+
columns: it.columnNames,
|
|
120
|
+
fnOrTable: it.foreignTableName,
|
|
121
|
+
foreignColumns: it.foreignColumnNames,
|
|
122
|
+
options: {
|
|
123
|
+
name: it.name,
|
|
124
|
+
},
|
|
125
|
+
})),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const it of extensions) {
|
|
130
|
+
ast.push({
|
|
131
|
+
type: 'extension',
|
|
132
|
+
action: 'create',
|
|
133
|
+
name: it.name,
|
|
134
|
+
schema: it.schemaName === 'public' ? undefined : it.schemaName,
|
|
135
|
+
version: it.version,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return ast;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const makeBelongsToTable =
|
|
143
|
+
(schema: string | undefined, table: string) =>
|
|
144
|
+
(item: { schemaName: string; tableName: string }) =>
|
|
145
|
+
item.schemaName === schema && item.tableName === table;
|