ormbridge 0.14.0 → 0.15.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.
- package/README.md +21 -21
- package/dist/cliRunner.js +2 -2
- package/dist/cliRunner.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/emitters/sequelize.d.ts +16 -3
- package/dist/emitters/sequelize.js +270 -45
- package/dist/emitters/sequelize.js.map +1 -1
- package/dist/emitters/sequelize7.d.ts +68 -0
- package/dist/emitters/sequelize7.js +199 -0
- package/dist/emitters/sequelize7.js.map +1 -0
- package/dist/emitters/sequelizeText.d.ts +12 -0
- package/dist/emitters/sequelizeText.js +37 -0
- package/dist/emitters/sequelizeText.js.map +1 -0
- package/dist/formats.d.ts +2 -2
- package/dist/formats.js +4 -3
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/io.js +44 -6
- package/dist/io.js.map +1 -1
- package/dist/parsers/efcore.js +60 -3
- package/dist/parsers/efcore.js.map +1 -1
- package/dist/parsers/efcoreAnnotations.js +1 -1
- package/dist/parsers/efcoreAnnotations.js.map +1 -1
- package/dist/parsers/efcoreBuild.d.ts +1 -0
- package/dist/parsers/efcoreBuild.js +15 -4
- package/dist/parsers/efcoreBuild.js.map +1 -1
- package/dist/parsers/efcoreEntities.js +90 -22
- package/dist/parsers/efcoreEntities.js.map +1 -1
- package/dist/parsers/efcoreMigrationHistory.d.ts +17 -0
- package/dist/parsers/efcoreMigrationHistory.js +105 -0
- package/dist/parsers/efcoreMigrationHistory.js.map +1 -0
- package/dist/parsers/efcoreMigrationReplay.d.ts +25 -0
- package/dist/parsers/efcoreMigrationReplay.js +790 -0
- package/dist/parsers/efcoreMigrationReplay.js.map +1 -0
- package/dist/parsers/efcoreMigrationSchema.d.ts +14 -0
- package/dist/parsers/efcoreMigrationSchema.js +550 -0
- package/dist/parsers/efcoreMigrationSchema.js.map +1 -0
- package/dist/parsers/efcoreMigrationState.d.ts +82 -0
- package/dist/parsers/efcoreMigrationState.js +104 -0
- package/dist/parsers/efcoreMigrationState.js.map +1 -0
- package/dist/parsers/efcoreModel.d.ts +18 -2
- package/dist/parsers/efcoreOwned.d.ts +12 -0
- package/dist/parsers/efcoreOwned.js +139 -0
- package/dist/parsers/efcoreOwned.js.map +1 -0
- package/dist/parsers/efcoreSnapshot.d.ts +85 -0
- package/dist/parsers/efcoreSnapshot.js +791 -0
- package/dist/parsers/efcoreSnapshot.js.map +1 -0
- package/dist/parsers/efcoreSteps.js +28 -3
- package/dist/parsers/efcoreSteps.js.map +1 -1
- package/dist/parsers/sequelize.js +37 -10
- package/dist/parsers/sequelize.js.map +1 -1
- package/dist/parsers/sequelizeAssociations.js +13 -2
- package/dist/parsers/sequelizeAssociations.js.map +1 -1
- package/dist/parsers/sequelizeCollect.d.ts +7 -2
- package/dist/parsers/sequelizeCollect.js +120 -9
- package/dist/parsers/sequelizeCollect.js.map +1 -1
- package/dist/parsers/sequelizeMigrationState.d.ts +75 -0
- package/dist/parsers/sequelizeMigrationState.js +330 -0
- package/dist/parsers/sequelizeMigrationState.js.map +1 -0
- package/dist/parsers/sequelizeMigrations.d.ts +14 -0
- package/dist/parsers/sequelizeMigrations.js +1123 -0
- package/dist/parsers/sequelizeMigrations.js.map +1 -0
- package/dist/parsers/sequelizeModel.d.ts +19 -2
- package/dist/styles.d.ts +3 -3
- package/dist/styles.js +4 -4
- package/dist/styles.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
import { singularize, toPascalCase, toSnakeCase } from '../naming.js';
|
|
2
|
+
import { addColumn, addConstraint, addIndex, changeColumn, createTable, defaultConstraintName, defaultIndexName, dropTable, findColumn, findTable, newState, removeColumn, removeConstraint, removeIndex, renameColumn, renameTable, } from './sequelizeMigrationState.js';
|
|
3
|
+
import { argsOf, boolOf, indexFieldOf, objectEntries, readAttribute, resolveNode, stringOf, stripWrappers, valueOf, } from './sequelizeCollect.js';
|
|
4
|
+
import { evaluateNode, } from './typescriptSyntax.js';
|
|
5
|
+
/**
|
|
6
|
+
* Replays `sequelize-cli` migrations: the `up` function of each file runs, in file-name order, against the
|
|
7
|
+
* schema the earlier files built. Nothing is run; the `queryInterface` calls are read from the syntax tree.
|
|
8
|
+
* The result is one model per table that is left, with the columns the migrations gave it, so the reader
|
|
9
|
+
* that turns models into the shared schema also turns migrations into one.
|
|
10
|
+
*/
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Finding the migration of a file
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const FUNCTION_NODES = new Set([
|
|
15
|
+
'arrow_function',
|
|
16
|
+
'function_expression',
|
|
17
|
+
'function',
|
|
18
|
+
'function_declaration',
|
|
19
|
+
'generator_function',
|
|
20
|
+
'generator_function_declaration',
|
|
21
|
+
'method_definition',
|
|
22
|
+
]);
|
|
23
|
+
/** The function-like declarations of a file by name, so `up: addTables` and `await addTables(queryInterface)` work. */
|
|
24
|
+
function declaredFunctions(file) {
|
|
25
|
+
const found = new Map();
|
|
26
|
+
for (const node of file.root.namedChildren) {
|
|
27
|
+
const declaration = node.type === 'export_statement'
|
|
28
|
+
? (node.namedChildren.find((child) => child.type === 'function_declaration' ||
|
|
29
|
+
child.type === 'lexical_declaration') ?? node)
|
|
30
|
+
: node;
|
|
31
|
+
if (declaration.type === 'function_declaration') {
|
|
32
|
+
const name = declaration.childForFieldName('name');
|
|
33
|
+
if (name !== null) {
|
|
34
|
+
found.set(name.text, declaration);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const [name, node] of file.consts) {
|
|
39
|
+
const value = stripWrappers(node);
|
|
40
|
+
if (FUNCTION_NODES.has(value.type) && !found.has(name)) {
|
|
41
|
+
found.set(name, value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
function asFunction(node, functions) {
|
|
47
|
+
if (node === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const stripped = stripWrappers(node);
|
|
51
|
+
if (FUNCTION_NODES.has(stripped.type)) {
|
|
52
|
+
return stripped;
|
|
53
|
+
}
|
|
54
|
+
return stripped.type === 'identifier'
|
|
55
|
+
? functions.get(stripped.text)
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
/** The name a parameter binds: `queryInterface`, or `queryInterface` from `{ context: queryInterface }`. */
|
|
59
|
+
function parameterName(parameter) {
|
|
60
|
+
if (parameter === undefined) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const pattern = parameter.childForFieldName('pattern') ?? parameter;
|
|
64
|
+
if (pattern.type === 'identifier') {
|
|
65
|
+
return pattern.text;
|
|
66
|
+
}
|
|
67
|
+
if (pattern.type === 'object_pattern') {
|
|
68
|
+
for (const member of pattern.namedChildren) {
|
|
69
|
+
if (member.type === 'shorthand_property_identifier_pattern') {
|
|
70
|
+
if (member.text === 'context') {
|
|
71
|
+
return 'context';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (member.type === 'pair_pattern') {
|
|
75
|
+
const key = member.childForFieldName('key');
|
|
76
|
+
const value = member.childForFieldName('value');
|
|
77
|
+
if (key !== null && key.text === 'context' && value !== null) {
|
|
78
|
+
return value.type === 'identifier' ? value.text : undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/** The parameters of a function-like node, in order. */
|
|
86
|
+
function parametersOf(node) {
|
|
87
|
+
const parameters = node.childForFieldName('parameters') ?? node.childForFieldName('parameter');
|
|
88
|
+
if (parameters === null) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return parameters.type === 'identifier'
|
|
92
|
+
? [parameters]
|
|
93
|
+
: parameters.namedChildren;
|
|
94
|
+
}
|
|
95
|
+
function upFromObject(object, functions, ctx) {
|
|
96
|
+
return asFunction(objectEntries(object, ctx).entries.get('up'), functions);
|
|
97
|
+
}
|
|
98
|
+
/** Finds `up` in `module.exports = { up }`, `exports.up = ...`, `export async function up` and `export default { up }`. */
|
|
99
|
+
function findUp(file, ctx) {
|
|
100
|
+
const functions = declaredFunctions(file);
|
|
101
|
+
let node;
|
|
102
|
+
for (const statement of file.root.namedChildren) {
|
|
103
|
+
if (statement.type === 'expression_statement') {
|
|
104
|
+
const assignment = statement.namedChildren[0];
|
|
105
|
+
if (assignment === undefined ||
|
|
106
|
+
assignment.type !== 'assignment_expression') {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const left = (assignment.childForFieldName('left')?.text ?? '').replace(/\s+/g, '');
|
|
110
|
+
const right = assignment.childForFieldName('right');
|
|
111
|
+
if (right === null) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (/^(?:module\.)?exports\.up$/.test(left)) {
|
|
115
|
+
node = asFunction(right, functions);
|
|
116
|
+
}
|
|
117
|
+
else if (left === 'module.exports' ||
|
|
118
|
+
left === 'exports.default' ||
|
|
119
|
+
left === 'module.exports.default') {
|
|
120
|
+
const value = resolveNode(right, ctx);
|
|
121
|
+
node =
|
|
122
|
+
value.type === 'object'
|
|
123
|
+
? upFromObject(value, functions, ctx)
|
|
124
|
+
: undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (statement.type === 'export_statement') {
|
|
128
|
+
const declaration = statement.namedChildren.find((child) => child.type === 'function_declaration' ||
|
|
129
|
+
child.type === 'lexical_declaration');
|
|
130
|
+
if (declaration?.type === 'function_declaration') {
|
|
131
|
+
if (declaration.childForFieldName('name')?.text === 'up') {
|
|
132
|
+
node = declaration;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (declaration?.type === 'lexical_declaration') {
|
|
136
|
+
for (const declarator of declaration.namedChildren) {
|
|
137
|
+
if (declarator.childForFieldName('name')?.text === 'up') {
|
|
138
|
+
node = asFunction(declarator.childForFieldName('value') ?? undefined, functions);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (/^export\s+default\b/.test(statement.text)) {
|
|
143
|
+
const value = statement.childForFieldName('value');
|
|
144
|
+
const resolved = value === null ? undefined : resolveNode(value, ctx);
|
|
145
|
+
node =
|
|
146
|
+
resolved?.type === 'object'
|
|
147
|
+
? upFromObject(resolved, functions, ctx)
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (node !== undefined) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// `module.exports = { up, down }` and `export { up, down }` name functions declared elsewhere in the file.
|
|
156
|
+
node ??= functions.get('up');
|
|
157
|
+
if (node === undefined) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const first = parameterName(parametersOf(node)[0]) ?? 'queryInterface';
|
|
161
|
+
return { node, queryInterfaces: [first] };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* True when the file is a migration: it has an `up` function that works on a query interface. The text test
|
|
165
|
+
* keeps ordinary models (which never call `queryInterface` or `createTable`) out of the tree walk.
|
|
166
|
+
*/
|
|
167
|
+
export function isMigrationFile(file, ctx) {
|
|
168
|
+
if (!/queryInterface|createTable|addColumn|context\b/.test(file.root.text)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return findUp(file, ctx) !== undefined;
|
|
172
|
+
}
|
|
173
|
+
function tally(counter, where) {
|
|
174
|
+
return counter === undefined
|
|
175
|
+
? { count: 1, first: where }
|
|
176
|
+
: { count: counter.count + 1, first: counter.first };
|
|
177
|
+
}
|
|
178
|
+
/** Queries that only look at the schema or at data and change nothing the shared schema holds. */
|
|
179
|
+
const READ_ONLY_CALLS = new Set([
|
|
180
|
+
'describeTable',
|
|
181
|
+
'showAllTables',
|
|
182
|
+
'tableExists',
|
|
183
|
+
'showIndex',
|
|
184
|
+
'showConstraint',
|
|
185
|
+
'getForeignKeysForTables',
|
|
186
|
+
'getForeignKeyReferencesForTable',
|
|
187
|
+
'quoteIdentifier',
|
|
188
|
+
'quoteIdentifiers',
|
|
189
|
+
'quoteTable',
|
|
190
|
+
'escape',
|
|
191
|
+
'select',
|
|
192
|
+
'rawSelect',
|
|
193
|
+
'createSchema',
|
|
194
|
+
'dropSchema',
|
|
195
|
+
'showAllSchemas',
|
|
196
|
+
'databaseVersion',
|
|
197
|
+
'getDatabaseName',
|
|
198
|
+
'getQueryInterface',
|
|
199
|
+
'queryGenerator',
|
|
200
|
+
]);
|
|
201
|
+
/** Calls that read or write rows. */
|
|
202
|
+
const DATA_CALLS = new Set([
|
|
203
|
+
'bulkInsert',
|
|
204
|
+
'bulkUpdate',
|
|
205
|
+
'bulkDelete',
|
|
206
|
+
'insert',
|
|
207
|
+
'update',
|
|
208
|
+
'upsert',
|
|
209
|
+
'delete',
|
|
210
|
+
'increment',
|
|
211
|
+
'decrement',
|
|
212
|
+
]);
|
|
213
|
+
/** The key under which calls made from a list that is computed at run time are counted. */
|
|
214
|
+
const COMPUTED_LIST = '<computed list>';
|
|
215
|
+
const CONTROL_STATEMENTS = new Set([
|
|
216
|
+
'if_statement',
|
|
217
|
+
'for_statement',
|
|
218
|
+
'for_in_statement',
|
|
219
|
+
'while_statement',
|
|
220
|
+
'do_statement',
|
|
221
|
+
'switch_statement',
|
|
222
|
+
]);
|
|
223
|
+
const CLAUSE_STATEMENTS = new Set([
|
|
224
|
+
'else_clause',
|
|
225
|
+
'catch_clause',
|
|
226
|
+
'finally_clause',
|
|
227
|
+
'switch_body',
|
|
228
|
+
'switch_case',
|
|
229
|
+
'switch_default',
|
|
230
|
+
]);
|
|
231
|
+
function line(node) {
|
|
232
|
+
return node.startPosition.row + 1;
|
|
233
|
+
}
|
|
234
|
+
function where(scope, node) {
|
|
235
|
+
return `${scope.file.label}:${line(node)}`;
|
|
236
|
+
}
|
|
237
|
+
function warn(run, message) {
|
|
238
|
+
if (!run.project.warnings.includes(message)) {
|
|
239
|
+
run.project.warnings.push(message);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function describeTable(table) {
|
|
243
|
+
return `"${table.schema === undefined ? table.name : `${table.schema}.${table.name}`}"`;
|
|
244
|
+
}
|
|
245
|
+
/** A table named by a string, a `{ tableName, schema }` object or a constant holding either. */
|
|
246
|
+
function readTableRef(node, scope) {
|
|
247
|
+
if (node === undefined) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const value = evaluateNode(resolveNode(node, scope.ctx));
|
|
251
|
+
if (value.kind === 'string' && value.value !== '') {
|
|
252
|
+
return { name: value.value };
|
|
253
|
+
}
|
|
254
|
+
if (value.kind === 'object') {
|
|
255
|
+
const name = value.properties['tableName'];
|
|
256
|
+
const schema = value.properties['schema'];
|
|
257
|
+
if (name !== undefined && name.kind === 'string') {
|
|
258
|
+
return {
|
|
259
|
+
name: name.value,
|
|
260
|
+
...(schema !== undefined &&
|
|
261
|
+
schema.kind === 'string' &&
|
|
262
|
+
schema.value !== ''
|
|
263
|
+
? { schema: schema.value }
|
|
264
|
+
: {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
function readString(node, scope) {
|
|
271
|
+
return node === undefined ? undefined : stringOf(node, scope.ctx);
|
|
272
|
+
}
|
|
273
|
+
function readStringList(node, scope) {
|
|
274
|
+
if (node === undefined) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
const resolved = resolveNode(node, scope.ctx);
|
|
278
|
+
if (resolved.type !== 'array') {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const names = [];
|
|
282
|
+
for (const item of resolved.namedChildren) {
|
|
283
|
+
const text = stringOf(item, scope.ctx);
|
|
284
|
+
if (text === undefined) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
names.push(text);
|
|
288
|
+
}
|
|
289
|
+
return names;
|
|
290
|
+
}
|
|
291
|
+
function skip(run, scope, call, action, reason) {
|
|
292
|
+
warn(run, `${where(scope, call)}: ${action}: ${reason}; the statement was skipped.`);
|
|
293
|
+
}
|
|
294
|
+
/** The column of a migration, as `sequelize.define` would read the same definition. */
|
|
295
|
+
function readColumn(name, node, scope, run, table) {
|
|
296
|
+
const attribute = readAttribute(name, node, scope.ctx);
|
|
297
|
+
// The names of a migration are the database's: there is no `field`.
|
|
298
|
+
delete attribute.field;
|
|
299
|
+
const label = `${where(scope, node)}: table "${table}", column "${name}"`;
|
|
300
|
+
if (attribute.unique?.name !== undefined) {
|
|
301
|
+
warn(run, `${label}: unique: '${attribute.unique.name}' does nothing in a migration (queryInterface only reads unique: true), so the column is not unique.`);
|
|
302
|
+
delete attribute.unique;
|
|
303
|
+
}
|
|
304
|
+
const resolved = resolveNode(node, scope.ctx);
|
|
305
|
+
if (resolved.type === 'object') {
|
|
306
|
+
const references = objectEntries(resolved, scope.ctx).entries.get('references');
|
|
307
|
+
const target = references === undefined ? undefined : valueOf(references, scope.ctx);
|
|
308
|
+
if (target !== undefined && target.kind === 'string') {
|
|
309
|
+
attribute.references = { model: target };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const model = attribute.references?.model;
|
|
313
|
+
if (model !== undefined && model.kind !== 'string') {
|
|
314
|
+
const table = model.kind === 'object' &&
|
|
315
|
+
model.properties['tableName']?.kind === 'string'
|
|
316
|
+
? model.properties['tableName'].value
|
|
317
|
+
: undefined;
|
|
318
|
+
if (table === undefined) {
|
|
319
|
+
warn(run, `${label}: the table in references could not be read statically, so no foreign key was created.`);
|
|
320
|
+
delete attribute.references;
|
|
321
|
+
delete attribute.onDelete;
|
|
322
|
+
delete attribute.onUpdate;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
attribute.references = {
|
|
326
|
+
...attribute.references,
|
|
327
|
+
model: { kind: 'string', value: table },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return attribute;
|
|
332
|
+
}
|
|
333
|
+
// -- createTable, addColumn and the other calls ---------------------------------------------------
|
|
334
|
+
function applyCreateTable(call, args, scope, run) {
|
|
335
|
+
const ref = readTableRef(args[0], scope);
|
|
336
|
+
if (ref === undefined) {
|
|
337
|
+
warn(run, `${where(scope, call)}: createTable: the table name cannot be read statically; the statement was skipped.`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const options = args[2] === undefined
|
|
341
|
+
? new Map()
|
|
342
|
+
: objectEntries(args[2], scope.ctx).entries;
|
|
343
|
+
const schema = ref.schema ?? readString(options.get('schema'), scope);
|
|
344
|
+
const table = {
|
|
345
|
+
name: ref.name,
|
|
346
|
+
...(schema === undefined ? {} : { schema }),
|
|
347
|
+
file: scope.file,
|
|
348
|
+
columns: [],
|
|
349
|
+
indexes: [],
|
|
350
|
+
constraints: [],
|
|
351
|
+
};
|
|
352
|
+
const attributes = args[1] === undefined ? undefined : objectEntries(args[1], scope.ctx);
|
|
353
|
+
if (attributes === undefined ||
|
|
354
|
+
(attributes.entries.size === 0 && attributes.incomplete)) {
|
|
355
|
+
warn(run, `${where(scope, call)}: createTable ${describeTable(table)}: the columns cannot be read statically; the table has none.`);
|
|
356
|
+
}
|
|
357
|
+
else if (attributes.incomplete) {
|
|
358
|
+
warn(run, `${where(scope, call)}: createTable ${describeTable(table)}: some columns are built from values that cannot be read statically and are missing.`);
|
|
359
|
+
}
|
|
360
|
+
for (const [name, node] of attributes?.entries ?? []) {
|
|
361
|
+
table.columns.push(readColumn(name, node, scope, run, ref.name));
|
|
362
|
+
}
|
|
363
|
+
const reason = createTable(run.state, table);
|
|
364
|
+
if (reason !== undefined) {
|
|
365
|
+
skip(run, scope, call, `createTable ${describeTable(table)}`, reason);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
run.touched.push(ref.name);
|
|
369
|
+
const keys = options.get('uniqueKeys');
|
|
370
|
+
if (keys !== undefined) {
|
|
371
|
+
for (const [name, value] of objectEntries(keys, scope.ctx).entries) {
|
|
372
|
+
const fields = readStringList(objectEntries(value, scope.ctx).entries.get('fields'), scope);
|
|
373
|
+
const known = fields !== undefined &&
|
|
374
|
+
fields.every((field) => findColumn(table, field) !== undefined);
|
|
375
|
+
if (fields === undefined || !known) {
|
|
376
|
+
warn(run, `${where(scope, call)}: createTable ${describeTable(table)}: the unique key "${name}" lists columns that cannot be read or do not exist; it was skipped.`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
table.constraints.push({ kind: 'unique', name, columns: fields });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function applyAddColumn(call, args, scope, run) {
|
|
384
|
+
const ref = readTableRef(args[0], scope);
|
|
385
|
+
const name = readString(args[1], scope);
|
|
386
|
+
if (ref === undefined || name === undefined || args[2] === undefined) {
|
|
387
|
+
warn(run, `${where(scope, call)}: addColumn: the table or column name cannot be read statically; the statement was skipped.`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const table = findTable(run.state, ref.name, ref.schema);
|
|
391
|
+
if (table === undefined) {
|
|
392
|
+
skip(run, scope, call, `addColumn ${describeTable(ref)}, column "${name}"`, `the table does not exist at this point`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const reason = addColumn(table, readColumn(name, args[2], scope, run, table.name));
|
|
396
|
+
if (reason !== undefined) {
|
|
397
|
+
skip(run, scope, call, `addColumn ${describeTable(table)}, column "${name}"`, reason);
|
|
398
|
+
}
|
|
399
|
+
run.touched.push(table.name);
|
|
400
|
+
}
|
|
401
|
+
function applyRemoveColumn(call, args, scope, run) {
|
|
402
|
+
const ref = readTableRef(args[0], scope);
|
|
403
|
+
const name = readString(args[1], scope);
|
|
404
|
+
if (ref === undefined || name === undefined) {
|
|
405
|
+
warn(run, `${where(scope, call)}: removeColumn: the table or column name cannot be read statically; the statement was skipped.`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const table = findTable(run.state, ref.name, ref.schema);
|
|
409
|
+
const reason = table === undefined
|
|
410
|
+
? 'the table does not exist at this point'
|
|
411
|
+
: removeColumn(table, name);
|
|
412
|
+
if (reason !== undefined) {
|
|
413
|
+
skip(run, scope, call, `removeColumn ${describeTable(ref)}, column "${name}"`, reason);
|
|
414
|
+
}
|
|
415
|
+
run.touched.push(ref.name);
|
|
416
|
+
}
|
|
417
|
+
function applyRenameColumn(call, args, scope, run) {
|
|
418
|
+
const ref = readTableRef(args[0], scope);
|
|
419
|
+
const from = readString(args[1], scope);
|
|
420
|
+
const to = readString(args[2], scope);
|
|
421
|
+
if (ref === undefined || from === undefined || to === undefined) {
|
|
422
|
+
warn(run, `${where(scope, call)}: renameColumn: the table or column names cannot be read statically; the statement was skipped.`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const table = findTable(run.state, ref.name, ref.schema);
|
|
426
|
+
const reason = table === undefined
|
|
427
|
+
? 'the table does not exist at this point'
|
|
428
|
+
: renameColumn(run.state, table, from, to);
|
|
429
|
+
if (reason !== undefined) {
|
|
430
|
+
skip(run, scope, call, `renameColumn ${describeTable(ref)}, column "${from}"`, reason);
|
|
431
|
+
}
|
|
432
|
+
run.touched.push(ref.name);
|
|
433
|
+
}
|
|
434
|
+
function applyRenameTable(call, args, scope, run) {
|
|
435
|
+
const from = readTableRef(args[0], scope);
|
|
436
|
+
const to = readTableRef(args[1], scope);
|
|
437
|
+
if (from === undefined || to === undefined) {
|
|
438
|
+
warn(run, `${where(scope, call)}: renameTable: the table names cannot be read statically; the statement was skipped.`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const reason = renameTable(run.state, from.name, to.name, from.schema);
|
|
442
|
+
if (reason !== undefined) {
|
|
443
|
+
skip(run, scope, call, `renameTable ${describeTable(from)}`, reason);
|
|
444
|
+
}
|
|
445
|
+
run.touched.push(from.name);
|
|
446
|
+
}
|
|
447
|
+
function applyDropTable(call, args, scope, run) {
|
|
448
|
+
const ref = readTableRef(args[0], scope);
|
|
449
|
+
if (ref === undefined) {
|
|
450
|
+
warn(run, `${where(scope, call)}: dropTable: the table name cannot be read statically; the statement was skipped.`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const { dropped, dangling } = dropTable(run.state, ref.name, ref.schema);
|
|
454
|
+
if (dropped === undefined) {
|
|
455
|
+
skip(run, scope, call, `dropTable ${describeTable(ref)}`, 'the table does not exist at this point');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
run.touched.push(ref.name);
|
|
459
|
+
const cascade = args[1] !== undefined &&
|
|
460
|
+
boolOf(objectEntries(args[1], scope.ctx).entries.get('cascade'), scope.ctx) === true;
|
|
461
|
+
if (dangling.length > 0 && !cascade) {
|
|
462
|
+
warn(run, `${where(scope, call)}: dropTable ${describeTable(ref)}: the foreign keys on ${dangling.join(', ')} pointed at the table; the columns stay as plain columns.`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function applyChangeColumn(call, args, scope, run) {
|
|
466
|
+
const ref = readTableRef(args[0], scope);
|
|
467
|
+
const name = readString(args[1], scope);
|
|
468
|
+
if (ref === undefined || name === undefined || args[2] === undefined) {
|
|
469
|
+
warn(run, `${where(scope, call)}: changeColumn: the table or column name cannot be read statically; the statement was skipped.`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const table = findTable(run.state, ref.name, ref.schema);
|
|
473
|
+
const action = `changeColumn ${describeTable(ref)}, column "${name}"`;
|
|
474
|
+
if (table === undefined) {
|
|
475
|
+
skip(run, scope, call, action, 'the table does not exist at this point');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const changes = readColumn(name, args[2], scope, run, table.name);
|
|
479
|
+
const spec = resolveNode(args[2], scope.ctx);
|
|
480
|
+
const named = new Set(spec.type === 'object'
|
|
481
|
+
? objectEntries(spec, scope.ctx).entries.keys()
|
|
482
|
+
: ['type']);
|
|
483
|
+
const column = findColumn(table, name);
|
|
484
|
+
if (column !== undefined &&
|
|
485
|
+
((!named.has('defaultValue') && column.defaultValue !== undefined) ||
|
|
486
|
+
(!named.has('allowNull') && column.allowNull === false))) {
|
|
487
|
+
warn(run, `${where(scope, call)}: ${action}: the call does not name ${[
|
|
488
|
+
...(named.has('allowNull') || column.allowNull !== false
|
|
489
|
+
? []
|
|
490
|
+
: ['allowNull']),
|
|
491
|
+
...(named.has('defaultValue') || column.defaultValue === undefined
|
|
492
|
+
? []
|
|
493
|
+
: ['defaultValue']),
|
|
494
|
+
].join(' or ')}; the column keeps it (SQLite does that, PostgreSQL and MySQL reset what the call leaves out), so the result may differ from the real database.`);
|
|
495
|
+
}
|
|
496
|
+
const reason = changeColumn(table, name, changes, named);
|
|
497
|
+
if (reason !== undefined) {
|
|
498
|
+
skip(run, scope, call, action, reason);
|
|
499
|
+
}
|
|
500
|
+
run.touched.push(table.name);
|
|
501
|
+
}
|
|
502
|
+
/** The fields of an index or constraint: strings, or `{ name, order, length }` objects. */
|
|
503
|
+
function readIndexFields(node, scope) {
|
|
504
|
+
if (node === undefined) {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
const resolved = resolveNode(node, scope.ctx);
|
|
508
|
+
if (resolved.type !== 'array') {
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
const fields = [];
|
|
512
|
+
let hasExpression = false;
|
|
513
|
+
for (const item of resolved.namedChildren) {
|
|
514
|
+
const field = indexFieldOf(item, scope.ctx);
|
|
515
|
+
if (field === 'expression') {
|
|
516
|
+
hasExpression = true;
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
fields.push(field);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return { fields, hasExpression };
|
|
523
|
+
}
|
|
524
|
+
function applyAddIndex(call, args, scope, run) {
|
|
525
|
+
const ref = readTableRef(args[0], scope);
|
|
526
|
+
if (ref === undefined) {
|
|
527
|
+
warn(run, `${where(scope, call)}: addIndex: the table name cannot be read statically; the statement was skipped.`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// addIndex(table, fields, options) or addIndex(table, { fields, ...options })
|
|
531
|
+
const second = args[1] === undefined ? undefined : resolveNode(args[1], scope.ctx);
|
|
532
|
+
const optionsNode = second?.type === 'array' ? args[2] : args[1];
|
|
533
|
+
const options = optionsNode === undefined
|
|
534
|
+
? new Map()
|
|
535
|
+
: objectEntries(optionsNode, scope.ctx).entries;
|
|
536
|
+
const list = readIndexFields(second?.type === 'array' ? args[1] : options.get('fields'), scope);
|
|
537
|
+
const action = `addIndex ${describeTable(ref)}`;
|
|
538
|
+
const table = findTable(run.state, ref.name, ref.schema);
|
|
539
|
+
if (table === undefined) {
|
|
540
|
+
skip(run, scope, call, action, 'the table does not exist at this point');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (list === undefined) {
|
|
544
|
+
skip(run, scope, call, action, 'the columns cannot be read statically');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const missing = list.fields.filter((field) => findColumn(table, field.name) === undefined);
|
|
548
|
+
if (missing.length > 0) {
|
|
549
|
+
skip(run, scope, call, `${action} (${list.fields.map((field) => field.name).join(', ')})`, `the column "${missing[0]?.name ?? ''}" does not exist at this point`);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const explicitName = readString(options.get('name'), scope);
|
|
553
|
+
const type = readString(options.get('type'), scope)?.toUpperCase();
|
|
554
|
+
const using = readString(options.get('using'), scope);
|
|
555
|
+
const names = list.fields.map((field) => field.name);
|
|
556
|
+
const index = {
|
|
557
|
+
fields: list.fields,
|
|
558
|
+
unique: boolOf(options.get('unique'), scope.ctx) === true || type === 'UNIQUE',
|
|
559
|
+
...(using === undefined ? {} : { using }),
|
|
560
|
+
...(type === 'FULLTEXT' || type === 'SPATIAL' ? { type } : {}),
|
|
561
|
+
isPartial: options.has('where'),
|
|
562
|
+
hasExpression: list.hasExpression,
|
|
563
|
+
};
|
|
564
|
+
const entry = {
|
|
565
|
+
name: explicitName ?? defaultIndexName(table.name, names),
|
|
566
|
+
isExplicit: explicitName !== undefined,
|
|
567
|
+
index: explicitName === undefined ? index : { ...index, name: explicitName },
|
|
568
|
+
};
|
|
569
|
+
const reason = addIndex(table, entry);
|
|
570
|
+
if (reason !== undefined) {
|
|
571
|
+
skip(run, scope, call, action, reason);
|
|
572
|
+
}
|
|
573
|
+
run.touched.push(table.name);
|
|
574
|
+
}
|
|
575
|
+
function applyRemoveIndex(call, args, scope, run) {
|
|
576
|
+
const ref = readTableRef(args[0], scope);
|
|
577
|
+
const target = args[1] === undefined ? undefined : resolveNode(args[1], scope.ctx);
|
|
578
|
+
const nameOrFields = target === undefined
|
|
579
|
+
? undefined
|
|
580
|
+
: target.type === 'array'
|
|
581
|
+
? readStringList(target, scope)
|
|
582
|
+
: readString(target, scope);
|
|
583
|
+
if (ref === undefined || nameOrFields === undefined) {
|
|
584
|
+
warn(run, `${where(scope, call)}: removeIndex: the table or index cannot be read statically; the statement was skipped.`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const table = findTable(run.state, ref.name, ref.schema);
|
|
588
|
+
const reason = table === undefined
|
|
589
|
+
? 'the table does not exist at this point'
|
|
590
|
+
: removeIndex(table, typeof nameOrFields === 'string' ? nameOrFields : nameOrFields);
|
|
591
|
+
if (reason !== undefined) {
|
|
592
|
+
skip(run, scope, call, `removeIndex ${describeTable(ref)}`, reason);
|
|
593
|
+
}
|
|
594
|
+
run.touched.push(ref.name);
|
|
595
|
+
}
|
|
596
|
+
const CONSTRAINT_KINDS = {
|
|
597
|
+
UNIQUE: 'unique',
|
|
598
|
+
'PRIMARY KEY': 'primary key',
|
|
599
|
+
'FOREIGN KEY': 'foreign key',
|
|
600
|
+
CHECK: 'check',
|
|
601
|
+
DEFAULT: 'default',
|
|
602
|
+
};
|
|
603
|
+
function applyAddConstraint(call, args, scope, run) {
|
|
604
|
+
const ref = readTableRef(args[0], scope);
|
|
605
|
+
const options = args[1] === undefined
|
|
606
|
+
? new Map()
|
|
607
|
+
: objectEntries(args[1], scope.ctx).entries;
|
|
608
|
+
const kind = CONSTRAINT_KINDS[(readString(options.get('type'), scope) ?? '').toUpperCase()];
|
|
609
|
+
const fields = readStringList(options.get('fields'), scope);
|
|
610
|
+
const typeText = readString(options.get('type'), scope);
|
|
611
|
+
if (ref !== undefined && typeText !== undefined && kind === undefined) {
|
|
612
|
+
warn(run, `${where(scope, call)}: addConstraint ${describeTable(ref)}: the constraint type "${typeText}" is not unique, primary key, foreign key, check or default; the statement was skipped.`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (ref === undefined ||
|
|
616
|
+
kind === undefined ||
|
|
617
|
+
fields === undefined ||
|
|
618
|
+
fields.length === 0) {
|
|
619
|
+
warn(run, `${where(scope, call)}: addConstraint: the table, type or fields cannot be read statically; the statement was skipped.`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const action = `addConstraint ${describeTable(ref)} (${fields.join(', ')})`;
|
|
623
|
+
const table = findTable(run.state, ref.name, ref.schema);
|
|
624
|
+
if (table === undefined) {
|
|
625
|
+
skip(run, scope, call, action, 'the table does not exist at this point');
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const columns = [];
|
|
629
|
+
for (const field of fields) {
|
|
630
|
+
const column = findColumn(table, field);
|
|
631
|
+
if (column === undefined) {
|
|
632
|
+
skip(run, scope, call, action, `the column "${field}" does not exist at this point`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
columns.push(column);
|
|
636
|
+
}
|
|
637
|
+
const explicitName = readString(options.get('name'), scope);
|
|
638
|
+
let referencedTable;
|
|
639
|
+
if (kind === 'foreign key') {
|
|
640
|
+
const references = objectEntries(options.get('references') ?? call, scope.ctx).entries;
|
|
641
|
+
const target = readTableRef(references.get('table'), scope);
|
|
642
|
+
const key = readString(references.get('field'), scope) ??
|
|
643
|
+
readStringList(references.get('fields'), scope)?.[0];
|
|
644
|
+
if (target === undefined || key === undefined) {
|
|
645
|
+
skip(run, scope, call, action, 'the referenced table or column cannot be read statically');
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (fields.length > 1) {
|
|
649
|
+
skip(run, scope, call, action, 'a foreign key over several columns is not part of the shared model');
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
referencedTable = target.name;
|
|
653
|
+
const column = columns[0];
|
|
654
|
+
if (column !== undefined) {
|
|
655
|
+
column.references = {
|
|
656
|
+
model: { kind: 'string', value: target.name },
|
|
657
|
+
key,
|
|
658
|
+
};
|
|
659
|
+
const onDelete = readString(options.get('onDelete'), scope);
|
|
660
|
+
const onUpdate = readString(options.get('onUpdate'), scope);
|
|
661
|
+
if (onDelete === undefined) {
|
|
662
|
+
delete column.onDelete;
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
column.onDelete = onDelete;
|
|
666
|
+
}
|
|
667
|
+
if (onUpdate === undefined) {
|
|
668
|
+
delete column.onUpdate;
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
column.onUpdate = onUpdate;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
else if (kind === 'primary key') {
|
|
676
|
+
for (const column of columns) {
|
|
677
|
+
column.primaryKey = true;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
else if (kind === 'default') {
|
|
681
|
+
const value = options.get('defaultValue');
|
|
682
|
+
const column = columns[0];
|
|
683
|
+
if (value !== undefined && column !== undefined) {
|
|
684
|
+
column.defaultValue = valueOf(value, scope.ctx);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
else if (kind === 'check') {
|
|
688
|
+
warn(run, `${where(scope, call)}: ${action}: a check constraint is not part of the shared model; the condition was ignored.`);
|
|
689
|
+
}
|
|
690
|
+
const reason = addConstraint(table, {
|
|
691
|
+
kind,
|
|
692
|
+
name: explicitName ??
|
|
693
|
+
defaultConstraintName(table.name, kind, fields, referencedTable),
|
|
694
|
+
columns: fields,
|
|
695
|
+
});
|
|
696
|
+
if (reason !== undefined) {
|
|
697
|
+
skip(run, scope, call, action, reason);
|
|
698
|
+
}
|
|
699
|
+
run.touched.push(table.name);
|
|
700
|
+
}
|
|
701
|
+
function applyRemoveConstraint(call, args, scope, run) {
|
|
702
|
+
const ref = readTableRef(args[0], scope);
|
|
703
|
+
const name = readString(args[1], scope);
|
|
704
|
+
if (ref === undefined || name === undefined) {
|
|
705
|
+
warn(run, `${where(scope, call)}: removeConstraint: the table or constraint name cannot be read statically; the statement was skipped.`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const table = findTable(run.state, ref.name, ref.schema);
|
|
709
|
+
const reason = table === undefined
|
|
710
|
+
? 'the table does not exist at this point'
|
|
711
|
+
: removeConstraint(table, name);
|
|
712
|
+
if (reason !== undefined) {
|
|
713
|
+
skip(run, scope, call, `removeConstraint ${describeTable(ref)} "${name}"`, reason);
|
|
714
|
+
}
|
|
715
|
+
run.touched.push(ref.name);
|
|
716
|
+
}
|
|
717
|
+
// -- Raw SQL ----------------------------------------------------------------------------------------
|
|
718
|
+
const SQL_TABLE = /\b(?:alter\s+table|create\s+table(?:\s+if\s+not\s+exists)?|drop\s+table(?:\s+if\s+exists)?|rename\s+table|create\s+(?:unique\s+)?index\s+\S+\s+on|drop\s+index\s+\S+\s+on|insert\s+into|update|delete\s+from)\s+[`"'[]?([A-Za-z0-9_.]+)/i;
|
|
719
|
+
function countRaw(call, args, scope, run) {
|
|
720
|
+
run.raw = tally(run.raw, where(scope, call));
|
|
721
|
+
const sql = readString(args[0], scope);
|
|
722
|
+
const table = sql === undefined ? undefined : SQL_TABLE.exec(sql)?.[1];
|
|
723
|
+
if (table !== undefined) {
|
|
724
|
+
run.rawTables.add(table);
|
|
725
|
+
run.touched.push(table);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// -- Walking ----------------------------------------------------------------------------------------
|
|
729
|
+
function isQueryInterface(node, scope) {
|
|
730
|
+
const stripped = stripWrappers(node);
|
|
731
|
+
return (stripped.type === 'identifier' && scope.queryInterfaces.has(stripped.text));
|
|
732
|
+
}
|
|
733
|
+
/** `queryInterface.sequelize` (or `queryInterface.sequelize.getQueryInterface()` is not followed). */
|
|
734
|
+
function isQuerySequelize(node, scope) {
|
|
735
|
+
const stripped = stripWrappers(node);
|
|
736
|
+
if (stripped.type !== 'member_expression') {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
const object = stripped.childForFieldName('object');
|
|
740
|
+
return (object !== null &&
|
|
741
|
+
isQueryInterface(object, scope) &&
|
|
742
|
+
stripped.childForFieldName('property')?.text === 'sequelize');
|
|
743
|
+
}
|
|
744
|
+
function runQueryInterfaceCall(method, call, scope, run) {
|
|
745
|
+
const args = argsOf(call);
|
|
746
|
+
switch (method) {
|
|
747
|
+
case 'createTable':
|
|
748
|
+
applyCreateTable(call, args, scope, run);
|
|
749
|
+
return;
|
|
750
|
+
case 'addColumn':
|
|
751
|
+
applyAddColumn(call, args, scope, run);
|
|
752
|
+
return;
|
|
753
|
+
case 'removeColumn':
|
|
754
|
+
applyRemoveColumn(call, args, scope, run);
|
|
755
|
+
return;
|
|
756
|
+
case 'renameColumn':
|
|
757
|
+
applyRenameColumn(call, args, scope, run);
|
|
758
|
+
return;
|
|
759
|
+
case 'renameTable':
|
|
760
|
+
applyRenameTable(call, args, scope, run);
|
|
761
|
+
return;
|
|
762
|
+
case 'changeColumn':
|
|
763
|
+
applyChangeColumn(call, args, scope, run);
|
|
764
|
+
return;
|
|
765
|
+
case 'addIndex':
|
|
766
|
+
applyAddIndex(call, args, scope, run);
|
|
767
|
+
return;
|
|
768
|
+
case 'removeIndex':
|
|
769
|
+
applyRemoveIndex(call, args, scope, run);
|
|
770
|
+
return;
|
|
771
|
+
case 'addConstraint':
|
|
772
|
+
applyAddConstraint(call, args, scope, run);
|
|
773
|
+
return;
|
|
774
|
+
case 'removeConstraint':
|
|
775
|
+
applyRemoveConstraint(call, args, scope, run);
|
|
776
|
+
return;
|
|
777
|
+
case 'dropTable':
|
|
778
|
+
applyDropTable(call, args, scope, run);
|
|
779
|
+
return;
|
|
780
|
+
default:
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
if (DATA_CALLS.has(method)) {
|
|
784
|
+
run.data = tally(run.data, where(scope, call));
|
|
785
|
+
const table = readTableRef(args[0], scope);
|
|
786
|
+
if (table !== undefined) {
|
|
787
|
+
run.touched.push(table.name);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
else if (method === 'sql' || method === 'query') {
|
|
791
|
+
countRaw(call, args, scope, run);
|
|
792
|
+
}
|
|
793
|
+
else if (!READ_ONLY_CALLS.has(method)) {
|
|
794
|
+
run.unsupported.set(method, tally(run.unsupported.get(method), where(scope, call)));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/** The statements of a function body, or the expression of an arrow function that has no block. */
|
|
798
|
+
function bodyStatements(fn) {
|
|
799
|
+
const body = fn.childForFieldName('body');
|
|
800
|
+
if (body === null) {
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
return body.type === 'statement_block' ? body.namedChildren : [body];
|
|
804
|
+
}
|
|
805
|
+
function walkFunction(fn, scope, run, extra = []) {
|
|
806
|
+
if (scope.active.has(fn)) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const inner = {
|
|
810
|
+
...scope,
|
|
811
|
+
queryInterfaces: new Set([...scope.queryInterfaces, ...extra]),
|
|
812
|
+
active: new Set([...scope.active, fn]),
|
|
813
|
+
};
|
|
814
|
+
walkStatements(bodyStatements(fn), inner, run);
|
|
815
|
+
}
|
|
816
|
+
function walkExpression(node, scope, run) {
|
|
817
|
+
const expression = stripWrappers(node);
|
|
818
|
+
if (expression.type === 'sequence_expression') {
|
|
819
|
+
for (const part of expression.namedChildren) {
|
|
820
|
+
walkExpression(part, scope, run);
|
|
821
|
+
}
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (expression.type !== 'call_expression') {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const callee = expression.childForFieldName('function');
|
|
828
|
+
if (callee === null) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const target = stripWrappers(callee);
|
|
832
|
+
const args = argsOf(expression);
|
|
833
|
+
if (target.type === 'identifier') {
|
|
834
|
+
// A helper declared in the same file: read its body with the query interface it was given.
|
|
835
|
+
const helper = scope.functions.get(target.text);
|
|
836
|
+
if (helper !== undefined) {
|
|
837
|
+
const names = [];
|
|
838
|
+
parametersOf(helper).forEach((parameter, at) => {
|
|
839
|
+
const argument = args[at];
|
|
840
|
+
const bound = parameterName(parameter);
|
|
841
|
+
if (argument !== undefined &&
|
|
842
|
+
bound !== undefined &&
|
|
843
|
+
isQueryInterface(argument, scope)) {
|
|
844
|
+
names.push(bound);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
walkFunction(helper, scope, run, names);
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (target.type !== 'member_expression') {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const object = target.childForFieldName('object');
|
|
855
|
+
const property = target.childForFieldName('property');
|
|
856
|
+
if (object === null || property === null) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const method = property.text;
|
|
860
|
+
if (isQueryInterface(object, scope)) {
|
|
861
|
+
runQueryInterfaceCall(method, expression, scope, run);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (isQuerySequelize(object, scope)) {
|
|
865
|
+
if (method === 'query') {
|
|
866
|
+
countRaw(expression, args, scope, run);
|
|
867
|
+
}
|
|
868
|
+
else if (method === 'transaction') {
|
|
869
|
+
const handler = asFunction(args[args.length - 1], scope.functions);
|
|
870
|
+
if (handler !== undefined) {
|
|
871
|
+
walkFunction(handler, scope, run);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (object.text === 'Promise') {
|
|
877
|
+
if (method === 'all' || method === 'allSettled' || method === 'race') {
|
|
878
|
+
const list = args[0] === undefined ? undefined : resolveNode(args[0], scope.ctx);
|
|
879
|
+
if (list !== undefined && list.type === 'array') {
|
|
880
|
+
for (const item of list.namedChildren) {
|
|
881
|
+
walkExpression(item, scope, run);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
else if (list !== undefined) {
|
|
885
|
+
walkComputedList(list, scope, run);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (method === 'then' || method === 'catch' || method === 'finally') {
|
|
891
|
+
// `createTable(...).then(() => addIndex(...))`: the receiver runs first.
|
|
892
|
+
walkExpression(object, scope, run);
|
|
893
|
+
if (method === 'then') {
|
|
894
|
+
const handler = asFunction(args[0], scope.functions);
|
|
895
|
+
if (handler !== undefined) {
|
|
896
|
+
walkFunction(handler, scope, run);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/** `Promise.all(list.map(...))` and similar: the calls depend on run-time values, so they are not replayed. */
|
|
902
|
+
function walkComputedList(list, scope, run) {
|
|
903
|
+
let mentionsQueryInterface = false;
|
|
904
|
+
const stack = [list];
|
|
905
|
+
while (stack.length > 0 && !mentionsQueryInterface) {
|
|
906
|
+
const node = stack.pop();
|
|
907
|
+
if (node === undefined) {
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
if (node.type === 'identifier' && scope.queryInterfaces.has(node.text)) {
|
|
911
|
+
mentionsQueryInterface = true;
|
|
912
|
+
}
|
|
913
|
+
stack.push(...node.namedChildren);
|
|
914
|
+
}
|
|
915
|
+
if (mentionsQueryInterface) {
|
|
916
|
+
run.unsupported.set(COMPUTED_LIST, tally(run.unsupported.get(COMPUTED_LIST), where(scope, list)));
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function nestedStatements(node) {
|
|
920
|
+
const found = [];
|
|
921
|
+
for (const child of node.namedChildren) {
|
|
922
|
+
if (CLAUSE_STATEMENTS.has(child.type)) {
|
|
923
|
+
found.push(...nestedStatements(child));
|
|
924
|
+
}
|
|
925
|
+
else if (child.type === 'statement_block' ||
|
|
926
|
+
child.type.endsWith('_statement')) {
|
|
927
|
+
found.push(child);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return found;
|
|
931
|
+
}
|
|
932
|
+
function controlLabel(node) {
|
|
933
|
+
switch (node.type) {
|
|
934
|
+
case 'if_statement':
|
|
935
|
+
return 'a conditional';
|
|
936
|
+
case 'switch_statement':
|
|
937
|
+
return 'a switch';
|
|
938
|
+
default:
|
|
939
|
+
return 'a loop';
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function walkStatements(nodes, scope, run) {
|
|
943
|
+
for (const node of nodes) {
|
|
944
|
+
switch (node.type) {
|
|
945
|
+
case 'comment':
|
|
946
|
+
break;
|
|
947
|
+
case 'statement_block':
|
|
948
|
+
walkStatements(node.namedChildren, scope, run);
|
|
949
|
+
break;
|
|
950
|
+
case 'expression_statement':
|
|
951
|
+
case 'return_statement': {
|
|
952
|
+
const expression = node.namedChildren.find((child) => child.type !== 'comment');
|
|
953
|
+
if (expression !== undefined) {
|
|
954
|
+
walkExpression(expression, scope, run);
|
|
955
|
+
}
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
case 'lexical_declaration':
|
|
959
|
+
case 'variable_declaration':
|
|
960
|
+
for (const declarator of node.namedChildren) {
|
|
961
|
+
const value = declarator.childForFieldName('value');
|
|
962
|
+
if (value !== null) {
|
|
963
|
+
walkExpression(value, scope, run);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
break;
|
|
967
|
+
default:
|
|
968
|
+
if (node.type === 'try_statement') {
|
|
969
|
+
// `try { ...; await transaction.commit() } catch { await transaction.rollback(); throw }` is the
|
|
970
|
+
// usual shape of a migration: the body is what runs, the handlers only undo it.
|
|
971
|
+
const body = node.childForFieldName('body');
|
|
972
|
+
if (body !== null) {
|
|
973
|
+
walkStatements(body.namedChildren, scope, run);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
else if (CONTROL_STATEMENTS.has(node.type)) {
|
|
977
|
+
const before = run.touched.length;
|
|
978
|
+
walkStatements(nestedStatements(node), scope, run);
|
|
979
|
+
const tables = [...new Set(run.touched.slice(before))];
|
|
980
|
+
warn(run, `${where(scope, node)}: ${controlLabel(node)} contains schema changes; it was applied as if it always ran` +
|
|
981
|
+
(tables.length > 0
|
|
982
|
+
? ` (tables: ${tables.join(', ')}), so the result may differ from the real database.`
|
|
983
|
+
: ', so the result may differ from the real database.'));
|
|
984
|
+
}
|
|
985
|
+
else if (!node.type.endsWith('_declaration')) {
|
|
986
|
+
// Anything else that is an expression in disguise (a bare call in an arrow body).
|
|
987
|
+
walkExpression(node, scope, run);
|
|
988
|
+
}
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// ---------------------------------------------------------------------------
|
|
994
|
+
// Entry point
|
|
995
|
+
// ---------------------------------------------------------------------------
|
|
996
|
+
function pluralCount(count, singular, plural) {
|
|
997
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
998
|
+
}
|
|
999
|
+
function reportSummaries(run) {
|
|
1000
|
+
if (run.raw !== undefined) {
|
|
1001
|
+
const tables = [...run.rawTables].join(', ');
|
|
1002
|
+
run.project.warnings.push(`Skipped ${pluralCount(run.raw.count, 'raw SQL query', 'raw SQL queries')} (first at ${run.raw.first}${tables === '' ? '' : `, tables: ${tables}`}): raw SQL is not replayed, so the schema may differ from the real database.`);
|
|
1003
|
+
}
|
|
1004
|
+
if (run.data !== undefined) {
|
|
1005
|
+
run.project.warnings.push(`Skipped ${pluralCount(run.data.count, 'data statement', 'data statements')} (first at ${run.data.first}): row data is not part of the schema.`);
|
|
1006
|
+
}
|
|
1007
|
+
for (const [method, counter] of run.unsupported) {
|
|
1008
|
+
const what = method === COMPUTED_LIST
|
|
1009
|
+
? pluralCount(counter.count, 'call made from a list computed at run time', 'calls made from a list computed at run time')
|
|
1010
|
+
: pluralCount(counter.count, `${method} call`, `${method} calls`);
|
|
1011
|
+
run.project.warnings.push(`Skipped ${what} (first at ${counter.first}): the change is not replayed, so the schema may differ from the real database.`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/** The options of a table read from migrations: the database's names, none of Sequelize's own columns. */
|
|
1015
|
+
function tableOptions(table) {
|
|
1016
|
+
return {
|
|
1017
|
+
tableName: table.name,
|
|
1018
|
+
freezeTableName: true,
|
|
1019
|
+
timestamps: false,
|
|
1020
|
+
paranoid: false,
|
|
1021
|
+
underscored: false,
|
|
1022
|
+
...(table.schema === undefined ? {} : { schema: table.schema }),
|
|
1023
|
+
createdAt: false,
|
|
1024
|
+
updatedAt: false,
|
|
1025
|
+
deletedAt: false,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
/** The model name for a table: `blog_posts` becomes `BlogPost`, and a clash gets a number. */
|
|
1029
|
+
function modelNameFor(table, taken) {
|
|
1030
|
+
let base = toPascalCase(singularize(toSnakeCase(table.name)));
|
|
1031
|
+
if (base === '') {
|
|
1032
|
+
base = 'Model';
|
|
1033
|
+
}
|
|
1034
|
+
if (/^\d/.test(base)) {
|
|
1035
|
+
base = `T${base}`;
|
|
1036
|
+
}
|
|
1037
|
+
if (taken.has(base) && table.schema !== undefined) {
|
|
1038
|
+
base = `${toPascalCase(table.schema)}${base}`;
|
|
1039
|
+
}
|
|
1040
|
+
let name = base;
|
|
1041
|
+
for (let suffix = 2; taken.has(name); suffix += 1) {
|
|
1042
|
+
name = `${base}${suffix}`;
|
|
1043
|
+
}
|
|
1044
|
+
taken.add(name);
|
|
1045
|
+
return name;
|
|
1046
|
+
}
|
|
1047
|
+
function toRawModel(table, modelName) {
|
|
1048
|
+
const indexes = [
|
|
1049
|
+
...table.indexes.map((entry) => ({
|
|
1050
|
+
...entry.index,
|
|
1051
|
+
fields: entry.index.fields.map((field) => ({ ...field })),
|
|
1052
|
+
})),
|
|
1053
|
+
...table.constraints
|
|
1054
|
+
.filter((constraint) => constraint.kind === 'unique')
|
|
1055
|
+
.map((constraint) => ({
|
|
1056
|
+
fields: constraint.columns.map((name) => ({
|
|
1057
|
+
name,
|
|
1058
|
+
})),
|
|
1059
|
+
unique: true,
|
|
1060
|
+
name: constraint.name,
|
|
1061
|
+
isPartial: false,
|
|
1062
|
+
hasExpression: false,
|
|
1063
|
+
})),
|
|
1064
|
+
];
|
|
1065
|
+
return {
|
|
1066
|
+
file: table.file,
|
|
1067
|
+
style: 'migration',
|
|
1068
|
+
modelName,
|
|
1069
|
+
aliases: [],
|
|
1070
|
+
attributes: table.columns,
|
|
1071
|
+
optionEntries: new Map(),
|
|
1072
|
+
decoratorOptions: {},
|
|
1073
|
+
indexes,
|
|
1074
|
+
foreignKeyHints: new Map(),
|
|
1075
|
+
accessors: [],
|
|
1076
|
+
unsupported: [],
|
|
1077
|
+
location: `table "${table.name}"`,
|
|
1078
|
+
presetOptions: tableOptions(table),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
/** Migration files in the order they run: by file name, as `sequelize-cli` (umzug) sorts them. */
|
|
1082
|
+
export function orderMigrationFiles(files) {
|
|
1083
|
+
return [...files].sort((left, right) => {
|
|
1084
|
+
if (left.label !== right.label) {
|
|
1085
|
+
return left.label < right.label ? -1 : 1;
|
|
1086
|
+
}
|
|
1087
|
+
return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Replays the `up` functions of the migration files and returns one model per table that exists at the end.
|
|
1092
|
+
* Warnings go to `project.warnings`.
|
|
1093
|
+
*/
|
|
1094
|
+
export function replayMigrations(files, project) {
|
|
1095
|
+
const run = {
|
|
1096
|
+
state: newState(),
|
|
1097
|
+
project,
|
|
1098
|
+
raw: undefined,
|
|
1099
|
+
rawTables: new Set(),
|
|
1100
|
+
data: undefined,
|
|
1101
|
+
unsupported: new Map(),
|
|
1102
|
+
touched: [],
|
|
1103
|
+
};
|
|
1104
|
+
for (const file of orderMigrationFiles(files)) {
|
|
1105
|
+
const ctx = { file, project };
|
|
1106
|
+
const up = findUp(file, ctx);
|
|
1107
|
+
if (up === undefined) {
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
const scope = {
|
|
1111
|
+
file,
|
|
1112
|
+
ctx,
|
|
1113
|
+
queryInterfaces: new Set(up.queryInterfaces),
|
|
1114
|
+
functions: declaredFunctions(file),
|
|
1115
|
+
active: new Set([up.node]),
|
|
1116
|
+
};
|
|
1117
|
+
walkStatements(bodyStatements(up.node), scope, run);
|
|
1118
|
+
}
|
|
1119
|
+
reportSummaries(run);
|
|
1120
|
+
const taken = new Set();
|
|
1121
|
+
return [...run.state.tables.values()].map((table) => toRawModel(table, modelNameFor(table, taken)));
|
|
1122
|
+
}
|
|
1123
|
+
//# sourceMappingURL=sequelizeMigrations.js.map
|